mirror of
https://github.com/apple/swift-nio-extras.git
synced 2025-05-14 00:42:41 +08:00
180 lines
6.9 KiB
Swift
180 lines
6.9 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the SwiftNIO open source project
|
|
//
|
|
// Copyright (c) 2020-2021 Apple Inc. and the SwiftNIO project authors
|
|
// Licensed under Apache License v2.0
|
|
//
|
|
// See LICENSE.txt for license information
|
|
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
|
|
//
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
import CNIOExtrasZlib
|
|
import NIOCore
|
|
|
|
/// Namespace for compression code.
|
|
public enum NIOCompression: Sendable {
|
|
|
|
/// Which algorithm should be used for compression.
|
|
public struct Algorithm: CustomStringConvertible, Equatable, Sendable {
|
|
fileprivate enum AlgorithmEnum: String {
|
|
case gzip
|
|
case deflate
|
|
}
|
|
fileprivate let algorithm: AlgorithmEnum
|
|
|
|
/// return as String
|
|
public var description: String { algorithm.rawValue }
|
|
|
|
/// `gzip` method
|
|
public static let gzip = Algorithm(algorithm: .gzip)
|
|
/// `deflate` method
|
|
public static let deflate = Algorithm(algorithm: .deflate)
|
|
}
|
|
|
|
/// Error types for ``NIOCompression``
|
|
public struct Error: Swift.Error, CustomStringConvertible, Equatable {
|
|
fileprivate enum ErrorEnum: String {
|
|
case uncompressedWritesPending
|
|
case noDataToWrite
|
|
}
|
|
fileprivate let error: ErrorEnum
|
|
|
|
/// return as String
|
|
public var description: String { error.rawValue }
|
|
|
|
/// There were writes pending which were not processed before shutdown.
|
|
public static let uncompressedWritesPending = Error(error: .uncompressedWritesPending)
|
|
/// Currently never used.
|
|
public static let noDataToWrite = Error(error: .noDataToWrite)
|
|
}
|
|
|
|
/// Data compression utility.
|
|
struct Compressor {
|
|
private var stream = z_stream()
|
|
var isActive = false
|
|
|
|
init() {}
|
|
|
|
/// Set up the encoder for compressing data according to a specific
|
|
/// algorithm.
|
|
mutating func initialize(encoding: Algorithm) {
|
|
assert(!isActive)
|
|
isActive = true
|
|
// zlib docs say: The application must initialize zalloc, zfree and opaque before calling the init function.
|
|
stream.zalloc = nil
|
|
stream.zfree = nil
|
|
stream.opaque = nil
|
|
|
|
let windowBits: Int32
|
|
switch encoding.algorithm {
|
|
case .deflate:
|
|
windowBits = 15
|
|
case .gzip:
|
|
windowBits = 16 + 15
|
|
}
|
|
|
|
let rc = CNIOExtrasZlib_deflateInit2(
|
|
&stream,
|
|
Z_DEFAULT_COMPRESSION,
|
|
Z_DEFLATED,
|
|
windowBits,
|
|
8,
|
|
Z_DEFAULT_STRATEGY
|
|
)
|
|
precondition(rc == Z_OK, "Unexpected return from zlib init: \(rc)")
|
|
}
|
|
|
|
mutating func compress(
|
|
inputBuffer: inout ByteBuffer,
|
|
allocator: ByteBufferAllocator,
|
|
finalise: Bool
|
|
) -> ByteBuffer {
|
|
assert(isActive)
|
|
let flags = finalise ? Z_FINISH : Z_SYNC_FLUSH
|
|
// don't compress an empty buffer if we aren't finishing the compress
|
|
guard inputBuffer.readableBytes > 0 || finalise == true else { return allocator.buffer(capacity: 0) }
|
|
// deflateBound() provides an upper limit on the number of bytes the input can
|
|
// compress to. We add 5 bytes to handle the fact that Z_SYNC_FLUSH will append
|
|
// an empty stored block that is 5 bytes long.
|
|
// From zlib docs (https://www.zlib.net/manual.html)
|
|
// If the parameter flush is set to Z_SYNC_FLUSH, all pending output is flushed to the output buffer and the output is
|
|
// aligned on a byte boundary, so that the decompressor can get all input data available so far. (In particular avail_in
|
|
// is zero after the call if enough output space has been provided before the call.) Flushing may degrade compression for
|
|
// some compression algorithms and so it should be used only when necessary. This completes the current deflate block and
|
|
// follows it with an empty stored block that is three bits plus filler bits to the next byte, followed by four bytes
|
|
// (00 00 ff ff).
|
|
let bufferSize = Int(deflateBound(&stream, UInt(inputBuffer.readableBytes)))
|
|
var outputBuffer = allocator.buffer(capacity: bufferSize + 5)
|
|
stream.oneShotDeflate(from: &inputBuffer, to: &outputBuffer, flag: flags)
|
|
return outputBuffer
|
|
}
|
|
|
|
mutating func shutdown() {
|
|
assert(isActive)
|
|
isActive = false
|
|
deflateEnd(&stream)
|
|
}
|
|
|
|
mutating func shutdownIfActive() {
|
|
if isActive {
|
|
isActive = false
|
|
deflateEnd(&stream)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension z_stream {
|
|
/// Executes deflate from one buffer to another buffer. The advantage of this method is that it
|
|
/// will ensure that the stream is "safe" after each call (that is, that the stream does not have
|
|
/// pointers to byte buffers any longer).
|
|
mutating func oneShotDeflate(from: inout ByteBuffer, to: inout ByteBuffer, flag: Int32) {
|
|
defer {
|
|
self.avail_in = 0
|
|
self.next_in = nil
|
|
self.avail_out = 0
|
|
self.next_out = nil
|
|
}
|
|
|
|
from.readWithUnsafeMutableReadableBytes { dataPtr in
|
|
let typedPtr = dataPtr.baseAddress!.assumingMemoryBound(to: UInt8.self)
|
|
let typedDataPtr = UnsafeMutableBufferPointer(
|
|
start: typedPtr,
|
|
count: dataPtr.count
|
|
)
|
|
|
|
self.avail_in = UInt32(typedDataPtr.count)
|
|
self.next_in = typedDataPtr.baseAddress!
|
|
|
|
let rc = deflateToBuffer(buffer: &to, flag: flag)
|
|
precondition(rc == Z_OK || rc == Z_STREAM_END, "One-shot compression failed: \(rc)")
|
|
|
|
return typedDataPtr.count - Int(self.avail_in)
|
|
}
|
|
}
|
|
|
|
/// A private function that sets the deflate target buffer and then calls deflate.
|
|
/// This relies on having the input set by the previous caller: it will use whatever input was
|
|
/// configured.
|
|
private mutating func deflateToBuffer(buffer: inout ByteBuffer, flag: Int32) -> Int32 {
|
|
var rc = Z_OK
|
|
|
|
buffer.writeWithUnsafeMutableBytes(minimumWritableBytes: buffer.capacity) { outputPtr in
|
|
let typedOutputPtr = UnsafeMutableBufferPointer(
|
|
start: outputPtr.baseAddress!.assumingMemoryBound(to: UInt8.self),
|
|
count: outputPtr.count
|
|
)
|
|
self.avail_out = UInt32(typedOutputPtr.count)
|
|
self.next_out = typedOutputPtr.baseAddress!
|
|
rc = deflate(&self, flag)
|
|
return typedOutputPtr.count - Int(self.avail_out)
|
|
}
|
|
|
|
return rc
|
|
}
|
|
}
|