blob: 265b18d23471da401360126adc45bc5b6b519922 [file] [log] [blame]
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
import Darwin
import Foundation
@_exported import Compression
/// Compression algorithms, wraps the C API constants.
public enum Algorithm: CaseIterable {
/// LZFSE
case lzfse
/// Deflate (conforming to RFC 1951)
case zlib
/// LZ4 with simple frame encapsulation
case lz4
/// LZMA in a XZ container
case lzma
public var rawValue: compression_algorithm {
switch self {
case .lzfse: return COMPRESSION_LZFSE
case .zlib: return COMPRESSION_ZLIB
case .lz4: return COMPRESSION_LZ4
case .lzma: return COMPRESSION_LZMA
}
}
}
/// Compression errors
public enum FilterError: Error {
/// Filter failed to initialize
case filterInitError
/// Invalid data in a call to compression_stream_process
case filterProcessError
/// Non-empty write after an output filter has been finalized
case writeToFinalizedFilter
}
/// Compression filter direction of operation, compress/decompress
public enum FilterOperation {
/// Compress raw data to a compressed payload
case compress
/// Decompress a compressed payload to raw data
case decompress
public var rawValue: compression_stream_operation {
switch self {
case .compress: return COMPRESSION_STREAM_ENCODE
case .decompress: return COMPRESSION_STREAM_DECODE
}
}
}
@available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
extension compression_stream {
/// Initialize a compression_stream struct
///
/// - Parameter operation: direction of operation
/// - Parameter algorithm: compression algorithm
///
/// - Throws: `FilterError.filterInitError` if `algorithm` is not supported
/// by the Compression stream API
///
internal init(operation: FilterOperation, algorithm: Algorithm) throws {
self.init(dst_ptr: UnsafeMutablePointer<UInt8>.allocate(capacity:0),
dst_size: 0,
src_ptr: UnsafeMutablePointer<UInt8>.allocate(capacity:0),
src_size: 0,
state: nil)
let status = compression_stream_init(&self, operation.rawValue, algorithm.rawValue)
guard status == COMPRESSION_STATUS_OK else { throw FilterError.filterInitError }
}
}
@available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
public class OutputFilter {
private var _stream: compression_stream
private var _buf: UnsafeMutablePointer<UInt8>
private let _bufCapacity: Int
private let _writeFunc: (Data?) throws -> ()
private var _finalized: Bool = false
/// Initialize an output filter
///
/// - Parameters:
/// - operation: direction of operation
/// - algorithm: compression algorithm
/// - bufferCapacity: capacity of the internal data buffer
/// - writeFunc: called to write the processed data
///
/// - Throws: `FilterError.StreamInitError` if stream initialization failed
public init(
_ operation: FilterOperation,
using algorithm: Algorithm,
bufferCapacity: Int = 65536,
writingTo writeFunc: @escaping (Data?) throws -> ()
) throws {
_stream = try compression_stream(operation: operation, algorithm: algorithm)
_buf = UnsafeMutablePointer<UInt8>.allocate(capacity: bufferCapacity)
_bufCapacity = bufferCapacity
_writeFunc = writeFunc
}
/// Send data to output filter
///
/// Processed output will be sent to the output closure.
/// A call with empty/nil data is interpreted as finalize().
/// Writing non empty/nil data to a finalized stream is an error.
///
/// - Parameter data: data to process
///
/// - Throws:
/// `FilterError.filterProcessError` if an error occurs during processing
/// `FilterError.writeToFinalizedFilter` if `data` is not empty/nil, and the
/// filter is the finalized state
public func write(_ data: Data?) throws {
// Finalize if data is empty/nil
if data == nil || data!.isEmpty { try finalize() ; return }
// Fail if already finalized
if _finalized { throw FilterError.writeToFinalizedFilter }
// Process all incoming data
try data!.withUnsafeBytes { (src_ptr: UnsafePointer<UInt8>) in
_stream.src_size = data!.count
_stream.src_ptr = src_ptr
while (_stream.src_size > 0) { _ = try process(finalizing: false) }
}
}
/// Finalize the stream, i.e. flush all data remaining in the stream
///
/// Processed output will be sent to the output closure.
/// When all output has been sent, the writingTo closure is called one last time with nil data.
/// Once the stream is finalized, writing non empty/nil data to the stream will throw an exception.
///
/// - Throws: `FilterError.StreamProcessError` if an error occurs during processing
public func finalize() throws {
// Do nothing if already finalized
if _finalized { return }
// Finalize stream
_stream.src_size = 0
var status = COMPRESSION_STATUS_OK
while (status != COMPRESSION_STATUS_END) { status = try process(finalizing: true) }
// Update state
_finalized = true
// Notify end of stream
try _writeFunc(nil)
}
// Cleanup resources. The filter is finalized now if it was not finalized yet.
deinit {
// Finalize now if not done earlier
try? finalize()
// Cleanup
_buf.deallocate()
compression_stream_destroy(&_stream)
}
// Call compression_stream_process with current src, and dst set to _buf, then write output to the closure
// Return status
private func process(finalizing finalize: Bool) throws -> compression_status {
// Process current input, and write to buf
_stream.dst_ptr = _buf
_stream.dst_size = _bufCapacity
let status = compression_stream_process(&_stream, (finalize ? Int32(COMPRESSION_STREAM_FINALIZE.rawValue) : 0))
guard status != COMPRESSION_STATUS_ERROR else { throw FilterError.filterProcessError }
// Number of bytes written to buf
let writtenBytes = _bufCapacity - _stream.dst_size
// Write output
if writtenBytes > 0 {
let outData = Data(bytesNoCopy: _buf, count: writtenBytes, deallocator: .none)
try _writeFunc(outData)
}
return status
}
}
@available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
public class InputFilter {
private var _stream: compression_stream
private var _buf: Data? = nil // current input data
private let _bufCapacity: Int // size to read when refilling _buf
private let _readFunc: (Int) throws -> Data?
private var _eofReached: Bool = false // did we read end-of-file from the input?
private var _endReached: Bool = false // did we reach end-of-file from the decoder stream?
/// Initialize an input filter
///
/// - Parameters:
/// - operation: direction of operation
/// - algorithm: compression algorithm
/// - bufferCapacity: capacity of the internal data buffer
/// - readFunc: called to read the input data
///
/// - Throws: `FilterError.filterInitError` if filter initialization failed
public init(
_ operation: FilterOperation,
using algorithm: Algorithm,
bufferCapacity: Int = 65536,
readingFrom readFunc: @escaping (Int) throws -> Data?
) throws {
_stream = try compression_stream(operation: operation, algorithm: algorithm)
_bufCapacity = bufferCapacity
_readFunc = readFunc
}
/// Read processed data from the filter
///
/// Input data, when needed, is obtained from the input closure
/// When the input closure returns a nil or empty Data object, the filter will be
/// finalized, and after all processed data has been read, readData will return nil
/// to signal end of input
///
/// - Parameter count: max number of bytes to read from the filter
///
/// - Returns: a new Data object containing at most `count` output bytes, or nil if no more data is available
///
/// - Throws:
/// `FilterError.filterProcessError` if an error occurs during processing
public func readData(ofLength count: Int) throws -> Data? {
// Sanity check
precondition(count > 0, "number of bytes to read can't be 0")
// End reached, return early, nothing to do
if _endReached { return nil }
// Allocate result
var result = Data(count: count)
try result.withUnsafeMutableBytes { (dst_ptr: UnsafeMutablePointer<UInt8>) in
// Write to result until full, or end reached
_stream.dst_size = count
_stream.dst_ptr = dst_ptr
while _stream.dst_size > 0 && !_endReached {
// Refill _buf if needed, and EOF was not yet read
if _stream.src_size == 0 && !_eofReached {
_buf = try _readFunc(_bufCapacity) // may be nil
// Reset src_size to full _buf size
if _buf?.count ?? 0 == 0 { _eofReached = true }
_stream.src_size = _buf?.count ?? 0
}
// Process some data
if let buf = _buf {
try buf.withUnsafeBytes { (src_ptr: UnsafePointer<UInt8>) in
// Next byte to read
_stream.src_ptr = src_ptr + buf.count - _stream.src_size
let status = compression_stream_process(&_stream, (_eofReached ? Int32(COMPRESSION_STREAM_FINALIZE.rawValue) : 0))
guard status != COMPRESSION_STATUS_ERROR else { throw FilterError.filterProcessError }
if status == COMPRESSION_STATUS_END { _endReached = true }
}
}
else {
let status = compression_stream_process(&_stream, (_eofReached ? Int32(COMPRESSION_STREAM_FINALIZE.rawValue) : 0))
guard status != COMPRESSION_STATUS_ERROR else { throw FilterError.filterProcessError }
if status == COMPRESSION_STATUS_END { _endReached = true }
}
}
} // result.withUnsafeMutableBytes
// Update actual size
result.count = count - _stream.dst_size
return result
}
// Cleanup resources
deinit {
compression_stream_destroy(&_stream)
}
}