2025-04-01 15:29:54 +01:00

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
}
}