diff --git a/Package.swift b/Package.swift index a1c6c16..9680490 100644 --- a/Package.swift +++ b/Package.swift @@ -6,89 +6,93 @@ let package = Package( name: "swift-compression", // MARK: Products products: [ - .library( - name: "SwiftCompression", - targets: ["SwiftCompression"] - ), - - .library( - name: "SwiftCompressionCSS", - targets: ["CompressionCSS"] - ), - - .library( - name: "SwiftCompressionDNA", - targets: ["CompressionDNA"] - ), - - .library( - name: "SwiftCompressionJavaScript", - targets: ["CompressionJavaScript"] - ), - - .library( - name: "SwiftCompressionLZ", - targets: ["CompressionLZ"] - ), + .library(name: "SwiftCompression", targets: ["SwiftCompression"]), + .library(name: "SwiftCompressionUtilities", targets: ["SwiftCompressionUtilities"]), - .library( - name: "SwiftCompressionSnappy", - targets: ["CompressionSnappy"] - ), + .library(name: "Brotli", targets: ["Brotli"]), + .library(name: "SwiftCompressionDNA", targets: ["CompressionDNA"]), + .library(name: "SwiftCompressionLZ", targets: ["CompressionLZ"]), + .library(name: "Huffman", targets: ["Huffman"]), + .library(name: "Snappy", targets: ["Snappy"]), + .library(name: "Zlib", targets: ["Zlib"]) ], // MARK: Targets targets: [ - .target( - name: "SwiftCompressionUtilities" - ), + .target(name: "ByteBuilder"), + .target(name: "FrequencyTables"), + .target(name: "SwiftCompressionUtilities"), .target( name: "SwiftCompression", dependencies: [ - "SwiftCompressionUtilities", - "CompressionCSS", + "Brotli", "CompressionDNA", - "CompressionJavaScript", "CompressionLZ", - "CompressionSnappy" + "Huffman", + "Snappy", + "Zlib", + "SwiftCompressionUtilities", ] ), // MARK: Techniques + .systemLibrary(name: "BrotliShim"), .target( - name: "CompressionCSS", + name: "Brotli", dependencies: [ - "SwiftCompressionUtilities" + "SwiftCompressionUtilities", + "BrotliShim" ], - path: "Sources/CSS" + linkerSettings: [ + .unsafeFlags(["-lbrotlienc", "-lbrotlidec"]), + ] ), + .systemLibrary(name: "ZlibShim"), .target( - name: "CompressionDNA", + name: "Zlib", dependencies: [ - "SwiftCompressionUtilities" - ], - path: "Sources/DNA" + "SwiftCompressionUtilities", + "ZlibShim" + ] ), + .target( - name: "CompressionJavaScript", + name: "CompressionDNA", dependencies: [ + "ByteBuilder", + "FrequencyTables", "SwiftCompressionUtilities" ], - path: "Sources/JavaScript" + path: "Sources/DNA" ), .target( name: "CompressionLZ", dependencies: [ + "ByteBuilder", "SwiftCompressionUtilities" ], path: "Sources/LZ" ), + .target( - name: "CompressionSnappy", + name: "Huffman", dependencies: [ + "ByteBuilder", "SwiftCompressionUtilities" + ] + ), + + .systemLibrary(name: "SnappyShim"), + .target( + name: "Snappy", + dependencies: [ + "ByteBuilder", + "SwiftCompressionUtilities", + "SnappyShim" ], - path: "Sources/Snappy" + linkerSettings: [ + .unsafeFlags(["-lsnappy"]) + ] ), // MARK: Run @@ -100,30 +104,12 @@ let package = Package( ), // MARK: Unit tests - .testTarget( - name: "SwiftCompressionTests", - dependencies: ["SwiftCompression"] - ), - .testTarget( - name: "CSSTests", - dependencies: ["CompressionCSS"] - ), - .testTarget( - name: "DNATests", - dependencies: ["CompressionDNA"] - ), - .testTarget( - name: "JavaScriptTests", - dependencies: ["CompressionJavaScript"] - ), - .testTarget( - name: "LZTests", - dependencies: ["CompressionLZ"] - ), - .testTarget( - name: "SnappyTests", - dependencies: ["CompressionSnappy"] - ) + .testTarget(name: "SwiftCompressionTests", dependencies: ["SwiftCompression"]), + .testTarget(name: "BrotliTests", dependencies: ["Brotli"]), + .testTarget(name: "DNATests", dependencies: ["CompressionDNA"]), + .testTarget(name: "LZTests", dependencies: ["CompressionLZ"]), + .testTarget(name: "SnappyTests", dependencies: ["Snappy"]), + .testTarget(name: "ZlibTests", dependencies: ["Zlib"]) ] ) diff --git a/README.md b/README.md index 08501b0..823daec 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Swift Compression -Requires at least Swift 5.9 Apache 2.0 License +Requires at least Swift 5.9 Apache 2.0 License Standalone compression & decompression library. @@ -8,26 +8,22 @@ Standalone compression & decompression library. ### Compression and decompression -- [Brotli](https://github.com/google/brotli) -- [DEFLATE](https://www.rfc-editor.org/rfc/rfc1951) -- [DNA Binary Encoding](https://en.wikipedia.org/wiki/DNA_digital_data_storage) -- [DNA Single-Block Encoding](https://www.mdpi.com/1999-4893/13/4/99) -- [Huffman Coding](https://en.wikipedia.org/wiki/Huffman_coding) -- [iWork Archive](https://en.wikipedia.org/wiki/IWork) -- [LZ77 & LZ78](https://en.wikipedia.org/wiki/LZ77_and_LZ78) -- [Move-to-front](https://en.wikipedia.org/wiki/Move-to-front_transform) -- [Run-Length Encoding](https://en.wikipedia.org/wiki/Run-length_encoding) -- [Snappy](https://github.com/google/snappy) -- [Snappy Framed](https://github.com/google/snappy/blob/main/framing_format.txt) -- [Zstd](https://github.com/facebook/zstd) - -### Minification - -- CSS -- JavaScript -- Swift - -## Getting starting +- [x] [Brotli](https://github.com/google/brotli) +- [ ] [DEFLATE](https://www.rfc-editor.org/rfc/rfc1951) + - [x] via zlib + - [ ] via libdeflate +- [x] [DNA Binary Encoding](https://en.wikipedia.org/wiki/DNA_digital_data_storage) +- [ ] [DNA Single-Block Encoding](https://www.mdpi.com/1999-4893/13/4/99) +- [x] [Huffman Coding](https://en.wikipedia.org/wiki/Huffman_coding) +- [ ] [iWork Archive](https://en.wikipedia.org/wiki/IWork) +- [ ] [LZ77 & LZ78](https://en.wikipedia.org/wiki/LZ77_and_LZ78) +- [x] [Move-to-front](https://en.wikipedia.org/wiki/Move-to-front_transform) +- [x] [Run-Length Encoding](https://en.wikipedia.org/wiki/Run-length_encoding) +- [x] [Snappy](https://github.com/google/snappy) +- [ ] [Snappy Framed](https://github.com/google/snappy/blob/main/framing_format.txt) +- [ ] [Zstd](https://github.com/facebook/zstd) + +## Getting started coming soon... @@ -36,11 +32,11 @@ coming soon... The products are broken up into their respective algorithms, but the __SwiftCompression__ product includes all of them and is the recommended one to use in your projects. - SwiftCompression -- SwiftCompressionCSS - SwiftCompressionDNA -- SwiftCompressionJavaScript - SwiftCompressionLZ -- SwiftCompressionSnappy +- Brotli +- Snappy +- Zlib ## Contributing diff --git a/Sources/Brotli/Brotli+Compress+Span.swift b/Sources/Brotli/Brotli+Compress+Span.swift new file mode 100644 index 0000000..6722ce0 --- /dev/null +++ b/Sources/Brotli/Brotli+Compress+Span.swift @@ -0,0 +1,28 @@ + +import BrotliShim + +extension Brotli { + public func compress( + span: Span, + configuration: CompressConfiguration + ) -> ConcreteCompressionResult { + var outLength = span.count + let maxSize = BrotliEncoderMaxCompressedSize(outLength) + let outBuffer = UnsafeMutableBufferPointer.allocate(capacity: maxSize) + outBuffer.initialize(repeating: 0) + defer { outBuffer.deallocate() } + + let success = span.withUnsafeBytes { + return BrotliEncoderCompress( + quality, + windowSize, + BrotliEncoderMode(rawValue: mode), + outLength, + $0.baseAddress, + &outLength, + outBuffer.baseAddress + ) + } + return success == BROTLI_TRUE ? [UInt8](outBuffer.prefix(outLength)) : nil + } +} \ No newline at end of file diff --git a/Sources/Brotli/Brotli+Compress.swift b/Sources/Brotli/Brotli+Compress.swift new file mode 100644 index 0000000..16c3bc0 --- /dev/null +++ b/Sources/Brotli/Brotli+Compress.swift @@ -0,0 +1,45 @@ + +import BrotliShim +import SwiftCompressionUtilities + +extension Brotli: Compressor { + public typealias ConcreteCompressionConfiguration = CompressConfiguration + public typealias ConcreteCompressionResult = [UInt8]? + + /// - Parameters: + /// - data: Sequence of bytes to compress. + /// - Complexity: O(_n_) where _n_ is the length of `data`. + public func compress( + data: some Collection, + configuration: ConcreteCompressionConfiguration + ) -> ConcreteCompressionResult { + var outLength = data.count + let maxSize = BrotliEncoderMaxCompressedSize(outLength) + let outBuffer = UnsafeMutableBufferPointer.allocate(capacity: maxSize) + outBuffer.initialize(repeating: 0) + defer { outBuffer.deallocate() } + + let success = data.withContiguousStorageIfAvailable { + return BrotliEncoderCompress( + quality, + windowSize, + BrotliEncoderMode(rawValue: mode), + outLength, + $0.baseAddress, + &outLength, + outBuffer.baseAddress + ) + } ?? BROTLI_FALSE + return success == BROTLI_TRUE ? [UInt8](outBuffer.prefix(outLength)) : nil + } +} + +// MARK: Configuration +extension Brotli { + public struct CompressConfiguration: CompressionConfiguration, DecompressionConfiguration { + public static var `default`: Self { .init() } + + public init() { + } + } +} \ No newline at end of file diff --git a/Sources/Brotli/Brotli+Decompress.swift b/Sources/Brotli/Brotli+Decompress.swift new file mode 100644 index 0000000..50e5302 --- /dev/null +++ b/Sources/Brotli/Brotli+Decompress.swift @@ -0,0 +1,52 @@ + +import BrotliShim +import SwiftCompressionUtilities + +extension Brotli: Decompressor { + public typealias ConcreteDecompressionConfiguration = DecompressConfiguration + public typealias ConcreteDecompressionResult = [UInt8]? + + /// - Parameters: + /// - data: Collection of bytes to decompress. + /// - closure: Logic to execute when a byte is decompressed. + /// - Complexity: O(_n_) where _n_ is the length of `data`. + public func decompress( + data: some Collection, + configuration: ConcreteDecompressionConfiguration + ) -> ConcreteDecompressionResult { + let compressedCount = data.count + var decodedSize = configuration.estimatedSize + let outBuffer = UnsafeMutableBufferPointer.allocate(capacity: decodedSize) + outBuffer.initialize(repeating: 0) + defer { outBuffer.deallocate() } + let result = data.withContiguousStorageIfAvailable { + BrotliDecoderDecompress( + compressedCount, + $0.baseAddress, + &decodedSize, + outBuffer.baseAddress + ) + } ?? BROTLI_DECODER_RESULT_ERROR + if result == BROTLI_DECODER_RESULT_SUCCESS { + return [UInt8](outBuffer.prefix(decodedSize)) + } else if result == BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT { + return decompress(data: data, configuration: .init(estimatedSize: configuration.estimatedSize * 2)) + } + return nil + } +} + +// MARK: Configuration +extension Brotli { + public struct DecompressConfiguration: DecompressionConfiguration { + public static var `default`: Self { .init(estimatedSize: 32768) } + + public let estimatedSize:Int + + public init( + estimatedSize: Int + ) { + self.estimatedSize = estimatedSize + } + } +} \ No newline at end of file diff --git a/Sources/Brotli/Brotli.swift b/Sources/Brotli/Brotli.swift new file mode 100644 index 0000000..d2261b0 --- /dev/null +++ b/Sources/Brotli/Brotli.swift @@ -0,0 +1,35 @@ + +import BrotliShim +import SwiftCompressionUtilities + +/// The Brotli compression technique. +/// +/// https://github.com/google/brotli +/// +/// https://en.wikipedia.org/wiki/Brotli +public struct Brotli: Sendable { + public let quality:Int32 + + /// Size of the window. + public let windowSize:Int32 + + public let mode:UInt32 + + public init( + quality: Int32 = BROTLI_DEFAULT_QUALITY, + windowSize: Int32 = BROTLI_DEFAULT_WINDOW, + mode: UInt32 = BROTLI_MODE_GENERIC.rawValue + ) { + self.quality = quality + self.windowSize = windowSize + self.mode = mode + } + + public var algorithm: CompressionAlgorithm { + .brotli(quality: quality, windowSize: windowSize, mode: mode) + } + + public var compressionQuality: CompressionQuality { + .lossless + } +} \ No newline at end of file diff --git a/Sources/BrotliShim/module.modulemap b/Sources/BrotliShim/module.modulemap new file mode 100644 index 0000000..30f3508 --- /dev/null +++ b/Sources/BrotliShim/module.modulemap @@ -0,0 +1,6 @@ +module BrotliShim [system] { + header "/usr/include/brotli/decode.h" + header "/usr/include/brotli/encode.h" + header "/usr/include/brotli/types.h" + export * +} \ No newline at end of file diff --git a/Sources/SwiftCompressionUtilities/ByteBuilder.swift b/Sources/ByteBuilder/ByteBuilder.swift similarity index 85% rename from Sources/SwiftCompressionUtilities/ByteBuilder.swift rename to Sources/ByteBuilder/ByteBuilder.swift index 52a08c8..6a3148f 100644 --- a/Sources/SwiftCompressionUtilities/ByteBuilder.swift +++ b/Sources/ByteBuilder/ByteBuilder.swift @@ -117,31 +117,7 @@ public struct ByteBuilder { bits = (false, false, false, false, false, false, false, false) } } - -// MARK: Stream & Data Builder -extension CompressionTechnique { - public struct DataBuilder { - public var data:[UInt8] - public var builder:ByteBuilder - - public init(data: [UInt8] = [], builder: ByteBuilder = ByteBuilder()) { - self.data = data - self.builder = builder - } - - public mutating func write(bit: Bool) { - if let wrote:UInt8 = builder.write(bit: bit) { - data.append(wrote) - } - } - public mutating func write(bits: [Bool]) { - builder.write(bits: bits, closure: { data.append($0) }) - } - public mutating func finalize() { - builder.flush(into: &data) - } - } -} +/* // MARK: StreamBuilder extension CompressionTechnique { @@ -167,4 +143,4 @@ extension CompressionTechnique { builder.flush(into: stream) } } -} \ No newline at end of file +}*/ \ No newline at end of file diff --git a/Sources/ByteBuilder/BytesBuilder.swift b/Sources/ByteBuilder/BytesBuilder.swift new file mode 100644 index 0000000..c15fcbd --- /dev/null +++ b/Sources/ByteBuilder/BytesBuilder.swift @@ -0,0 +1,22 @@ + +public struct BytesBuilder { + public var data:[UInt8] + public var builder:ByteBuilder + + public init(data: [UInt8] = [], builder: ByteBuilder = ByteBuilder()) { + self.data = data + self.builder = builder + } + + public mutating func write(bit: Bool) { + if let wrote:UInt8 = builder.write(bit: bit) { + data.append(wrote) + } + } + public mutating func write(bits: [Bool]) { + builder.write(bits: bits, closure: { data.append($0) }) + } + public mutating func finalize() { + builder.flush(into: &data) + } +} \ No newline at end of file diff --git a/Sources/SwiftCompressionUtilities/extensions/IntExtensions.swift b/Sources/ByteBuilder/IntExtensions.swift similarity index 100% rename from Sources/SwiftCompressionUtilities/extensions/IntExtensions.swift rename to Sources/ByteBuilder/IntExtensions.swift diff --git a/Sources/CSS/CSS.swift b/Sources/CSS/CSS.swift deleted file mode 100644 index 3bc0080..0000000 --- a/Sources/CSS/CSS.swift +++ /dev/null @@ -1,120 +0,0 @@ - -import SwiftCompressionUtilities - -extension CompressionTechnique { - - /// CSS compression techniques. - public static let css = CSS() - - public struct CSS: Compressor { - @inlinable - public var algorithm: CompressionAlgorithm { - .programmingLanguage(.css) - } - - @inlinable - public var quality: CompressionQuality { - .lossy - } - } -} - -// MARK: Compress -extension CompressionTechnique.CSS { - public func compress(data: some Collection, reserveCapacity: Int) throws(CompressionError) -> CompressionResult<[UInt8]> { - throw CompressionError.unsupportedOperation // TODO: support? - } - - /// - Parameters: - /// - data: Sequence of bytes to compress. - /// - closure: Logic to execute for a run. - /// - Complexity: O(_n_) where _n_ is the length of `data`. - public func compress(data: some Sequence, closure: (UInt8) -> Void) -> UInt8? { - return nil // TODO: support? - } -} - -// MARK: Minify -extension CompressionTechnique.CSS { - /// Optimizes CSS code to make it suitable for production-only usage, which results in the minimum binary size required to represent the same code. - /// - /// Optionally removes comments and unnecessary whitespace - public func minify(data: some Collection, reserveCapacity: Int) -> [UInt8] { - var index = 0 - let count = data.count - var i = 0 - var result = [UInt8](repeating: 0, count: count) - - let space:UInt8 = 32 - let pound:UInt8 = 35 - let lineFeed:UInt8 = 10 - let carriageReturn:UInt8 = 13 - let asterisk:UInt8 = 42 - let horizontalTab:UInt8 = 9 - let period:UInt8 = 46 - let forwardSlash:UInt8 = 47 - let a:UInt8 = 97 - let c:UInt8 = 99 - let d:UInt8 = 100 - let l:UInt8 = 108 - let n:UInt8 = 110 - while data[i] == space || data[i] == horizontalTab || data[i] == lineFeed || data[i] == carriageReturn { - i += 1 - } - var calcDepth:UInt8 = 0 - while i < count { - let char = data[i] - switch char { - case space, - horizontalTab, - lineFeed, - carriageReturn: - if calcDepth > 0 { - assign(byte: char, to: &index, in: &result) - } else { - let next = data[i+1] - let iMinus1 = data[i-1] - if iMinus1.char.isLetter && (next == pound || next == period || next.char.isNumber || next.char.isLetter || iMinus1 == d && data[i-2] == n && data[i-3] == a) { - assign(byte: char, to: &index, in: &result) - } - } - case forwardSlash: - if data[i+1] == asterisk { - i += 2 - for j in i.. 0 || data[i-1] == c && data[i-2] == l && data[i-3] == a && data[i-4] == c { // in calc or calc( was found - calcDepth += 1 - } - assign(byte: char, to: &index, in: &result) - case 41: // ) - if calcDepth > 0 { - calcDepth -= 1 - } - assign(byte: char, to: &index, in: &result) - default: - assign(byte: char, to: &index, in: &result) - } - i += 1 - } - return .init(result[0.. DNABinaryEncoding { - return DNABinaryEncoding(baseBits: baseBits) - } - - public struct DNABinaryEncoding: Compressor, Decompressor { - public let baseBits:[UInt8:[Bool]] - - public init(baseBits: [UInt8:[Bool]] = [ - 65: [false, false], // A - 67: [false, true], // C - 71: [true, false], // G - 84: [true, true] // T - ]) { - self.baseBits = baseBits - } - - @inlinable - public var algorithm: CompressionAlgorithm { - .dnaBinaryEncoding(baseBits: baseBits) - } - - @inlinable - public var quality: CompressionQuality { - .lossless - } - - public var baseBitsReversed: [[Bool]:UInt8] { - var reversed = [[Bool]:UInt8]() - reversed.reserveCapacity(baseBits.count) - for (byte, bits) in baseBits { - reversed[bits] = byte - } - return reversed - } - } -} - -// MARK: Compress -extension CompressionTechnique.DNABinaryEncoding { - /// Compress a collection of bytes using the DNA binary encoding technique. - /// - /// - Parameters: - /// - data: Collection of bytes to compress. - /// - baseBits: Bit codes for the unique base nucleotides. - /// - closure: Logic to execute when a byte was encoded. - /// - Returns: Valid bits for the last byte, if necessary. - /// - Complexity: O(_n_) where _n_ is the length of `data`. - public func compress( - data: some Collection, - closure: (UInt8) -> Void - ) -> UInt8? { - var bitWriter = ByteBuilder() - for base in data { - if let bits = baseBits[base] { - for bit in bits { - if let wrote = bitWriter.write(bit: bit) { - closure(wrote) - } - } - } - } - guard let (byte, validBits) = bitWriter.flush() else { return nil } - closure(byte) - return validBits - } -} - -// MARK: Decompress -extension CompressionTechnique.DNABinaryEncoding { - /// Decompress a collection of bytes using the DNA binary encoding technique. - /// - /// - Parameters: - /// - data: Collection of bytes to decompress. - /// - closure: Logic to execute when a given base nucleotide is found. - /// - Complexity: O(_n_) where _n_ is the length of `data`. - public func decompress( - data: some Collection, - closure: (UInt8) -> Void - ) { - let reversed = baseBitsReversed - for byte in data { - var bits = [Bool]() - bits.reserveCapacity(4) - for bit in byte.bits { - bits.append(bit) - if let base = reversed[bits] { - closure(base) - bits.removeAll(keepingCapacity: true) - } - } - } - } -} \ No newline at end of file diff --git a/Sources/DNA/binary/DNABinaryEncoding+Compress+Span.swift b/Sources/DNA/binary/DNABinaryEncoding+Compress+Span.swift new file mode 100644 index 0000000..baee96d --- /dev/null +++ b/Sources/DNA/binary/DNABinaryEncoding+Compress+Span.swift @@ -0,0 +1,15 @@ + +import SwiftCompressionUtilities + +extension DNABinaryEncoding { + public func compress( + span: Span, + configuration: ConcreteCompressionConfiguration + ) -> ConcreteCompressionResult { + var result = ConcreteCompressionResult() + let validBitsInLastByte = span.withUnsafeBufferPointer { + compress(buffer: $0, closure: { result.append($0) }) + } + return result + } +} \ No newline at end of file diff --git a/Sources/DNA/binary/DNABinaryEncoding+Compress.swift b/Sources/DNA/binary/DNABinaryEncoding+Compress.swift new file mode 100644 index 0000000..79aecdc --- /dev/null +++ b/Sources/DNA/binary/DNABinaryEncoding+Compress.swift @@ -0,0 +1,55 @@ + +import ByteBuilder +import SwiftCompressionUtilities + +extension DNABinaryEncoding: Compressor { + public typealias ConcreteCompressionConfiguration = CompressConfiguration + + public func compress( + data: some Collection, + configuration: ConcreteCompressionConfiguration + ) -> ConcreteCompressionResult { + var result = ConcreteCompressionResult() + let validBitsInLastByte = data.withContiguousStorageIfAvailable { + compress(buffer: $0, closure: { result.append($0) }) + } + return result + } + + /// Compress a collection of bytes using the DNA binary encoding technique. + /// + /// - Parameters: + /// - data: Collection of bytes to compress. + /// - baseBits: Bit codes for the unique base nucleotides. + /// - closure: Logic to execute when a byte was encoded. + /// - Returns: Valid bits for the last byte, if necessary. + /// - Complexity: O(_n_) where _n_ is the length of `data`. + func compress( + buffer: UnsafeBufferPointer, + closure: (UInt8) -> Void + ) -> UInt8? { + var bitWriter = ByteBuilder() + for base in buffer { + if let bits = baseBits[base] { + for bit in bits { + if let wrote = bitWriter.write(bit: bit) { + closure(wrote) + } + } + } + } + guard let (byte, validBits) = bitWriter.flush() else { return nil } + closure(byte) + return validBits + } +} + +// MARK: Configuration +extension DNABinaryEncoding { + public struct CompressConfiguration: CompressionConfiguration, DecompressionConfiguration { + public static var `default`: Self { .init() } + + public init() { + } + } +} \ No newline at end of file diff --git a/Sources/DNA/binary/DNABinaryEncoding+Decompress.swift b/Sources/DNA/binary/DNABinaryEncoding+Decompress.swift new file mode 100644 index 0000000..f60d7e6 --- /dev/null +++ b/Sources/DNA/binary/DNABinaryEncoding+Decompress.swift @@ -0,0 +1,46 @@ + +import SwiftCompressionUtilities + +extension DNABinaryEncoding: Decompressor { + public typealias ConcreteDecompressionConfiguration = CompressConfiguration + public typealias ConcreteDecompressionResult = [UInt8] + + /// Decompress a collection of bytes using the DNA binary encoding technique. + /// + /// - Parameters: + /// - data: Collection of bytes to decompress. + /// - closure: Logic to execute when a given base nucleotide is found. + /// - Complexity: O(_n_) where _n_ is the length of `data`. + public func decompress( + data: some Collection, + configuration: ConcreteDecompressionConfiguration + ) -> ConcreteDecompressionResult { + var result = ConcreteDecompressionResult() + decompress(data: data, closure: { result.append($0) }) + return result + } + + /// Decompress a collection of bytes using the DNA binary encoding technique. + /// + /// - Parameters: + /// - data: Collection of bytes to decompress. + /// - closure: Logic to execute when a given base nucleotide is found. + /// - Complexity: O(_n_) where _n_ is the length of `data`. + private func decompress( + data: some Collection, + closure: (UInt8) -> Void + ) { + let reversed = baseBitsReversed + for byte in data { + var bits = [Bool]() + bits.reserveCapacity(4) + for bit in byte.bits { + bits.append(bit) + if let base = reversed[bits] { + closure(base) + bits.removeAll(keepingCapacity: true) + } + } + } + } +} \ No newline at end of file diff --git a/Sources/DNA/binary/DNABinaryEncoding.swift b/Sources/DNA/binary/DNABinaryEncoding.swift new file mode 100644 index 0000000..f176d5c --- /dev/null +++ b/Sources/DNA/binary/DNABinaryEncoding.swift @@ -0,0 +1,33 @@ + +import SwiftCompressionUtilities + +/// The DNA binary encoding compression technique. +public struct DNABinaryEncoding: Sendable { + public let baseBits:[UInt8:[Bool]] + + public init(baseBits: [UInt8:[Bool]] = [ + 65: [false, false], // A + 67: [false, true], // C + 71: [true, false], // G + 84: [true, true] // T + ]) { + self.baseBits = baseBits + } + + public var algorithm: CompressionAlgorithm { + .dnaBinaryEncoding(baseBits: baseBits) + } + + public var compressionQuality: CompressionQuality { + .lossless + } + + public var baseBitsReversed: [[Bool]:UInt8] { + var reversed = [[Bool]:UInt8]() + reversed.reserveCapacity(baseBits.count) + for (byte, bits) in baseBits { + reversed[bits] = byte + } + return reversed + } +} \ No newline at end of file diff --git a/Sources/DNA/singleBlock/DNASingleBlockEncoding+Compress+Span.swift b/Sources/DNA/singleBlock/DNASingleBlockEncoding+Compress+Span.swift new file mode 100644 index 0000000..12708b5 --- /dev/null +++ b/Sources/DNA/singleBlock/DNASingleBlockEncoding+Compress+Span.swift @@ -0,0 +1,11 @@ + +import SwiftCompressionUtilities + +extension DNASingleBlockEncoding { + public func compress( + span: Span, + configuration: ConcreteCompressionConfiguration = .default + ) -> ConcreteCompressionResult { + return span.withUnsafeBufferPointer { compress(buffer: $0, configuration: configuration) } + } +} \ No newline at end of file diff --git a/Sources/DNA/DNASingleBlockEncoding.swift b/Sources/DNA/singleBlock/DNASingleBlockEncoding+Compress.swift similarity index 65% rename from Sources/DNA/DNASingleBlockEncoding.swift rename to Sources/DNA/singleBlock/DNASingleBlockEncoding+Compress.swift index 9864056..712d770 100644 --- a/Sources/DNA/DNASingleBlockEncoding.swift +++ b/Sources/DNA/singleBlock/DNASingleBlockEncoding+Compress.swift @@ -1,58 +1,30 @@ +import FrequencyTables import SwiftCompressionUtilities -extension CompressionTechnique { - /// The DNA single block encoding compression technique. - /// - /// https://www.mdpi.com/1999-4893/13/4/99 - public static let dnaSingleBlockEncoding:DNASingleBlockEncoding = DNASingleBlockEncoding() - - public struct DNASingleBlockEncoding: Compressor, Decompressor { - @inlinable - public var algorithm: CompressionAlgorithm { - .dnaSingleBlockEncoding - } - - @inlinable - public var quality: CompressionQuality { - .lossless - } - } +extension DNASingleBlockEncoding: Compressor { + public typealias ConcreteCompressionConfiguration = CompressConfiguration + public typealias ConcreteCompressionResult = [UInt8:[UInt8]] } -// MARK: Compress -extension CompressionTechnique.DNASingleBlockEncoding { - public func compress(data: some Collection, closure: (UInt8) -> Void) throws(CompressionError) -> UInt8? { // TODO: fix - return nil - } - - /// Compress a sequence of bytes using the DNA single block encoding technique. +extension DNASingleBlockEncoding { + /// Compress a sequence of bytes using phase one (compress data to binary) of the DNA single block encoding technique. /// /// - Parameters: /// - data: Sequence of bytes to compress. /// - Complexity: O(_n_ + (_m_ log _m_)) where _n_ is the length of `data` and _m_ is the number of unique bytes in `data`. - public static func compress( + public func compress( data: some Collection, - reserveCapacity: Int - ) -> CompressionResult<[UInt8]>? { // TODO: finish - let results = compressBinary(data: data) - for (base, _) in results { - print("base=\(Character(Unicode.Scalar(base)));result=\(results[base]!.debugDescription)") - } - return nil + configuration: CompressConfiguration + ) -> ConcreteCompressionResult { + return data.withContiguousStorageIfAvailable { compress(buffer: $0, configuration: configuration) } ?? .init() } -} -extension CompressionTechnique.DNASingleBlockEncoding { - /// Compress a sequence of bytes using phase one (compress data to binary) of the DNA single block encoding technique. - /// - /// - Parameters: - /// - data: Sequence of bytes to compress. - /// - Complexity: O(_n_ + (_m_ log _m_)) where _n_ is the length of `data` and _m_ is the number of unique bytes in `data`. - static func compressBinary( - data: some Sequence - ) -> [UInt8:[UInt8]] { - let frequencyTable:[UInt8:Int] = CompressionTechnique.buildFrequencyTable(data: data) + func compress( + buffer: UnsafeBufferPointer, + configuration: CompressConfiguration + ) -> ConcreteCompressionResult { + let frequencyTable:[UInt8:Int] = buildFrequencyTable(buffer: buffer) var sortedFrequencyTable = frequencyTable.sorted(by: { guard $0.value != $1.value else { return $0.key < $1.key } return $0.value > $1.value @@ -64,7 +36,7 @@ extension CompressionTechnique.DNASingleBlockEncoding { results[key] = [] sortedIndexes[key] = index } - for byte in data { + for byte in buffer { let sortedIndex = sortedIndexes[byte] ?? sortedIndexes.count for i in 0.., closure: (UInt8) -> Void) throws(DecompressionError) { - } +// MARK: Configuration +extension DNASingleBlockEncoding { + public struct CompressConfiguration: CompressionConfiguration { + public static var `default`: Self { .init() } - /// Decompress a sequence of bytes using the DNA single block encoding technique. - /// - /// - Parameters: - /// - data: Sequence of bytes to decompress. - public static func decompress(data: [UInt8]) -> [UInt8] { - return [] + public init() { + } } } \ No newline at end of file diff --git a/Sources/DNA/singleBlock/DNASingleBlockEncoding+Decompress.swift b/Sources/DNA/singleBlock/DNASingleBlockEncoding+Decompress.swift new file mode 100644 index 0000000..e884086 --- /dev/null +++ b/Sources/DNA/singleBlock/DNASingleBlockEncoding+Decompress.swift @@ -0,0 +1,28 @@ + +import SwiftCompressionUtilities + +extension DNASingleBlockEncoding: Decompressor { // TODO: finish + public typealias ConcreteDecompressionConfiguration = DecompressConfiguration + public typealias ConcreteDecompressionResult = [UInt8] + + /// Decompress a sequence of bytes using the DNA single block encoding technique. + /// + /// - Parameters: + /// - data: Sequence of bytes to decompress. + public func decompress( + data: some Collection, + configuration: ConcreteDecompressionConfiguration + ) -> ConcreteDecompressionResult { + return [] + } +} + +// MARK: Configuration +extension DNASingleBlockEncoding { + public struct DecompressConfiguration: DecompressionConfiguration { + public static var `default`: Self { .init() } + + public init() { + } + } +} \ No newline at end of file diff --git a/Sources/DNA/singleBlock/DNASingleBlockEncoding.swift b/Sources/DNA/singleBlock/DNASingleBlockEncoding.swift new file mode 100644 index 0000000..75f8561 --- /dev/null +++ b/Sources/DNA/singleBlock/DNASingleBlockEncoding.swift @@ -0,0 +1,18 @@ + +import SwiftCompressionUtilities + +/// The DNA single block encoding compression technique. +/// +/// https://www.mdpi.com/1999-4893/13/4/99 +public struct DNASingleBlockEncoding: Sendable { + public var algorithm: CompressionAlgorithm { + .dnaSingleBlockEncoding + } + + public var compressionQuality: CompressionQuality { + .lossless + } + + public init() { + } +} \ No newline at end of file diff --git a/Sources/FrequencyTables/BuildFrequencyTable.swift b/Sources/FrequencyTables/BuildFrequencyTable.swift new file mode 100644 index 0000000..df0cbcd --- /dev/null +++ b/Sources/FrequencyTables/BuildFrequencyTable.swift @@ -0,0 +1,91 @@ + +/// Creates a universal frequency table from a sequence of raw bytes. +/// +/// - Parameters: +/// - data: Sequence of raw bytes. +/// - Returns: A universal frequency table. +/// - Complexity: O(_n_) where _n_ is the length of `data`. +public func buildFrequencyTable(data: some Sequence) -> [Int] { + var table = Array(repeating: 0, count: 255) + for byte in data { + table[Int(byte)] += 1 + } + return table +} + +/// Creates a lookup frequency table from a sequence of raw bytes. +/// +/// - Parameters: +/// - data: Sequence of raw bytes. +/// - Returns: A lookup frequency table. +/// - Complexity: O(_n_) where _n_ is the length of `data`. +public func buildFrequencyTable(data: some Sequence) -> [UInt8:Int] { + var table = [UInt8:Int]() + for byte in data { + table[byte, default: 0] += 1 + } + return table +} + +/// Creates a lookup frequency table from a sequence of raw bytes. +/// +/// - Parameters: +/// - data: Sequence of raw bytes. +/// - Returns: A lookup frequency table. +/// - Complexity: O(_n_) where _n_ is the length of `data`. +public func buildFrequencyTable(buffer: UnsafeBufferPointer)-> [UInt8:Int] { + var table = [UInt8:Int]() + for byte in buffer { + table[byte, default: 0] += 1 + } + return table +} + +/// Creates a universal frequency table from a character frequency dictionary. +/// +/// - Parameters: +/// - chars: A frequency table that represents how many times a character is present. +/// - Returns: A universal frequency table. +/// - Complexity: O(_n_) where _n_ is the sum of the `Character` utf8 lengths in `chars`. +public func buildFrequencyTable(chars: [Character:Int]) -> [Int] { + var table = Array(repeating: 0, count: 255) + for (char, freq) in chars { + for byte in char.utf8 { + table[Int(byte)] = freq + } + } + return table +} + +#if compiler(>=6.2) +// MARK: Inline Frequency tables +/// Creates a universal frequency table from a sequence of raw bytes. +/// +/// - Parameters: +/// - data: Sequence of raw bytes. +/// - Returns: A universal frequency table. +/// - Complexity: O(_n_) where _n_ is the length of `data`. +public func buildInlineFrequencyTable(data: some Sequence) -> [255 of Int] { + var table = [255 of Int](repeating: 0) + for byte in data { + table[Int(byte)] += 1 + } + return table +} + +/// Creates a universal frequency table from a character frequency dictionary. +/// +/// - Parameters: +/// - chars: A frequency table that represents how many times a character is present. +/// - Returns: A universal frequency table. +/// - Complexity: O(_n_) where _n_ is the sum of the `Character` utf8 lengths in `chars`. +public func buildInlineFrequencyTable(chars: [Character:Int]) -> [255 of Int] { + var table = [255 of Int](repeating: 0) + for (char, freq) in chars { + for byte in char.utf8 { + table[Int(byte)] = freq + } + } + return table +} +#endif \ No newline at end of file diff --git a/Sources/SwiftCompressionUtilities/CompressionResult.swift b/Sources/Huffman/CompressionResult.swift similarity index 78% rename from Sources/SwiftCompressionUtilities/CompressionResult.swift rename to Sources/Huffman/CompressionResult.swift index 83f4722..65a20ed 100644 --- a/Sources/SwiftCompressionUtilities/CompressionResult.swift +++ b/Sources/Huffman/CompressionResult.swift @@ -1,13 +1,13 @@ public struct CompressionResult: Sendable { public var data:T - public var rootNode:CompressionTechnique.Huffman.Node? + public var rootNode:Huffman.Node? public var frequencyTable:[Int]? public var validBitsInLastByte:UInt8 public init( data: T, - rootNode: CompressionTechnique.Huffman.Node? = nil, + rootNode: Huffman.Node? = nil, frequencyTable: [Int]? = nil, validBitsInLastByte: UInt8 = 8 ) { diff --git a/Sources/Huffman/Huffman+Compress+Span.swift b/Sources/Huffman/Huffman+Compress+Span.swift new file mode 100644 index 0000000..bf45fed --- /dev/null +++ b/Sources/Huffman/Huffman+Compress+Span.swift @@ -0,0 +1,20 @@ + +extension Huffman { + public func compress( + span: Span, + configuration: CompressConfiguration + ) throws(Never) -> ConcreteCompressionResult { + return span.withUnsafeBufferPointer { buffer in + return compress(buffer: buffer, closure: ({ frequencies, codes, root in + var compressed:[UInt8] = [8] + var vBitsInLastByte:UInt8 = 8 + if let (lastByte, validBitsInLastByte) = translate(buffer: buffer, codes: codes, closure: { compressed.append($0) }) { + compressed[0] = validBitsInLastByte + compressed.append(lastByte) + vBitsInLastByte = validBitsInLastByte + } + return .init(data: compressed, rootNode: root, frequencyTable: frequencies, validBitsInLastByte: vBitsInLastByte) + })) + } + } +} \ No newline at end of file diff --git a/Sources/Huffman/Huffman+Compress.swift b/Sources/Huffman/Huffman+Compress.swift new file mode 100644 index 0000000..0e86752 --- /dev/null +++ b/Sources/Huffman/Huffman+Compress.swift @@ -0,0 +1,99 @@ + +import ByteBuilder +import SwiftCompressionUtilities + +extension Huffman: Compressor { + public typealias ConcreteCompressionConfiguration = CompressConfiguration + public typealias ConcreteCompressionResult = CompressionResult<[UInt8]>? + + /// Compress a sequence of bytes using the Huffman Coding technique. + /// + /// - Parameters: + /// - data: Sequence of bytes to compress. + public func compress( + data: some Collection, + configuration: ConcreteCompressionConfiguration + ) -> ConcreteCompressionResult { + return data.withContiguousStorageIfAvailable { buffer in + return compress(buffer: buffer, closure: ({ frequencies, codes, root in + var compressed:[UInt8] = [8] + var vBitsInLastByte:UInt8 = 8 + if let (lastByte, validBitsInLastByte) = translate(buffer: buffer, codes: codes, closure: { compressed.append($0) }) { + compressed[0] = validBitsInLastByte + compressed.append(lastByte) + vBitsInLastByte = validBitsInLastByte + } + return .init(data: compressed, rootNode: root, frequencyTable: frequencies, validBitsInLastByte: vBitsInLastByte) + })) + } ?? nil + } + + /// Compress a sequence of bytes to a stream using the Huffman Coding technique. + /// + /// - Parameters: + /// - data: Sequence of bytes to compress. + /// - continuation: The `AsyncStream.Continuation`. + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + public func compress( + data: some Collection, + bufferingPolicy limit: AsyncStream.Continuation.BufferingPolicy = .unbounded + ) -> CompressionResult>? { + // TODO: fix + return data.withContiguousStorageIfAvailable { buffer in + return compress(buffer: buffer) { frequencies, codes, root in + var vBitsInLastByte:UInt8 = 8 + let stream = AsyncStream(bufferingPolicy: limit) { continuation in + if let (lastByte, validBitsInLastByte) = translate(buffer: buffer, codes: codes, closure: { continuation.yield($0) }) { + continuation.yield(lastByte) + vBitsInLastByte = validBitsInLastByte + } + continuation.finish() + } + return CompressionResult(data: stream, rootNode: root, frequencyTable: frequencies, validBitsInLastByte: vBitsInLastByte) + } + } ?? nil + } + + func compress( + buffer: UnsafeBufferPointer, + closure: ([Int], [UInt8:String], Node) -> T + ) -> T? { + var frequencies = Array(repeating: 0, count: Int(UInt8.max-1)) + for byte in buffer { + frequencies[Int(byte)] += 1 + } + guard let root = buildTree(frequencies: frequencies) else { return nil } + var codes = [UInt8:String]() + generateCodes(node: root, codes: &codes) + return closure(frequencies, codes, root) + } + + /// - Complexity: O(_n_ + _m_) where _n_ is the length of `data` and _m_ is the sum of the code lengths. + func translate( + buffer: UnsafeBufferPointer, + codes: [UInt8:String], + closure: (UInt8) -> Void + ) -> (lastByte: UInt8, validBits: UInt8)? { + var builder = ByteBuilder() + for byte in buffer { + if let tree = codes[byte] { + for char in tree { + if let wrote = builder.write(bit: char == "1") { + closure(wrote) + } + } + } + } + return builder.flush() + } +} + +// MARK: Configuration +extension Huffman { + public struct CompressConfiguration: CompressionConfiguration, DecompressionConfiguration { + public static var `default`: Self { .init() } + + public init() { + } + } +} \ No newline at end of file diff --git a/Sources/Huffman/Huffman+Decompress.swift b/Sources/Huffman/Huffman+Decompress.swift new file mode 100644 index 0000000..e18974b --- /dev/null +++ b/Sources/Huffman/Huffman+Decompress.swift @@ -0,0 +1,145 @@ + +import ByteBuilder +import SwiftCompressionUtilities + +extension Huffman: Decompressor { + public typealias ConcreteDecompressionConfiguration = DecompressConfiguration + public typealias ConcreteDecompressionResult = [UInt8] + + /// Decompress a sequence of bytes using the Huffman Coding technique. + /// + /// - Parameters: + /// - data: Sequence of bytes to decompress. + /// - root: The root Huffman Node. + /// - Complexity: O(_n_) where _n_ is the length of `data`. + public func decompress( + data: some Collection, + configuration: ConcreteDecompressionConfiguration + ) -> ConcreteDecompressionResult { + var result = [UInt8]() + decompress(data: data, root: configuration.root) { result.append($0) } + return result + } + + /// Decompress a sequence of bytes into a stream using the Huffman Coding technique. + /// + /// - Parameters: + /// - data: Sequence of bytes to decompress. + /// - root: The root Huffman Node. + /// - continuation: The `AsyncStream.Continuation`. + /// - Complexity: O(_n_) where _n_ is the length of `data`. + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + public func decompress( + data: [UInt8], + root: Node?, + continuation: AsyncStream.Continuation + ) { + decompress(data: data, root: root) { continuation.yield($0) } + } + + /// Decompress a sequence of bytes using the Huffman Coding technique. + /// + /// - Complexity: O(_n_) where _n_ is the length of `data`. + func decompress( + data: some Collection, + root: Node?, + closure: (UInt8) -> Void + ) { + let countMinusOne = data.count-1 + var node = root + var index = 1 + while index < countMinusOne { + let bits = data[index].bits + for bit in 0..<8 { + if bits[bit] { + node = node?.right + } else { + node = node?.left + } + if let char = node?.character { + closure(char) + node = root + } + } + index += 1 + } + let validBitsInLastByte = data[0] + let lastBits = data[countMinusOne].bits + for bit in 0.., + frequencyTable: [Int] + ) -> [UInt8] { + guard let root = buildTree(frequencies: frequencyTable) else { return [UInt8](data) } + return decompress(data: data, configuration: .init(root: root)) + } + + /// Decompress a sequence of bytes into a stream using the Huffman Coding technique. + /// + /// - Parameters: + /// - data: Sequence of bytes to decompress. + /// - frequencyTable: A Huffman frequency table of characters. + /// - continuation: The `AsyncStream.Continuation`. + // /// - Complexity: O(_n_ + _m_) where _n_ is the length of `data` and _m_ is the length of `frequencyTable`. // TODO: FIX + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + public func decompress( + data: [UInt8], + frequencyTable: [Int], + continuation: AsyncStream.Continuation + ) { + guard let root = buildTree(frequencies: frequencyTable) else { return } + decompress(data: data, root: root, continuation: continuation) + } + + public func decompress(data: [UInt8], frequencyTable: [Int], closure: (UInt8) -> Void) { + guard let root = buildTree(frequencies: frequencyTable) else { return } + decompress(data: data, root: root, closure: closure) + } + + public func decompress(data: [UInt8], codes: [[Bool]:UInt8], closure: (UInt8) -> Void) { + var code = [Bool]() + code.reserveCapacity(3) + for bit in data { + code.append(bit == 1) + if let char = codes[code] { + closure(char) + code.removeAll(keepingCapacity: true) + } + } + } +} + +// MARK: Configuration +extension Huffman { + public struct DecompressConfiguration: DecompressionConfiguration { + public static var `default`: Self { .init() } + + public let root:Node? + + public init( + root: Node? = nil + ) { + self.root = root + } + } +} \ No newline at end of file diff --git a/Sources/Huffman/Huffman.swift b/Sources/Huffman/Huffman.swift new file mode 100644 index 0000000..f0681a0 --- /dev/null +++ b/Sources/Huffman/Huffman.swift @@ -0,0 +1,170 @@ + +import SwiftCompressionUtilities + +/// The Huffman coding compression technique. +/// +/// https://en.wikipedia.org/wiki/Huffman_coding +public struct Huffman: Sendable { + public init() { + } + + public var algorithm: CompressionAlgorithm { + .huffmanCoding + } + + public var compressionQuality: CompressionQuality { + .lossless + } +} + +// MARK: Node +extension Huffman { + /// A Huffman Node. + public final class Node: Comparable, Hashable, Sendable { + public static func < (left: Node, right: Node) -> Bool { + return left.frequency < right.frequency + } + public static func == (left: Node, right: Node) -> Bool { + return left.frequency == right.frequency + } + + public let character:UInt8? + public let frequency:Int + public let left:Node? + public let right:Node? + + public init( + character: UInt8? = nil, + frequency: Int, + left: Node? = nil, + right: Node? = nil + ) { + self.character = character + self.frequency = frequency + self.left = left + self.right = right + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(character) + hasher.combine(frequency) + hasher.combine(left) + hasher.combine(right) + } + } +} + +// MARK: PriorityQueue +extension Huffman { + public struct PriorityQueue { + public var heap:[T] + + public init(heap: [T] = []) { + self.heap = heap + } + + /// - Complexity: O(_n_) where _n_ is the length of the sequence. + mutating func push(_ element: T) { + heap.append(element) + siftUp(from: heap.count - 1) + } + + mutating func pop() -> T? { + guard !heap.isEmpty else { return nil } + if heap.count == 1 { + return heap.removeLast() + } + let root = heap[0] + heap[0] = heap.removeLast() + siftDown(from: 0) + return root + } + + /// - Complexity: O(_n_) where _n_ is the distance between `0` and `index`. + mutating func siftUp(from index: Int) { + var index = index + while index > 0 { + let parentIndex = (index - 1) / 2 + if heap[index] >= heap[parentIndex] { break } + heap.swapAt(index, parentIndex) + index = parentIndex + } + } + + mutating func siftDown(from index: Int) { + let element = heap[index] + let count = heap.count + var index = index + while true { + let leftIndex = (2 * index) + 1 + let rightIndex = leftIndex + 1 + var minIndex = index + + if leftIndex < count && heap[leftIndex] < heap[minIndex] { + minIndex = leftIndex + } + if rightIndex < count && heap[rightIndex] < heap[minIndex] { + minIndex = rightIndex + } + if minIndex == index { break } + + heap[index] = heap[minIndex] + index = minIndex + } + heap[index] = element + } + } +} + +// MARK: Logic +extension Huffman { + /// Builds a Huffman tree. + /// + /// - Parameters: + /// - frequencies: A universal frequency table. + /// - Returns: The root node of the Huffman tree. + /// - Complexity: O(?) + func buildTree(frequencies: [Int]) -> Node? { + var queue = PriorityQueue() + for (char, freq) in frequencies.enumerated() { + if freq != 0 { + queue.push(Node(character: UInt8(char), frequency: freq)) + } + } + while queue.heap.count > 1 { + let left = queue.pop()! + let right = queue.pop()! + let merged = Node(frequency: left.frequency + right.frequency, left: left, right: right) + queue.push(merged) + } + return queue.pop() + } + + /// Generates the binary codes for a node. + /// + /// - Complexity: O(1). + func generateCodes(node: Node?, code: String = "", codes: inout [UInt8:String]) { + guard let node else { return } + if let char = node.character { + codes[char] = code + } else { + generateCodes(node: node.left, code: code + "0", codes: &codes) + generateCodes(node: node.right, code: code + "1", codes: &codes) + } + } +} + +// MARK: StringProtocol +extension StringProtocol { + /// - Returns: A Huffman frequency table for the characters. + /// - Complexity: O(_n_) where _n_ is the length of the collection. + public func huffmanFrequencyTable() -> [Int] { + var table = Array(repeating: 0, count: 255) + for char in self { + for byte in char.utf8 { + table[Int(byte)] += 1 + } + } + return table + } +} \ No newline at end of file diff --git a/Sources/JavaScript/JavaScript.swift b/Sources/JavaScript/JavaScript.swift deleted file mode 100644 index 0bf7400..0000000 --- a/Sources/JavaScript/JavaScript.swift +++ /dev/null @@ -1,176 +0,0 @@ - -import SwiftCompressionUtilities - -extension CompressionTechnique { - - /// JavaScript compression techniques. - public static let javascript:JavaScript = JavaScript() - - public struct JavaScript: Compressor { - @inlinable - public var algorithm: CompressionAlgorithm { - .programmingLanguage(.javascript) - } - - @inlinable - public var quality: CompressionQuality { - .lossy - } - } -} - -// MARK: Compress -extension CompressionTechnique.JavaScript { - public func compress(data: some Collection, reserveCapacity: Int) throws(CompressionError) -> CompressionResult<[UInt8]> { - throw CompressionError.unsupportedOperation // TODO: support? - } - - /// - Parameters: - /// - data: Sequence of bytes to compress. - /// - closure: Logic to execute for a run. - /// - Complexity: O(_n_) where _n_ is the length of `data`. - public func compress(data: some Sequence, closure: (UInt8) -> Void) -> UInt8? { - return nil // TODO: support? - } -} - -// MARK: Minify -extension CompressionTechnique.JavaScript { - /// - Complexity: O(_n_) where _n_ is the length of the collection. - public func minify(data: some Collection) -> [UInt8] { // TODO: optimize? - var index = 0 - var keep = true - let count = data.count - var i = 0 - var result:[UInt8] = .init(repeating: 0, count: count) - - let space:UInt8 = 32 - let lineFeed:UInt8 = 10 - let carriageReturn:UInt8 = 13 - let asterisk:UInt8 = 42 - let horizontalTab:UInt8 = 9 - let forwardSlash:UInt8 = 47 - let a:UInt8 = 97 - let c:UInt8 = 99 - let e:UInt8 = 101 - let f:UInt8 = 102 - let iChar:UInt8 = 105 - let l:UInt8 = 108 - let n:UInt8 = 110 - let o:UInt8 = 111 - let p:UInt8 = 112 - let r:UInt8 = 114 - let s:UInt8 = 115 - let t:UInt8 = 116 - let u:UInt8 = 117 - let v:UInt8 = 118 - let w:UInt8 = 119 - let y:UInt8 = 121 - var char = data[i] - while char == space || char == horizontalTab || char == lineFeed || char == carriageReturn { - i += 1 - char = data[i] - } - while i < count { - char = data[i] - switch char { - case space, - horizontalTab, - lineFeed, - carriageReturn: - let iPlus1 = data.getPositive(i+1) - let iPlus2 = data.getPositive(i+2) - let iPlus3 = data.getPositive(i+3) - keep = iPlus1 == o && iPlus2 == f && iPlus3 == space // of - || iPlus1 == iChar && iPlus2 == n && iPlus3 == s && data.getPositive(i+4) == t && data.getPositive(i+5) == a && data.getPositive(i+6) == n && data.getPositive(i+7) == c && data.getPositive(i+8) == e && data.getPositive(i+9) == o && data.getPositive(i+10) == f && data.getPositive(i+11) == space // instanceof - if !keep { - switch data.get(i-1) { - case r: // var - keep = data.get(i-2) == a && data.get(i-3) == v - case t: // let, const - let iMinus2 = data.get(i-2) - let iMinus3 = data.get(i-3) - keep = (iMinus2 == e && iMinus3 == l) || (iMinus2 == s && iMinus3 == n && data.get(i-4) == o && data.get(i-5) == c) - case n: // function, return - let iMinus2 = data.get(i-2) - let iMinus3 = data.get(i-3) - let iMinus4 = data.get(i-4) - let iMinus5 = data.get(i-5) - let iMinus6 = data.get(i-6) - keep = (iMinus2 == o && iMinus3 == iChar && iMinus4 == t && iMinus5 == c && iMinus6 == n && data.get(i-7) == u && data.get(i-8) == f) - || (iMinus2 == r && iMinus3 == u && iMinus4 == t && iMinus5 == e && iMinus6 == r) - case f: // of, typeof, instanceof - let iMinus3 = data.get(i-3) - let iMinus4 = data.get(i-4) - let iMinus5 = data.get(i-5) - let iMinus6 = data.get(i-6) - keep = data.get(i-2) == o && (iMinus3 == space - || iMinus3 == e && iMinus4 == p && iMinus5 == y && iMinus6 == t - || iMinus3 == e && iMinus4 == c && iMinus5 == n && iMinus6 == a && data.get(i-7) == t && data.get(i-8) == s && data.get(i-9) == n && data.get(i-10) == iChar - ) - case w: // new - keep = data.get(i-2) == e && data.get(i-3) == n - case e: // else if, case 0-9 - let iMinus2 = data.get(i-2) - let iMinus3 = data.get(i-3) - let iMinus4 = data.get(i-4) - keep = (iMinus2 == s && iMinus3 == l && iMinus4 == e) && iPlus1 == iChar && iPlus2 == f - || (iMinus2 == s && iMinus3 == a && iMinus4 == c && (iPlus1 != nil ? iPlus1! >= 48 && iPlus1! <= 57 : false)) // 48 = "0"; 57 = "9" - default: - break - } - } - case forwardSlash: - let iPlus1 = data.getPositive(i+1), isMultiline:Bool = iPlus1 == asterisk - if iPlus1 == forwardSlash || isMultiline { - let endFunction:(UInt8?, Int) -> Bool, increment:Int - if isMultiline { - endFunction = { char, index in - return char == asterisk && data.getPositive(index+1) == forwardSlash - } - increment = 1 - } else { - endFunction = { char, _ in - return char == horizontalTab || char == lineFeed || char == carriageReturn - } - increment = 0 - } - i += 2 - loop: for j in i.. Brotli { - return Brotli(windowSize: windowSize) - } - - public struct Brotli: Compressor, Decompressor { // TODO: finish - /// Size of the window. - public let windowSize:Int - - /// Predefined dictionary to use. - //public let dictionary:[String:String] - - public init(windowSize: Int = 32768) { - self.windowSize = windowSize - } - - @inlinable - public var algorithm: CompressionAlgorithm { - .brotli(windowSize: windowSize) - } - - @inlinable - public var quality: CompressionQuality { - .lossless - } - } -} - -// MARK: Compress -extension CompressionTechnique.Brotli { // TODO: finish - /// - Parameters: - /// - data: Sequence of bytes to compress. - /// - Complexity: O(_n_) where _n_ is the length of `data`. - public func compress(data: some Collection, closure: (UInt8) -> Void) -> UInt8? { - return nil - } -} - -// MARK: Decompress -extension CompressionTechnique.Brotli { // TODO: finish - /// - Parameters: - /// - data: Collection of bytes to decompress. - /// - closure: Logic to execute when a byte is decompressed. - /// - Complexity: O(_n_) where _n_ is the length of `data`. - public func decompress( - data: some Collection, - closure: (UInt8) -> Void - ) { - } -} \ No newline at end of file diff --git a/Sources/LZ/DEFLATE.swift b/Sources/LZ/DEFLATE.swift deleted file mode 100644 index 6aa2653..0000000 --- a/Sources/LZ/DEFLATE.swift +++ /dev/null @@ -1,178 +0,0 @@ - -import SwiftCompressionUtilities - -// https://en.wikipedia.org/wiki/Gzip -// https://www.rfc-editor.org/rfc/rfc1952#page-5 - -extension CompressionTechnique { - /// The Deflate compression technique. - /// - /// https://en.wikipedia.org/wiki/Deflate - public enum Deflate { - struct Block: Sendable { - let head:Head - } - } -} - -// MARK: Head -extension CompressionTechnique.Deflate { - struct Head: Sendable { - let bits:UInt8 - - var isLastBlockInStream: Bool { - bits & 1 > 0 - } - - var encoding: Encoding { - switch (bits & 0b00000011) { - case 0: .stored - case 1: .staticHuffman - case 2: .dynamicHuffman - default: .reserved - } - } - } -} - -// MARK: Encoding -extension CompressionTechnique.Deflate { - public enum Encoding: ~Copyable, Sendable { - /// A stored (a.k.a. raw or literal) section, between 0 and 65,535 bytes in length - case stored - - /// A _static Huffman_ compressed block, using a pre-agreed [Huffman tree](https://en.wikipedia.org/wiki/Huffman_coding) defined in the RFC - case staticHuffman - - /// A _dynamic Huffman_ compressed block, complete with the Huffman table supplied - case dynamicHuffman - - /// Reserved: don't use - case reserved - } -} - -// MARK: Flags -extension CompressionTechnique.Deflate { - public struct Flags: ~Copyable, Sendable { - let bits:UInt8 - - public init( - FTEXT: Bool, - FHCRC: Bool, - FEXTRA: Bool, - FNAME: Bool, - FCOMMENT: Bool - ) { - bits = (FTEXT ? 1 : 0) - | (FHCRC ? 2 : 0) - | (FEXTRA ? 4 : 0) - | (FNAME ? 8 : 0) - | (FCOMMENT ? 16 : 0) - } - - /// - Warning: Make sure `flags` is in little-endian! - public init(flags: UInt8) { - self.bits = flags - } - } -} - -// MARK: Quality -extension CompressionTechnique.Deflate { - public enum Quality: ~Copyable, Sendable { - case `default` - - /// Best compression (level 9) - case best - - /// Fastest compression (level 1) - case fastest - - var rawValue: UInt8 { - switch self { - case .default: 0 - case .best: 2 - case .fastest: 4 - } - } - } -} - -// MARK: FileSystem -extension CompressionTechnique.Deflate { - public enum FileSystem: ~Copyable, Sendable { - case fat - case amiga - case openVMS - case unix - case vmCMS - case atariTOS - case hpfs - case macintosh - case zsystem - case cpm - case tops20 - case ntfs - case qdos - case acornRISCOS - case unknown - - var rawValue: UInt8 { - switch self { - case .fat: 0 - case .amiga: 1 - case .openVMS: 2 - case .unix: 3 - case .vmCMS: 4 - case .atariTOS: 5 - case .hpfs: 6 - case .macintosh: 7 - case .zsystem: 8 - case .cpm: 9 - case .tops20: 10 - case .ntfs: 11 - case .qdos: 12 - case .acornRISCOS: 13 - case .unknown: 255 - } - } - } -} - -// MARK: Compress -extension CompressionTechnique.Deflate { - public static func compress( - data: some Sequence, - flags: borrowing Flags = .init(flags: 0), - mtime: (UInt8, UInt8, UInt8, UInt8) = (0, 0, 0, 0), - quality: borrowing Quality = .default, - os: borrowing FileSystem = .unknown - ) -> CompressionResult<[UInt8]>? { - var result = [UInt8]() - - // gzip header - result.append(contentsOf: [ - 0x1F, // ID1 - 0x8B, // ID2 - 0x08, // CM (compression method; deflate=8) - flags.bits, // FLG (flags) - mtime.0.littleEndian, // MTIME byte 1 (modification time) - mtime.1.littleEndian, // MTIME byte 2 (modification time) - mtime.2.littleEndian, // MTIME byte 3 (modification time) - mtime.3.littleEndian, // MTIME byte 4 (modification time) - quality.rawValue.littleEndian // XFL (extra flags) - ]) - // TODO: finish - return .init(data: result) - } -} - -// MARK: Decompress -extension CompressionTechnique.Deflate { - public static func decompress( - data: [UInt8] - ) -> [UInt8] { - return data - } -} \ No newline at end of file diff --git a/Sources/LZ/LZ77.swift b/Sources/LZ/LZ77.swift deleted file mode 100644 index d935efa..0000000 --- a/Sources/LZ/LZ77.swift +++ /dev/null @@ -1,168 +0,0 @@ -// -// LZ77.swift -// -// -// Created by Evan Anderson on 12/12/24. -// - -import SwiftCompressionUtilities - -extension CompressionTechnique { - /// The LZ77 compression technique. - /// - /// - Parameters: - /// - T: Integer type the offset is encoded as. Default is `UInt16`. - /// - windowSize: Size of the sliding window, measured in bytes. - /// - bufferSize: Size of the buffer, measured in bytes. - /// - /// https://en.wikipedia.org/wiki/LZ77_and_LZ78 - public static func lz77(windowSize: Int, bufferSize: Int) -> LZ77 { - return LZ77(windowSize: windowSize, bufferSize: bufferSize) - } - - /// The LZ77 compression technique where the offset is encoded as a `UInt16`. - /// - /// - Parameters: - /// - windowSize: Size of the sliding window, measured in bytes. - /// - bufferSize: Size of the buffer, measured in bytes. - /// - /// https://en.wikipedia.org/wiki/LZ77_and_LZ78 - public static func lz77(windowSize: Int, bufferSize: Int) -> LZ77 { - return LZ77(windowSize: windowSize, bufferSize: bufferSize) - } - - public struct LZ77: Compressor, Decompressor { - /// Size of the window. - public let windowSize:Int - - /// Size of the buffer. - public let bufferSize:Int - - public init(windowSize: Int, bufferSize: Int) { - self.windowSize = windowSize - self.bufferSize = bufferSize - } - - @inlinable - public var algorithm: CompressionAlgorithm { - .lz77(windowSize: windowSize, bufferSize: bufferSize, offsetBitWidth: T.bitWidth) - } - - @inlinable - public var quality: CompressionQuality { - .lossless - } - } -} - -// MARK: Compress -extension CompressionTechnique.LZ77 { - /// Compress a collection of bytes using the LZ77 technique. - /// - /// - Parameters: - /// - data: Collection of bytes to compress. - /// - closure: Logic to execute when a byte is compressed. - /// - Complexity: O(_n_) where _n_ is the length of `data`. - public func compress( - data: some Collection, - closure: (UInt8) -> Void - ) -> UInt8? { - let count = data.count - var index = 0 - while index < count { - let bufferEndIndex = min(index + bufferSize, count) - guard index < bufferEndIndex else { break } - let bufferCount = bufferEndIndex - index - let bufferRange = data.index(data.startIndex, offsetBy: index)..= windowCount { - break - } - } - if length > bestLength { - bestLength = length - offset = windowCount - i - } - } - let byte:UInt8 - if index + bestLength < count { - byte = data[index + bestLength] - } else { - byte = 0 - } - for byte in T(offset).reversedBytes { - closure(byte) - } - closure(UInt8(bestLength)) - closure(byte) - index += bestLength + 1 - } - return nil - } -} - -// MARK: Decompress -extension CompressionTechnique.LZ77 { - /// Decompress a collection of bytes using the LZ77 technique. - /// - /// - Parameters: - /// - data: Collection of bytes to decompress. - /// - closure: Logic to execute when a byte was decompressed. - /// - Complexity: O(_n_) where _n_ is the length of `data`. - public func decompress( - data: some Collection, - closure: (UInt8) -> Void - ) { - let count = data.count - var history = [UInt8]() - var window = [UInt8]() - var index = 0 - let bytesForOffset = T.bitWidth / 8 - let byteIndexOffset = bytesForOffset + 1 - while index < count { - let length = Int(data[index + bytesForOffset]) - if length > 0 { - let offset = parseOffset(data: data, index: index) - let startIndex = window.count - Int(offset) - let endIndex = min(startIndex + length, window.count) - if startIndex < endIndex { - let bytes = window[startIndex.. windowSize { - window.removeFirst(window.count - windowSize) - } - index += bytesForOffset + 2 - } - } - - func parseOffset(data: some Collection, index: Int) -> T { - var byte = T() - var offsetIndex = index - for _ in 0...(T.bitWidth / 8)-1 { - byte <<= 8 - byte += T(data[offsetIndex]) - offsetIndex += 1 - } - return byte - } -} \ No newline at end of file diff --git a/Sources/LZ/LZ78.swift b/Sources/LZ/LZ78.swift index 8ec0cfd..e9cc4c0 100644 --- a/Sources/LZ/LZ78.swift +++ b/Sources/LZ/LZ78.swift @@ -1,9 +1,3 @@ -// -// LZ78.swift -// -// -// Created by Evan Anderson on 12/25/24. -// import SwiftCompressionUtilities diff --git a/Sources/LZ/lz77/LZ77+Compress+Span.swift b/Sources/LZ/lz77/LZ77+Compress+Span.swift new file mode 100644 index 0000000..ad21730 --- /dev/null +++ b/Sources/LZ/lz77/LZ77+Compress+Span.swift @@ -0,0 +1,22 @@ + +import SwiftCompressionUtilities + +extension LZ77 { + // Compress a collection of bytes using the LZ77 technique. + /// + /// - Parameters: + /// - data: Collection of bytes to compress. + /// - closure: Logic to execute when a byte is compressed. + /// - Complexity: O(_n_) where _n_ is the length of `data`. + public func compress( + span: Span, + configuration: ConcreteCompressionConfiguration + ) -> ConcreteCompressionResult { + var result = ConcreteCompressionResult() + result.reserveCapacity(configuration.reserveCapacity) + span.withUnsafeBufferPointer { + compress(buffer: $0, closure: { result.append($0) }) + } + return result + } +} \ No newline at end of file diff --git a/Sources/LZ/lz77/LZ77+Compress.swift b/Sources/LZ/lz77/LZ77+Compress.swift new file mode 100644 index 0000000..916f088 --- /dev/null +++ b/Sources/LZ/lz77/LZ77+Compress.swift @@ -0,0 +1,91 @@ + +import ByteBuilder +import SwiftCompressionUtilities + +extension LZ77: Compressor { + public typealias ConcreteCompressionConfiguration = CompressConfiguration + + // Compress a collection of bytes using the LZ77 technique. + /// + /// - Parameters: + /// - data: Collection of bytes to compress. + /// - closure: Logic to execute when a byte is compressed. + /// - Complexity: O(_n_) where _n_ is the length of `data`. + public func compress( + data: some Collection, + configuration: ConcreteCompressionConfiguration + ) -> ConcreteCompressionResult { + var result = ConcreteCompressionResult() + result.reserveCapacity(configuration.reserveCapacity) + data.withContiguousStorageIfAvailable { + compress(buffer: $0, closure: { result.append($0) }) + } + return result + } + + /// Compress a collection of bytes using the LZ77 technique. + /// + /// - Parameters: + /// - data: Collection of bytes to compress. + /// - closure: Logic to execute when a byte is compressed. + /// - Complexity: O(_n_) where _n_ is the length of `data`. + func compress( + buffer: UnsafeBufferPointer, + closure: (UInt8) -> Void + ) { + let count = buffer.count + var index = 0 + while index < count { + let bufferEndIndex = min(index + bufferSize, count) + guard index < bufferEndIndex else { break } + let bufferCount = bufferEndIndex - index + let bufferRange = index..= windowCount { + break + } + } + if length > bestLength { + bestLength = length + offset = windowCount - i + } + } + let byte:UInt8 + if index + bestLength < count { + byte = buffer[index + bestLength] + } else { + byte = 0 + } + for byte in T(offset).reversedBytes { + closure(byte) + } + closure(UInt8(bestLength)) + closure(byte) + index += bestLength + 1 + } + } +} + +// MARK: Configuration +extension LZ77 { + public struct CompressConfiguration: CompressionConfiguration, DecompressionConfiguration { + public static var `default`: Self { .init() } + + public let reserveCapacity:Int + + public init( + reserveCapacity: Int = 1024 + ) { + self.reserveCapacity = reserveCapacity + } + } +} \ No newline at end of file diff --git a/Sources/LZ/lz77/LZ77+Decompress.swift b/Sources/LZ/lz77/LZ77+Decompress.swift new file mode 100644 index 0000000..363b355 --- /dev/null +++ b/Sources/LZ/lz77/LZ77+Decompress.swift @@ -0,0 +1,77 @@ + +import SwiftCompressionUtilities + +extension LZ77: Decompressor { + public typealias ConcreteDecompressionConfiguration = CompressConfiguration + public typealias ConcreteDecompressionResult = [UInt8] + + /// Decompress a collection of bytes using the LZ77 technique. + /// + /// - Parameters: + /// - data: Collection of bytes to decompress. + /// - closure: Logic to execute when a byte was decompressed. + /// - Complexity: O(_n_) where _n_ is the length of `data`. + public func decompress( + data: some Collection, + configuration: ConcreteDecompressionConfiguration + ) -> ConcreteDecompressionResult { + var result = ConcreteDecompressionResult() + decompress(data: data, closure: { result.append($0) }) + result.reserveCapacity(configuration.reserveCapacity) + return result + } + + /// Decompress a collection of bytes using the LZ77 technique. + /// + /// - Parameters: + /// - data: Collection of bytes to decompress. + /// - closure: Logic to execute when a byte was decompressed. + /// - Complexity: O(_n_) where _n_ is the length of `data`. + private func decompress( + data: some Collection, + closure: (UInt8) -> Void + ) { + let count = data.count + var history = [UInt8]() + var window = [UInt8]() + var index = 0 + let bytesForOffset = T.bitWidth / 8 + let byteIndexOffset = bytesForOffset + 1 + while index < count { + let length = Int(data[index + bytesForOffset]) + if length > 0 { + let offset = parseOffset(data: data, index: index) + let startIndex = window.count - Int(offset) + let endIndex = min(startIndex + length, window.count) + if startIndex < endIndex { + let bytes = window[startIndex.. windowSize { + window.removeFirst(window.count - windowSize) + } + index += bytesForOffset + 2 + } + } + + func parseOffset(data: some Collection, index: Int) -> T { + var byte = T() + var offsetIndex = index + for _ in 0...(T.bitWidth / 8)-1 { + byte <<= 8 + byte += T(data[offsetIndex]) + offsetIndex += 1 + } + return byte + } +} \ No newline at end of file diff --git a/Sources/LZ/lz77/LZ77.swift b/Sources/LZ/lz77/LZ77.swift new file mode 100644 index 0000000..9580b1b --- /dev/null +++ b/Sources/LZ/lz77/LZ77.swift @@ -0,0 +1,32 @@ + +import SwiftCompressionUtilities + +/// The LZ77 compression technique. +/// +/// - Generics: +/// - T: Integer type the offset is encoded as. +/// +/// https://en.wikipedia.org/wiki/LZ77_and_LZ78 +public struct LZ77: Sendable { + /// Size of the window. + public let windowSize:Int + + /// Size of the buffer. + public let bufferSize:Int + + /// - Parameters: + /// - windowSize: Size of the sliding window, measured in bytes. + /// - bufferSize: Size of the buffer, measured in bytes. + public init(windowSize: Int, bufferSize: Int) { + self.windowSize = windowSize + self.bufferSize = bufferSize + } + + public var algorithm: CompressionAlgorithm { + .lz77(windowSize: windowSize, bufferSize: bufferSize, offsetBitWidth: T.bitWidth) + } + + public var compressionQuality: CompressionQuality { + .lossless + } +} \ No newline at end of file diff --git a/Sources/Snappy/IWA.swift b/Sources/Snappy/IWA.swift deleted file mode 100644 index 17fd362..0000000 --- a/Sources/Snappy/IWA.swift +++ /dev/null @@ -1,53 +0,0 @@ - -import SwiftCompressionUtilities - -extension CompressionTechnique { - /// The iWork Archive (iwa) compression technique. - /// - /// https://en.wikipedia.org/wiki/IWork - public static func iwa(version: IWAVersion) -> IWA { - return IWA(version: version) - } - - public struct IWA: Compressor, Decompressor { - /// Version of the iWork Archive to use. - public let version:IWAVersion - - public init(version: IWAVersion) { - self.version = version - } - - @inlinable - public var algorithm: CompressionAlgorithm { - .iwa(version: version) - } - - @inlinable - public var quality: CompressionQuality { - .lossless - } - } -} - -// MARK: Compress -extension CompressionTechnique.IWA { // TODO: finish - /// - Parameters: - /// - data: Sequence of bytes to compress. - /// - Complexity: O(_n_) where _n_ is the length of `data`. - public func compress(data: some Collection, closure: (UInt8) -> Void) -> UInt8? { - return nil - } -} - -// MARK: Decompress -extension CompressionTechnique.IWA { // TODO: finish - /// - Parameters: - /// - data: Collection of bytes to decompress. - /// - closure: Logic to execute when a byte is decompressed. - /// - Complexity: O(_n_) where _n_ is the length of `data`. - public func decompress( - data: some Collection, - closure: (UInt8) -> Void - ) { - } -} \ No newline at end of file diff --git a/Sources/Snappy/Snappy+Compress+Span.swift b/Sources/Snappy/Snappy+Compress+Span.swift new file mode 100644 index 0000000..3060f26 --- /dev/null +++ b/Sources/Snappy/Snappy+Compress+Span.swift @@ -0,0 +1,26 @@ + +import SnappyShim +import SwiftCompressionUtilities + +extension Snappy { + public func compress( + span: Span, + configuration: ConcreteCompressionConfiguration + ) -> ConcreteCompressionResult { + let inputSize = span.count + var outputSize = snappy_max_compressed_length(inputSize) + let buffer = UnsafeMutableBufferPointer.allocate(capacity: outputSize) + buffer.initialize(repeating: 0) + defer { buffer.deallocate() } + let result = span.withUnsafeBufferPointer { + snappy_compress( + $0.baseAddress, + inputSize, + buffer.baseAddress, + &outputSize + ) + } + guard result == SNAPPY_OK else { return nil } + return [UInt8](buffer.prefix(outputSize)) + } +} \ No newline at end of file diff --git a/Sources/Snappy/Snappy+Compress.swift b/Sources/Snappy/Snappy+Compress.swift new file mode 100644 index 0000000..68fa7e1 --- /dev/null +++ b/Sources/Snappy/Snappy+Compress.swift @@ -0,0 +1,39 @@ + +import SnappyShim +import SwiftCompressionUtilities + +extension Snappy: Compressor { + public typealias ConcreteCompressionConfiguration = CompressConfiguration + public typealias ConcreteCompressionResult = [UInt8]? + + public func compress( + data: some Collection, + configuration: ConcreteCompressionConfiguration + ) -> ConcreteCompressionResult { + let inputSize = data.count + var outputSize = snappy_max_compressed_length(inputSize) + let buffer = UnsafeMutableBufferPointer.allocate(capacity: outputSize) + buffer.initialize(repeating: 0) + defer { buffer.deallocate() } + let result = data.withContiguousStorageIfAvailable { + snappy_compress( + $0.baseAddress, + inputSize, + buffer.baseAddress, + &outputSize + ) + } + guard result == SNAPPY_OK else { return nil } + return [UInt8](buffer.prefix(outputSize)) + } +} + +// MARK: Configuration +extension Snappy { + public struct CompressConfiguration: CompressionConfiguration, DecompressionConfiguration { + public static var `default`: Self { .init() } + + public init() { + } + } +} \ No newline at end of file diff --git a/Sources/Snappy/Snappy+Decompress.swift b/Sources/Snappy/Snappy+Decompress.swift new file mode 100644 index 0000000..3fcc7d4 --- /dev/null +++ b/Sources/Snappy/Snappy+Decompress.swift @@ -0,0 +1,203 @@ + +import ByteBuilder +import SwiftCompressionUtilities + +extension Snappy: Decompressor { + public typealias ConcreteDecompressionConfiguration = CompressConfiguration + public typealias ConcreteDecompressionResult = [UInt8] + + /// - Parameters: + /// - data: Collection of bytes to decompress. + /// - reserveCapacity: Ignored. + public func decompress( + data: some Collection, + configuration: ConcreteDecompressionConfiguration = .default + ) throws(DecompressionError) -> [UInt8] { + var decompressed = [UInt8]() + var index = data.startIndex + let length:Int = try decompressLength(data: data, index: &index) + decompressed.reserveCapacity(length) + try decompress(data: data, index: &index, amount: length) { decompressed.append($0) } + return decompressed + } + + /// - Parameters: + /// - data: Collection of bytes to decompress. + /// - index: Where to begin decompressing data. + /// - amount: Number of bytes to decompress. + /// - closure: Logic to execute when a byte is decompressed. + /// - Complexity: O(_n_) where _n_ is the length of `data`. + func decompress>( + data: C, + index: inout C.Index, + amount: Int, + closure: (UInt8) -> Void + ) throws(DecompressionError) { + guard let endIndex = data.index(data.startIndex, offsetBy: amount, limitedBy: data.endIndex) else { throw DecompressionError.malformedInput } + while index < endIndex { + let control = data[index] + switch control & 0b11 { + case 0: decompressLiteral(flagBits: control, index: &index, compressed: data, closure: closure) + case 1: decompressCopy1(flagBits: control, index: &index, compressed: data, closure: closure) + case 2: decompressCopy2(flagBits: control, index: &index, compressed: data, closure: closure) + case 3: decompressCopy4(flagBits: control, index: &index, compressed: data, closure: closure) + default: throw DecompressionError.malformedInput + } + } + } + + /// - Parameters: + /// - data: Collection of bytes to decompress. + /// - index: Where to begin parsing the uncompressed length. + /// - Returns: The uncompressed length, as described in the `Preamble` at https://github.com/google/snappy/blob/main/format_description.txt . + /// - Complexity: O(1). + func decompressLength, I: FixedWidthInteger>( + data: C, + index: inout C.Index + ) throws(DecompressionError) -> I { + var totalSize:I + var byte = data[index] + if byte & 0b10000000 != 0 { + totalSize = I(byte) + data.formIndex(after: &index) + guard let second = data[positive: index] else { throw DecompressionError.malformedInput } + byte = second + var shift = 7 + while byte & 0b10000000 != 0 { + totalSize |= I(byte) << shift + shift += 7 + data.formIndex(after: &index) + if let next = data[positive: index] { + byte = next + } else { + throw DecompressionError.malformedInput + } + } + // final length byte + totalSize |= I(byte) << shift + } else { + totalSize = I(byte) + } + data.formIndex(after: &index) + return totalSize + } +} + +// MARK: Literal +extension Snappy { + /// - Complexity: O(_n_) where _n_ is the length of the literal. + func decompressLiteral>( + flagBits: UInt8, + index: inout C.Index, + compressed: C, + closure: (_ byte: UInt8) -> Void + ) { + let length = decompressLiteralLength(flagBits: flagBits, index: &index, compressed: compressed) + for _ in 0...length { + closure(compressed[index]) + compressed.formIndex(after: &index) + } + } + + /// - Complexity: O(1). + func decompressLiteralLength>(flagBits: UInt8, index: inout C.Index, compressed: C) -> Int { + let length = flagBits >> 2 // ignore tag bits + compressed.formIndex(after: &index) + var totalLength:Int + if length >= 60 { + totalLength = 0 + for _ in 0..>( + flagBits: UInt8, + index: inout C.Index, + compressed: C, + closure: (_ byte: UInt8) -> Void + ) { + let length = 4 + ((flagBits >> 2) & 0b00000111) + let offset = Int(((UInt16(flagBits) << 8) & 0b11100000) + UInt16(compressed[compressed.index(index, offsetBy: 1)])) + var begins = compressed.index(index, offsetBy: -offset) + for _ in 0..>( + flagBits: UInt8, + index: inout C.Index, + compressed: C, + closure: (_ byte: UInt8) -> Void + ) { + //let offset:UInt16 = UInt16( UInt16(compressed[compressed.index(index, offsetBy: 1)]) << 8 | UInt16(compressed[compressed.index(index, offsetBy: 2)]) ) + let offset = UInt16(fromBits: + compressed[compressed.index(index, offsetBy: 1)], + compressed[compressed.index(index, offsetBy: 2)] + ) + decompressCopyN(flagBits: flagBits, index: &index, compressed: compressed, offset: offset, readBytes: 3, closure: closure) + } + + /// - Complexity: O(_n_) where _n_ is the `UInt8` created from the `flagBits`. + func decompressCopy4>( + flagBits: UInt8, + index: inout C.Index, + compressed: C, + closure: (_ byte: UInt8) -> Void + ) { + let offset = UInt32.init(fromBits: + compressed[compressed.index(index, offsetBy: 1)], + compressed[compressed.index(index, offsetBy: 2)], + compressed[compressed.index(index, offsetBy: 3)] + ) + decompressCopyN(flagBits: flagBits, index: &index, compressed: compressed, offset: offset, readBytes: 5, closure: closure) + } + + /// - Complexity: O(_n_) where _n_ is the `UInt8` created from the `flagBits`. + func decompressCopyN, T: FixedWidthInteger>( + flagBits: UInt8, + index: inout C.Index, + compressed: C, + offset: T, + readBytes: Int, + closure: (_ byte: UInt8) -> Void + ) { + let length = flagBits & 0b11111100 + //print("decompressCopyN;readBytes=\(readBytes);length=\(length)") + var begins = compressed.index(index, offsetBy: -Int(offset)) + for _ in 0.., + continuation: AsyncThrowingStream.Continuation + ) throws(DecompressionError) { + var index = data.startIndex + let length:Int = try decompressLength(data: data, index: &index) + try decompress(data: data, index: &index, amount: length) { continuation.yield($0) } + } +} \ No newline at end of file diff --git a/Sources/Snappy/Snappy.swift b/Sources/Snappy/Snappy.swift index 2fe48db..df17a61 100644 --- a/Sources/Snappy/Snappy.swift +++ b/Sources/Snappy/Snappy.swift @@ -1,328 +1,21 @@ import SwiftCompressionUtilities -extension CompressionTechnique { - /// The Snappy (Zippy) compression technique. - /// - /// https://en.wikipedia.org/wiki/Snappy_(compression) - /// - /// https://github.com/google/snappy - public static func snappy(windowSize: Int = Int(UInt16.max)) -> Snappy { - return Snappy(windowSize: windowSize) - } - - public struct Snappy: Compressor, Decompressor { - - /// Size of the window. - public let windowSize:Int - - public init(windowSize: Int = Int(UInt16.max)) { - self.windowSize = windowSize - } - - @inlinable - public var algorithm: CompressionAlgorithm { - .snappy(windowSize: windowSize) - } - - @inlinable - public var quality: CompressionQuality { - .lossless - } - } -} - -// MARK: Compress -extension CompressionTechnique.Snappy { - public func compress(data: some Collection, closure: (UInt8) -> Void) throws(CompressionError) -> UInt8? { - return nil - } - public func compress(data: some Collection, reserveCapacity: Int) throws -> CompressionResult<[UInt8]> { - throw CompressionError.unsupportedOperation - } -} -/* -extension CompressionTechnique.Snappy { // TODO: finish - /// - Parameters: - /// - data: Sequence of bytes to compress. - /// - Complexity: O(_n_) where _n_ is the length of `data`. - public func compress(data: some Collection, closure: (UInt8) -> Void) -> UInt8? { - var index = data.startIndex - while index != data.endIndex { - let (length, matchLength, offset) = longestMatch(data, from: index) - if matchLength == 0 { - let next = data.index(index, offsetBy: length) - compressLiteral(data[index..>(_ data: C, from startIndex: C.Index) -> (length: Int, matchLength: Int, offset: Int) { - let maxLength = 60 - var longestMatchLength = 0 - var offset = 0 - - var length = 0 - var index = startIndex - while length < maxLength && index != data.endIndex { - let starts = data.index(index, offsetBy: -min(length, windowSize), limitedBy: data.startIndex) ?? data.startIndex - let longestMatch = longestCommonPrefix(data, index1: index, index2: starts) - if length > longestMatchLength { - longestMatchLength = longestMatch - offset = data.distance(from: starts, to: index) - } - length += 1 - index = data.index(after: index) - } - return (length, longestMatchLength, offset: offset) - } - - func longestCommonPrefix>(_ data: C, index1: C.Index, index2: C.Index) -> Int { - var length = 0 - var index1 = index1 - var index2 = index2 - while index1 != data.endIndex && index2 != data.endIndex && data[index1] == data[index2] { - length += 1 - index1 = data.index(after: index1) - index2 = data.index(after: index2) - } - return length - } -} - -// MARK: Literal -extension CompressionTechnique.Snappy { - func compressLiteral>(_ data: C, closure: (UInt8) -> Void) { - let count = data.count - if count < 60 { - closure(UInt8(count << 2)) - } else { - closure(UInt8(60 << 2)) - closure(UInt8(count)) - } - for value in data { - closure(value) - } - } -} - -// MARK: Copy -extension CompressionTechnique.Snappy { - func compressCopy(length: Int, offset: Int, closure: (UInt8) -> Void) { - if length < 12 && offset < 2048 { - let cmd = UInt8((offset >> 8) << 5 | (length - 4) << 2 | 1) - closure(cmd) - closure(UInt8(offset & 0xFF)) - } else { - closure(UInt8((length - 1) << 2) | 2) - closure(UInt8(offset & 0xFF)) - closure(UInt8((offset >> 8) & 0xFF)) - } - } -}*/ - -// MARK: Decompress -extension CompressionTechnique.Snappy { - /// - Parameters: - /// - data: Collection of bytes to decompress. - /// - reserveCapacity: Ignored. - public func decompress( - data: some Collection, - reserveCapacity: Int = 0 - ) throws(DecompressionError) -> [UInt8] { - var decompressed = [UInt8]() - var index = data.startIndex - let length:Int = try decompressLength(data: data, index: &index) - decompressed.reserveCapacity(length) - try decompress(data: data, index: &index, amount: length) { decompressed.append($0) } - return decompressed - } - - /// - Parameters: - /// - data: Collection of bytes to decompress. - /// - continuation: Yielding async throwing stream continuation. - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) - public func decompress( - data: some Collection, - continuation: AsyncThrowingStream.Continuation - ) throws(DecompressionError) { - var index = data.startIndex - let length:Int = try decompressLength(data: data, index: &index) - try decompress(data: data, index: &index, amount: length) { continuation.yield($0) } - } - - /// Calling this function directly will throw a DecompressionError.unsupportedOperation. - @available(*, deprecated, message: "Use decompress(data:index:amount:closure:) instead") - public func decompress(data: some Collection, closure: (UInt8) -> Void) throws(DecompressionError) { - throw DecompressionError.unsupportedOperation("Use decompress(data:index:amount:closure:) instead") - } - - /// - Parameters: - /// - data: Collection of bytes to decompress. - /// - index: Where to begin decompressing data. - /// - amount: Number of bytes to decompress. - /// - closure: Logic to execute when a byte is decompressed. - /// - Complexity: O(_n_) where _n_ is the length of `data`. - public func decompress>( - data: C, - index: inout C.Index, - amount: Int, - closure: (UInt8) -> Void - ) throws(DecompressionError) { - guard let endIndex = data.index(data.startIndex, offsetBy: amount, limitedBy: data.endIndex) else { throw DecompressionError.malformedInput } - while index < endIndex { - let control = data[index] - switch control & 0b11 { - case 0: decompressLiteral(flagBits: control, index: &index, compressed: data, closure: closure) - case 1: decompressCopy1(flagBits: control, index: &index, compressed: data, closure: closure) - case 2: decompressCopy2(flagBits: control, index: &index, compressed: data, closure: closure) - case 3: decompressCopy4(flagBits: control, index: &index, compressed: data, closure: closure) - default: throw DecompressionError.malformedInput - } - } - } - - /// - Parameters: - /// - data: Collection of bytes to decompress. - /// - index: Where to begin parsing the uncompressed length. - /// - Returns: The uncompressed length, as described in the `Preamble` at https://github.com/google/snappy/blob/main/format_description.txt . - /// - Complexity: O(1). - func decompressLength, I: FixedWidthInteger>( - data: C, - index: inout C.Index - ) throws(DecompressionError) -> I { - var totalSize:I - var byte = data[index] - if byte & 0b10000000 != 0 { - totalSize = I(byte) - data.formIndex(after: &index) - guard let second = data[positive: index] else { throw DecompressionError.malformedInput } - byte = second - var shift = 7 - while byte & 0b10000000 != 0 { - totalSize |= I(byte) << shift - shift += 7 - data.formIndex(after: &index) - if let next = data[positive: index] { - byte = next - } else { - throw DecompressionError.malformedInput - } - } - // final length byte - totalSize |= I(byte) << shift - } else { - totalSize = I(byte) - } - data.formIndex(after: &index) - return totalSize - } -} - -// MARK: Literal -extension CompressionTechnique.Snappy { - /// - Complexity: O(_n_) where _n_ is the length of the literal. - func decompressLiteral>( - flagBits: UInt8, - index: inout C.Index, - compressed: C, - closure: (_ byte: UInt8) -> Void - ) { - let length = decompressLiteralLength(flagBits: flagBits, index: &index, compressed: compressed) - for _ in 0...length { - closure(compressed[index]) - compressed.formIndex(after: &index) - } - } - - /// - Complexity: O(1). - func decompressLiteralLength>(flagBits: UInt8, index: inout C.Index, compressed: C) -> Int { - let length = flagBits >> 2 // ignore tag bits - compressed.formIndex(after: &index) - var totalLength:Int - if length >= 60 { - totalLength = 0 - for _ in 0..>( - flagBits: UInt8, - index: inout C.Index, - compressed: C, - closure: (_ byte: UInt8) -> Void - ) { - let length = 4 + ((flagBits >> 2) & 0b00000111) - let offset = Int(((UInt16(flagBits) << 8) & 0b11100000) + UInt16(compressed[compressed.index(index, offsetBy: 1)])) - var begins = compressed.index(index, offsetBy: -offset) - for _ in 0..>( - flagBits: UInt8, - index: inout C.Index, - compressed: C, - closure: (_ byte: UInt8) -> Void - ) { - //let offset:UInt16 = UInt16( UInt16(compressed[compressed.index(index, offsetBy: 1)]) << 8 | UInt16(compressed[compressed.index(index, offsetBy: 2)]) ) - let offset = UInt16(fromBits: - compressed[compressed.index(index, offsetBy: 1)], - compressed[compressed.index(index, offsetBy: 2)] - ) - decompressCopyN(flagBits: flagBits, index: &index, compressed: compressed, offset: offset, readBytes: 3, closure: closure) + public init() { } - /// - Complexity: O(_n_) where _n_ is the `UInt8` created from the `flagBits`. - func decompressCopy4>( - flagBits: UInt8, - index: inout C.Index, - compressed: C, - closure: (_ byte: UInt8) -> Void - ) { - let offset = UInt32.init(fromBits: - compressed[compressed.index(index, offsetBy: 1)], - compressed[compressed.index(index, offsetBy: 2)], - compressed[compressed.index(index, offsetBy: 3)] - ) - decompressCopyN(flagBits: flagBits, index: &index, compressed: compressed, offset: offset, readBytes: 5, closure: closure) + public var algorithm: CompressionAlgorithm { + .snappy } - /// - Complexity: O(_n_) where _n_ is the `UInt8` created from the `flagBits`. - func decompressCopyN, T: FixedWidthInteger>( - flagBits: UInt8, - index: inout C.Index, - compressed: C, - offset: T, - readBytes: Int, - closure: (_ byte: UInt8) -> Void - ) { - let length = flagBits & 0b11111100 - //print("decompressCopyN;readBytes=\(readBytes);length=\(length)") - var begins = compressed.index(index, offsetBy: -Int(offset)) - for _ in 0.., closure: (UInt8) -> Void) -> UInt8? { - return nil - } -} - -// MARK: Decompress -extension CompressionTechnique.SnappyFramed { // TODO: finish - /// - Parameters: - /// - data: Collection of bytes to decompress. - /// - closure: Logic to execute when a byte is decompressed. - /// - Complexity: O(_n_) where _n_ is the length of `data`. - public func decompress( - data: some Collection, - closure: (UInt8) -> Void - ) { - } -} \ No newline at end of file diff --git a/Sources/Snappy/framed/SnappyFramed+Compress.swift b/Sources/Snappy/framed/SnappyFramed+Compress.swift new file mode 100644 index 0000000..f61028f --- /dev/null +++ b/Sources/Snappy/framed/SnappyFramed+Compress.swift @@ -0,0 +1,11 @@ + +import SwiftCompressionUtilities + +extension SnappyFramed { // TODO: finish + /// - Parameters: + /// - data: Sequence of bytes to compress. + /// - Complexity: O(_n_) where _n_ is the length of `data`. + public func compress(data: some Collection, closure: (UInt8) -> Void) -> UInt8? { + return nil + } +} \ No newline at end of file diff --git a/Sources/Snappy/framed/SnappyFramed+Decompress.swift b/Sources/Snappy/framed/SnappyFramed+Decompress.swift new file mode 100644 index 0000000..d335d63 --- /dev/null +++ b/Sources/Snappy/framed/SnappyFramed+Decompress.swift @@ -0,0 +1,14 @@ + +import SwiftCompressionUtilities + +extension SnappyFramed { // TODO: finish + /// - Parameters: + /// - data: Collection of bytes to decompress. + /// - closure: Logic to execute when a byte is decompressed. + /// - Complexity: O(_n_) where _n_ is the length of `data`. + public func decompress( + data: some Collection, + closure: (UInt8) -> Void + ) { + } +} \ No newline at end of file diff --git a/Sources/Snappy/framed/SnappyFramed.swift b/Sources/Snappy/framed/SnappyFramed.swift new file mode 100644 index 0000000..9c24d5d --- /dev/null +++ b/Sources/Snappy/framed/SnappyFramed.swift @@ -0,0 +1,17 @@ + +import SwiftCompressionUtilities + +/// The Snappy Framed compression technique. +/// +/// https://en.wikipedia.org/wiki/Snappy_(compression)#Framing_format +/// +/// https://github.com/google/snappy +public struct SnappyFramed: Sendable { + public var algorithm: CompressionAlgorithm { + .snappyFramed + } + + public var compressionQuality: CompressionQuality { + .lossless + } +} \ No newline at end of file diff --git a/Sources/Snappy/iwa/IWA+Compress.swift b/Sources/Snappy/iwa/IWA+Compress.swift new file mode 100644 index 0000000..1657289 --- /dev/null +++ b/Sources/Snappy/iwa/IWA+Compress.swift @@ -0,0 +1,23 @@ + +import SwiftCompressionUtilities + +extension IWA { // TODO: finish + public typealias ConcreteCompressionConfiguration = Configuration + + /// - Parameters: + /// - data: Sequence of bytes to compress. + /// - Complexity: O(_n_) where _n_ is the length of `data`. + public func compress( + data: some Collection, + configuration: ConcreteCompressionConfiguration + ) -> ConcreteCompressionResult { + return .init() + } + + public func compress( + span: Span, + configuration: Configuration + ) throws(Never) -> [UInt8] { + return .init() + } +} \ No newline at end of file diff --git a/Sources/Snappy/iwa/IWA+Decompress.swift b/Sources/Snappy/iwa/IWA+Decompress.swift new file mode 100644 index 0000000..9635dd8 --- /dev/null +++ b/Sources/Snappy/iwa/IWA+Decompress.swift @@ -0,0 +1,16 @@ + +extension IWA { // TODO: finish + public typealias ConcreteDecompressionConfiguration = Configuration + public typealias ConcreteDecompressionResult = [UInt8] + + /// - Parameters: + /// - data: Collection of bytes to decompress. + /// - closure: Logic to execute when a byte is decompressed. + /// - Complexity: O(_n_) where _n_ is the length of `data`. + public func decompress( + data: some Collection, + configuration: ConcreteDecompressionConfiguration + ) -> ConcreteDecompressionResult { + return .init() + } +} \ No newline at end of file diff --git a/Sources/Snappy/iwa/IWA.swift b/Sources/Snappy/iwa/IWA.swift new file mode 100644 index 0000000..a41c5f1 --- /dev/null +++ b/Sources/Snappy/iwa/IWA.swift @@ -0,0 +1,32 @@ + +import SwiftCompressionUtilities + +/// The iWork Archive (iwa) compression technique. +/// +/// https://en.wikipedia.org/wiki/IWork +public struct IWA: Compressor, Decompressor { + /// Version of the iWork Archive to use. + public let version:IWAVersion + + public init(version: IWAVersion) { + self.version = version + } + + public var algorithm: CompressionAlgorithm { + .iwa(version: version) + } + + public var compressionQuality: CompressionQuality { + .lossless + } +} + +// MARK: Configuration +extension IWA { + public struct Configuration: CompressionConfiguration, DecompressionConfiguration { + public static var `default`: Self { .init() } + + public init() { + } + } +} \ No newline at end of file diff --git a/Sources/SnappyShim/module.modulemap b/Sources/SnappyShim/module.modulemap new file mode 100644 index 0000000..05b544a --- /dev/null +++ b/Sources/SnappyShim/module.modulemap @@ -0,0 +1,4 @@ +module SnappyShim [system] { + header "/usr/include/snappy-c.h" + export * +} \ No newline at end of file diff --git a/Sources/SwiftCompression/SwiftCompression.swift b/Sources/SwiftCompression/SwiftCompression.swift index a520976..66a1e85 100644 --- a/Sources/SwiftCompression/SwiftCompression.swift +++ b/Sources/SwiftCompression/SwiftCompression.swift @@ -1,9 +1,10 @@ +@_exported import Brotli @_exported import CompressionDNA -@_exported import CompressionCSS -@_exported import CompressionJavaScript @_exported import CompressionLZ -@_exported import CompressionSnappy +@_exported import Huffman +@_exported import Snappy +@_exported import Zlib @_exported import SwiftCompressionUtilities // MARK: Technique @@ -16,27 +17,27 @@ extension CompressionAlgorithm { case .mp3: return nil case .arithmetic: return nil - case .brotli(let windowSize): - return CompressionTechnique.brotli(windowSize: windowSize) + case .brotli(let quality, let windowSize, let mode): + return Brotli(quality: quality, windowSize: windowSize, mode: mode) case .bwt: return nil - case .deflate: return nil + case .deflate(let bufferSize, let level): + return Deflate(bufferSize: bufferSize, level: level) case .huffmanCoding: return nil - case .json: return nil case .lz4: return nil case .lz77(let windowSize, let bufferSize, let offsetBitWidth): switch offsetBitWidth { case 8: - return CompressionTechnique.LZ77(windowSize: windowSize, bufferSize: bufferSize) + return LZ77(windowSize: windowSize, bufferSize: bufferSize) case 16: - return CompressionTechnique.LZ77(windowSize: windowSize, bufferSize: bufferSize) + return LZ77(windowSize: windowSize, bufferSize: bufferSize) case 32: - return CompressionTechnique.LZ77(windowSize: windowSize, bufferSize: bufferSize) + return LZ77(windowSize: windowSize, bufferSize: bufferSize) case 64: - return CompressionTechnique.LZ77(windowSize: windowSize, bufferSize: bufferSize) + return LZ77(windowSize: windowSize, bufferSize: bufferSize) #if compiler(>=6.0) case 128: if #available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) { - return CompressionTechnique.LZ77(windowSize: windowSize, bufferSize: bufferSize) + return LZ77(windowSize: windowSize, bufferSize: bufferSize) } return nil #endif @@ -46,16 +47,18 @@ extension CompressionAlgorithm { case .lzw: return nil case .mtf: return nil case .runLengthEncoding(let minRun, let alwaysIncludeRunCount): - return CompressionTechnique.runLength(minRun: minRun, alwaysIncludeRunCount: alwaysIncludeRunCount) - case .snappy(let windowSize): - return CompressionTechnique.snappy(windowSize: windowSize) + return RunLengthEncoding(minRun: minRun, alwaysIncludeRunCount: alwaysIncludeRunCount) + case .snappy: + return Snappy() case .snappyFramed: - return CompressionTechnique.snappyFramed + //return CompressionTechnique.snappyFramed + return nil case .zstd: return nil case ._7z: return nil case .bzip2: return nil - case .gzip: return nil + case .gzip(let bufferSize, let level, let memLevel, let strategy): + return Gzip(bufferSize: bufferSize, level: level, memLevel: memLevel, strategy: strategy) case .rar: return nil case .h264: return nil @@ -69,9 +72,9 @@ extension CompressionAlgorithm { case .fibonacci: return nil case .dnaBinaryEncoding(let baseBits): - return CompressionTechnique.dnaBinaryEncoding(baseBits: baseBits) + return DNABinaryEncoding(baseBits: baseBits) case .dnaSingleBlockEncoding: - return CompressionTechnique.dnaSingleBlockEncoding + return DNASingleBlockEncoding() case .boringSSL: return nil @@ -80,15 +83,8 @@ extension CompressionAlgorithm { case .mpeg: return nil case .iwa(let version): - return CompressionTechnique.iwa(version: version) + return IWA(version: version) - case .programmingLanguage(let lang): - switch lang { - case .css: return CompressionTechnique.css - case .javascript: return CompressionTechnique.javascript - case .swift: return CompressionTechnique.swift - @unknown default: return nil - } @unknown default: return nil } } diff --git a/Sources/SwiftCompression/techniques/JSON.swift b/Sources/SwiftCompression/techniques/JSON.swift deleted file mode 100644 index ff9adaf..0000000 --- a/Sources/SwiftCompression/techniques/JSON.swift +++ /dev/null @@ -1,87 +0,0 @@ - -import SwiftCompressionUtilities - -#if canImport(Foundation) -import Foundation -#endif - -extension CompressionTechnique { - public enum JSON { // TODO: finish - } -} - -#if canImport(Foundation) -// MARK: Compress encodable -extension CompressionTechnique.JSON { - public static func compress(encodable: some Encodable) -> CompressionResult<[UInt8]>? { - guard let data = try? JSONEncoder().encode(encodable) else { return nil } - var compressed = [UInt8]() - compressed.reserveCapacity(data.count) - guard let object = try? JSONSerialization.jsonObject(with: data) else { return nil } - let quotationMark:UInt8 = 34 - let comma:UInt8 = 44 - func encode(_ value: Any) { - if var string = value as? String { - compressed.append(comma) - compressed.append(quotationMark) - while !string.isEmpty { - let char = string.removeFirst() - if let value = char.asciiValue { - compressed.append(value) - } - } - compressed.append(quotationMark) - } else if let int = value as? any FixedWidthInteger { - compressed.append(comma) - var builder = CompressionTechnique.DataBuilder() - builder.write(bits: int.bits) - builder.finalize() - compressed.append(contentsOf: builder.data) - } else if let dic = value as? [String:Any] { - compressed.append(comma) - for value in dic.values { - encode(value) - } - } else if let array = value as? [Any] { - compressed.append(comma) - for value in array { - encode(value) - } - } else if let bool = value as? Bool { - compressed.append(comma) - compressed.append(contentsOf: bool ? [] : []) - } - } - if let dic = object as? [String:Any] { - for value in dic.values { - encode(value) - } - compressed[0] = 91 // [ - compressed.append(93) // ] - } else if let array = object as? [Any] { - for value in array { - encode(value) - } - compressed[0] = 91 // [ - compressed.append(93) // ] - } else { - return nil - } - return compress(data: compressed) - } -} -#endif - -// MARK: Compress -extension CompressionTechnique.JSON { - public static func compress(data: some Sequence) -> CompressionResult<[UInt8]>? { - return nil - } -} - -// MARK: Decompress -extension CompressionTechnique.JSON { - public static func decompress(data: [UInt8]) -> [UInt8] { - return data - } -} \ No newline at end of file diff --git a/Sources/SwiftCompression/techniques/MoveToFront.swift b/Sources/SwiftCompression/techniques/MoveToFront.swift index 285f073..c591f11 100644 --- a/Sources/SwiftCompression/techniques/MoveToFront.swift +++ b/Sources/SwiftCompression/techniques/MoveToFront.swift @@ -1,16 +1,14 @@ import SwiftCompressionUtilities -extension CompressionTechnique { - /// The Move-to-front transform compression technique. - /// - /// https://en.wikipedia.org/wiki/Move-to-front_transform - public enum MoveToFront { // TODO: finish - } +/// The Move-to-front transform compression technique. +/// +/// https://en.wikipedia.org/wiki/Move-to-front_transform +public enum MoveToFront { // TODO: finish } // MARK: Transform -extension CompressionTechnique.MoveToFront { +extension MoveToFront { public static func transform( data: some Sequence, reserveCapacity: Int = 1024 @@ -26,7 +24,7 @@ extension CompressionTechnique.MoveToFront { } } -extension CompressionTechnique.MoveToFront { +extension MoveToFront { struct Record { } } \ No newline at end of file diff --git a/Sources/SwiftCompression/techniques/RunLengthEncoding.swift b/Sources/SwiftCompression/techniques/RunLengthEncoding.swift deleted file mode 100644 index f7f7eab..0000000 --- a/Sources/SwiftCompression/techniques/RunLengthEncoding.swift +++ /dev/null @@ -1,123 +0,0 @@ - -import SwiftCompressionUtilities - -extension CompressionTechnique { - - /// The Run-length encoding compression technique. - /// - /// https://en.wikipedia.org/wiki/Run-length_encoding - public static func runLength(minRun: Int, alwaysIncludeRunCount: Bool) -> RunLengthEncoding { - return RunLengthEncoding(minRun: minRun, alwaysIncludeRunCount: alwaysIncludeRunCount) - } - - public struct RunLengthEncoding: Compressor, Decompressor { - public typealias CompressClosureParameters = (run: Int, byte: UInt8) - - /// Minimum run count required to compress identical sequential bytes. - public let minRun:Int - - /// Whether or not to always include the run count in the result, regardless of run count. - public let alwaysIncludeRunCount:Bool - - public init(minRun: Int, alwaysIncludeRunCount: Bool) { - self.minRun = minRun - self.alwaysIncludeRunCount = alwaysIncludeRunCount - } - - @inlinable - public var algorithm: CompressionAlgorithm { - .runLengthEncoding(minRun: minRun, alwaysIncludeRunCount: alwaysIncludeRunCount) - } - - @inlinable - public var quality: CompressionQuality { - .lossless - } - } -} - -// MARK: Compress -extension CompressionTechnique.RunLengthEncoding { - public func compressClosure(closure: @escaping (UInt8) -> Void) -> @Sendable (CompressClosureParameters) -> Void { - if alwaysIncludeRunCount { - return { (arg) in - let (run, runByte) = arg - closure(UInt8(191 + run)) - closure(runByte) - } - } else { - return { (arg) in - let (run, runByte) = arg - if runByte <= 191 && run < minRun { - for byte in Array(repeating: runByte, count: run) { - closure(byte) - } - } else { - closure(UInt8(191 + run)) - closure(runByte) - } - } - } - } - - /// - Parameters: - /// - data: Sequence of bytes to compress. - /// - minRun: Minimum run count required to compress identical sequential bytes. - /// - closure: Logic to execute for a run. - /// - Complexity: O(_n_) where _n_ is the length of `data`. - public func compress(data: some Sequence, closure: (CompressClosureParameters) -> Void) -> UInt8? { - var run = 0 - var runByte:UInt8? = nil - data.withContiguousStorageIfAvailable { p in - for index in 0.., closure: (UInt8) -> Void) { - let count = data.count - var index = 0 - var run:UInt8 = 0 - var character:UInt8 = 0 - while index < count { - run = data[index] - if run > 191 { - run -= 191 - character = data[index+1] - index += 2 - for _ in 0.., reserveCapacity: Int) throws(CompressionError) -> CompressionResult<[UInt8]> { - throw CompressionError.unsupportedOperation // TODO: support? - } - - /// - Parameters: - /// - data: Sequence of bytes to compress. - /// - closure: Logic to execute for a run. - /// - Complexity: O(_n_) where _n_ is the length of `data`. - public func compress(data: some Sequence, closure: (UInt8) -> Void) -> UInt8? { - return nil // TODO: support? - } -} - -/* -extension CompressionTechnique.SwiftLang { // TODO: support - public func compress(filePath: String) throws(CompressionError) -> String { - return "" - } -}*/ -// MARK: Minify -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension CompressionTechnique.SwiftLang { - /// Minifies Swift code to make it suitable for production-only usage, which results in the minimum binary size required to represent the same code. - /// - /// Optionally removes documentation, comments, unnecessary whitespace and access control declarations, and unit tests - /// - /// - Parameters: - /// - swiftSourceCode: Literal, valid, Swift code. - func minify(swiftSourceCode: T) throws -> T { - // TODO: finish - let accessControlInternal = try Regex(#"(internal\s+)"#) - let documentation = try Regex(#"(\/\/\/.*)"#) - let comments = try Regex(#"(\/\/.*)"#) - - var cleanup:[(Regex, String)] = try [ - (Regex(#"(\s*:\s*)"#), ":"), - (Regex(#"(\s*{\s+)"#), "{"), - (Regex(#"(\s*\(\s+)"#), "("), - (Regex(#"(\s*\)\s+)"#), ")"), - - (Regex(#"(\s*{\s*get\s*})"#), "{get}"), // { get } - (Regex(#"(\s*{\s*set\s*})"#), "{set}"), // { set } - (Regex(#"(\s*{(\s*get\s*)\s*(\s*set\s*)\s*})"#), "{get set}"), // { get set } - (Regex(#"(\s*{(\s*set\s*)\s*(\s*get\s*)\s*})"#), "{set get}") // { set get } - ] - let recursive:[Regex:String] = try [ - Regex(#"(\s*{\s+{)"#): "{{", - Regex(#"(\s*}\s+})"#): "}}" - ] - let finalCleanup:[(Regex, String)] = try [ - (Regex(#"(}\s+fileprivate\s+var)"#), "};fileprivate var"), - (Regex(#"(}\s+fileprivate\s+let)"#), "};fileprivate let"), - (Regex(#"(}\s+open\s+var)"#), "};open var"), - (Regex(#"(}\s+open\s+let)"#), "};open let"), - (Regex(#"(}\s+package\s+var)"#), "};package var"), - (Regex(#"(}\s+package\s+let)"#), "};package let"), - (Regex(#"(}\s+private\s+var)"#), "};private var"), - (Regex(#"(}\s+private\s+let)"#), "};private let"), - (Regex(#"(}\s+public\s+var)"#), "};public var"), - (Regex(#"(}\s+public\s+let)"#), "};public let"), - (Regex(#"(}\s+var)"#), "};var"), - (Regex(#"(}\s+let)"#), "};let"), - ] - cleanup.append(contentsOf: finalCleanup) - - //let stringLiterals:Regex = try Regex(#"(".+")"#) - var result = String(swiftSourceCode) - //let stringLiteralMatches = result.matches(of: stringLiterals) - result.replace(accessControlInternal, with: "") - result.replace(documentation, with: "") - result.replace(comments, with: "") - for (regex, replacement) in recursive { - var matches = result.matches(of: regex) - while !matches.isEmpty { - for match in matches.reversed() { - result.replaceSubrange(match.range, with: replacement) - } - matches = result.matches(of: regex) - } - } - for (regex, replacement) in cleanup { - result.replace(regex, with: replacement) - } - while result.first?.isWhitespace == true { - result.removeFirst() - } - return T(stringLiteral: result) - } -} - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension Regex: @retroactive Equatable { - public static func == (lhs: Regex, rhs: Regex) -> Bool { - false - } -} - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension Regex: @retroactive Hashable { - public func hash(into hasher: inout Hasher) { - } -} \ No newline at end of file diff --git a/Sources/SwiftCompression/techniques/runLength/RunLengthEncoding+Compress+Span.swift b/Sources/SwiftCompression/techniques/runLength/RunLengthEncoding+Compress+Span.swift new file mode 100644 index 0000000..e4a32db --- /dev/null +++ b/Sources/SwiftCompression/techniques/runLength/RunLengthEncoding+Compress+Span.swift @@ -0,0 +1,15 @@ + +import SwiftCompressionUtilities + +extension RunLengthEncoding { + public func compress( + span: Span, + configuration: CompressConfiguration + ) throws(Never) -> ConcreteCompressionResult { + var result = ConcreteCompressionResult() + span.withUnsafeBufferPointer { + compress(buffer: $0, closure: compressClosure(closure: { result.append($0) })) + } + return result + } +} \ No newline at end of file diff --git a/Sources/SwiftCompression/techniques/runLength/RunLengthEncoding+Compress.swift b/Sources/SwiftCompression/techniques/runLength/RunLengthEncoding+Compress.swift new file mode 100644 index 0000000..711a613 --- /dev/null +++ b/Sources/SwiftCompression/techniques/runLength/RunLengthEncoding+Compress.swift @@ -0,0 +1,82 @@ + +import SwiftCompressionUtilities + +extension RunLengthEncoding: Compressor { + public typealias ConcreteCompressionConfiguration = CompressConfiguration + + public func compress( + data: some Collection, + configuration: ConcreteCompressionConfiguration + ) -> ConcreteCompressionResult { + var result = ConcreteCompressionResult() + data.withContiguousStorageIfAvailable { + compress(buffer: $0, closure: compressClosure(closure: { result.append($0) })) + } + return result + } + + func compressClosure(closure: @escaping (UInt8) -> Void) -> (CompressClosureParameters) -> Void { + if alwaysIncludeRunCount { + return { (arg) in + let (run, runByte) = arg + closure(UInt8(191 + run)) + closure(runByte) + } + } else { + return { (arg) in + let (run, runByte) = arg + if runByte <= 191 && run < minRun { + for byte in Array(repeating: runByte, count: run) { + closure(byte) + } + } else { + closure(UInt8(191 + run)) + closure(runByte) + } + } + } + } + + /// - Parameters: + /// - data: Sequence of bytes to compress. + /// - minRun: Minimum run count required to compress identical sequential bytes. + /// - closure: Logic to execute for a run. + /// - Complexity: O(_n_) where _n_ is the length of `data`. + func compress( + buffer: UnsafeBufferPointer, + closure: (CompressClosureParameters) -> Void + ) { + var run = 0 + var runByte:UInt8? = nil + for index in 0.., + configuration: ConcreteDecompressionConfiguration + ) -> ConcreteDecompressionResult { + var result = ConcreteDecompressionResult() + decompress(data: data, closure: { result.append($0) }) + return result + } + + /// - Parameters: + /// - data: Sequence of bytes to decompress. + /// - closure: Logic to execute for a run. + /// - Complexity: O(_n_) where _n_ is the length of `data`. + private func decompress(data: some Collection, closure: (UInt8) -> Void) { + let count = data.count + var index = 0 + var run:UInt8 = 0 + var character:UInt8 = 0 + while index < count { + run = data[index] + if run > 191 { + run -= 191 + character = data[index+1] + index += 2 + for _ in 0..) -> [Int] { - var table = Array(repeating: 0, count: 255) - for byte in data { - table[Int(byte)] += 1 - } - return table - } - - /// Creates a lookup frequency table from a sequence of raw bytes. - /// - /// - Parameters: - /// - data: Sequence of raw bytes. - /// - Returns: A lookup frequency table. - /// - Complexity: O(_n_) where _n_ is the length of `data`. - public static func buildFrequencyTable(data: some Sequence) -> [UInt8:Int] { - var table = [UInt8:Int]() - for byte in data { - table[byte, default: 0] += 1 - } - return table - } - - /// Creates a universal frequency table from a character frequency dictionary. - /// - /// - Parameters: - /// - chars: A frequency table that represents how many times a character is present. - /// - Returns: A universal frequency table. - /// - Complexity: O(_n_) where _n_ is the sum of the `Character` utf8 lengths in `chars`. - public static func buildFrequencyTable(chars: [Character:Int]) -> [Int] { - var table = Array(repeating: 0, count: 255) - for (char, freq) in chars { - for byte in char.utf8 { - table[Int(byte)] = freq - } - } - return table - } +public enum CompressionTechnique: Sendable { } \ No newline at end of file diff --git a/Sources/SwiftCompressionUtilities/Compressor.swift b/Sources/SwiftCompressionUtilities/Compressor.swift index 08e92ef..a02e8ac 100644 --- a/Sources/SwiftCompressionUtilities/Compressor.swift +++ b/Sources/SwiftCompressionUtilities/Compressor.swift @@ -1,71 +1,16 @@ -// MARK: Compressor -public protocol Compressor: AnyCompressor { - associatedtype CompressClosureParameters - func compressClosure(closure: @escaping @Sendable (UInt8) -> Void) -> @Sendable (CompressClosureParameters) -> Void +public protocol Compressor: AnyCompressor, ~Copyable { + associatedtype ConcreteCompressionConfiguration:CompressionConfiguration + associatedtype ConcreteCompressionResult = [UInt8] + associatedtype ConcreteCompressionError:Error = CompressionError func compress( data: some Collection, - reserveCapacity: Int - ) throws -> CompressionResult<[UInt8]> + configuration: ConcreteCompressionConfiguration + ) throws(ConcreteCompressionError) -> ConcreteCompressionResult - /// Compress a collection of bytes using this technique. - /// - /// - Parameters: - /// - data: Collection of bytes to compress. - /// - closure: Logic to execute when a byte is compressed. - /// - Returns: The number of valid bits in the last byte. - /// - Complexity: where _n_ is the length of `data` - /// - DNA binary encoding: O(_n_) - /// - LZ77: O(_n_) - /// - Snappy: O(_n_) func compress( - data: some Collection, - closure: (CompressClosureParameters) -> Void - ) throws(CompressionError) -> UInt8? -} - -// MARK: Compress -extension Compressor { - /// Compress a collection of bytes using this technique. - /// - /// - Parameters: - /// - data: Collection of bytes to compress. - /// - reserveCapacity: Space to reserve for the compressed result. - /// - Complexity: where _n_ is the length of `data` - /// - DNA binary encoding: O(_n_) - /// - LZ77: O(_n_) - /// - Snappy: O(_n_) - public func compress( - data: some Collection, - reserveCapacity: Int = 1024 - ) throws(CompressionError) -> CompressionResult<[UInt8]> { - var compressed = [UInt8]() - compressed.reserveCapacity(reserveCapacity) - let validBitsInLastByte:UInt8 = try compress(data: data, closure: compressClosure { compressed.append($0) }) ?? 8 // TODO: fix Swift 6 error - return CompressionResult(data: compressed, validBitsInLastByte: validBitsInLastByte) - } - - /// Compress a collection of bytes into a stream using this technique. - /// - /// - Parameters: - /// - data: Collection of bytes to compress. - /// - continuation: The `AsyncStream.Continuation`. - /// - Complexity: where _n_ is the length of `data` - /// - DNA binary encoding: O(_n_) - /// - LZ77: O(_n_) - /// - Snappy: O(_n_) - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) - public func compress( - data: some Collection, - continuation: AsyncStream.Continuation - ) throws { - // TODO: finish - let _:UInt8 = try compress(data: data, closure: compressClosure { continuation.yield($0) }) ?? 8 - } -} -extension Compressor where CompressClosureParameters == UInt8 { - public func compressClosure(closure: @escaping @Sendable (UInt8) -> Void) -> @Sendable (CompressClosureParameters) -> Void { - closure - } + span: Span, + configuration: ConcreteCompressionConfiguration + ) throws(ConcreteCompressionError) -> ConcreteCompressionResult } \ No newline at end of file diff --git a/Sources/SwiftCompressionUtilities/DecompressionConfiguration.swift b/Sources/SwiftCompressionUtilities/DecompressionConfiguration.swift new file mode 100644 index 0000000..35d5464 --- /dev/null +++ b/Sources/SwiftCompressionUtilities/DecompressionConfiguration.swift @@ -0,0 +1,4 @@ + +public protocol DecompressionConfiguration: Sendable, ~Copyable { + static var `default`: Self { get } +} \ No newline at end of file diff --git a/Sources/SwiftCompressionUtilities/Decompressor.swift b/Sources/SwiftCompressionUtilities/Decompressor.swift index 484f9ad..c88f47c 100644 --- a/Sources/SwiftCompressionUtilities/Decompressor.swift +++ b/Sources/SwiftCompressionUtilities/Decompressor.swift @@ -1,61 +1,15 @@ // MARK: Decompressor -public protocol Decompressor: AnyDecompressor { - associatedtype DecompressClosureParameters +public protocol Decompressor: AnyDecompressor, ~Copyable { + associatedtype ConcreteDecompressionConfiguration:DecompressionConfiguration + associatedtype ConcreteDecompressionResult /// Decompress a collection of bytes using this technique. /// /// - Parameters: /// - data: Collection of bytes to decompress. - /// - closure: Logic to execute when a byte is decompressed. - /// - Complexity: where _n_ is the length of `data` - /// - DNA binary encoding: O(_n_) - /// - LZ77: O(_n_) - /// - Run-length encoding: O(_n_) - /// - Snappy: O(_n_) func decompress( data: some Collection, - closure: (DecompressClosureParameters) -> Void - ) throws(DecompressionError) -} - -// MARK: Decompress -extension Decompressor where DecompressClosureParameters == UInt8 { - /// Decompress a collection of bytes using this technique. - /// - /// - Parameters: - /// - data: Collection of bytes to decompress. - /// - reserveCapacity: Space to reserve for the decompressed result (if no length was decompressed). - /// - Complexity: where _n_ is the length of `data` - /// - DNA binary encoding: O(_n_) - /// - LZ77: O(_n_) - /// - Run-length encoding: O(_n_) - /// - Snappy: O(_n_) - public func decompress( - data: some Collection, - reserveCapacity: Int = 1024 - ) throws(DecompressionError) -> [UInt8] { - var decompressed = [UInt8]() - decompressed.reserveCapacity(reserveCapacity) - try decompress(data: data) { decompressed.append($0) } - return decompressed - } - - /// Decompress a collection of bytes into a stream using this technique. - /// - /// - Parameters: - /// - data: Collection of bytes to decompress. - /// - continuation: The `AsyncThrowingStream.Continuation`. - /// - Complexity: where _n_ is the length of `data` - /// - DNA binary encoding: O(_n_) - /// - LZ77: O(_n_) - /// - Run-length encoding: O(_n_) - /// - Snappy: O(_n_) - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) - public func decompress( - data: some Collection, - continuation: AsyncThrowingStream.Continuation - ) throws(DecompressionError) { - try decompress(data: data) { continuation.yield($0) } - } + configuration: ConcreteDecompressionConfiguration + ) throws(DecompressionError) -> ConcreteDecompressionResult } \ No newline at end of file diff --git a/Sources/SwiftCompressionUtilities/ProgrammingLanguage.swift b/Sources/SwiftCompressionUtilities/ProgrammingLanguage.swift deleted file mode 100644 index d81493f..0000000 --- a/Sources/SwiftCompressionUtilities/ProgrammingLanguage.swift +++ /dev/null @@ -1,7 +0,0 @@ - -/// List of supported programming languages with compression techniques. -public enum ProgrammingLanguage: Sendable { - case css - case javascript - case swift -} \ No newline at end of file diff --git a/Sources/SwiftCompressionUtilities/SwiftCompressionUtilities.swift b/Sources/SwiftCompressionUtilities/SwiftCompressionUtilities.swift index c7834ec..745a33b 100644 --- a/Sources/SwiftCompressionUtilities/SwiftCompressionUtilities.swift +++ b/Sources/SwiftCompressionUtilities/SwiftCompressionUtilities.swift @@ -1,4 +1,7 @@ + +/* + // MARK: Collection extension Collection where Element == UInt8 { /// Compress a copy of this data using the specified technique(s). @@ -133,4 +136,6 @@ extension String { self = try String(data: Data(data.compressed(using: technique).data), encoding: encoding) ?? "" } } -#endif \ No newline at end of file +#endif + +*/ \ No newline at end of file diff --git a/Sources/SwiftCompressionUtilities/errors/DecompressionError.swift b/Sources/SwiftCompressionUtilities/errors/DecompressionError.swift index 95da849..12af7d5 100644 --- a/Sources/SwiftCompressionUtilities/errors/DecompressionError.swift +++ b/Sources/SwiftCompressionUtilities/errors/DecompressionError.swift @@ -2,5 +2,5 @@ /// SwiftCompression errors that can be thrown when decompressing data. public enum DecompressionError: Error { case malformedInput - case unsupportedOperation(String = "") + case unsupportedOperation } \ No newline at end of file diff --git a/Sources/SwiftCompressionUtilities/extensions/SequenceExtensions.swift b/Sources/SwiftCompressionUtilities/extensions/SequenceExtensions.swift index 90a9a28..9ef1b92 100644 --- a/Sources/SwiftCompressionUtilities/extensions/SequenceExtensions.swift +++ b/Sources/SwiftCompressionUtilities/extensions/SequenceExtensions.swift @@ -1,14 +1,4 @@ -#if canImport(Foundation) -import Foundation - -extension Sequence where Element == UInt8 { - package func hexadecimal(separator: String = "") -> String { - return map({ String.init(format: "%02X", $0) }).joined(separator: separator) - } -} -#endif - extension Collection { /// - Returns: The element at the given index if within bounds. Otherwise `nil`. /// - Complexity: O(1). diff --git a/Sources/SwiftCompressionUtilities/techniques/Huffman.swift b/Sources/SwiftCompressionUtilities/techniques/Huffman.swift deleted file mode 100644 index cb83312..0000000 --- a/Sources/SwiftCompressionUtilities/techniques/Huffman.swift +++ /dev/null @@ -1,345 +0,0 @@ - -extension CompressionTechnique { - /// The Huffman coding compression technique. - /// - /// https://en.wikipedia.org/wiki/Huffman_coding - public enum Huffman { - } -} - -// MARK: Compress -extension CompressionTechnique.Huffman { - /// Compress a sequence of bytes using the Huffman Coding technique. - /// - /// - Parameters: - /// - data: Sequence of bytes to compress. - public static func compress(data: some Sequence) -> CompressionResult<[UInt8]>? { - return compress(data: data) { frequencies, codes, root in - var compressed:[UInt8] = [8] - var vBitsInLastByte:UInt8 = 8 - if let (lastByte, validBitsInLastByte) = translate(data: data, codes: codes, closure: { compressed.append($0) }) { - compressed[0] = validBitsInLastByte - compressed.append(lastByte) - vBitsInLastByte = validBitsInLastByte - } - return CompressionResult(data: compressed, rootNode: root, frequencyTable: frequencies, validBitsInLastByte: vBitsInLastByte) - } - } - - /// Compress a sequence of bytes to a stream using the Huffman Coding technique. - /// - /// - Parameters: - /// - data: Sequence of bytes to compress. - /// - continuation: The `AsyncStream.Continuation`. - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) - public static func compress( - data: some Sequence, - bufferingPolicy limit: AsyncStream.Continuation.BufferingPolicy = .unbounded - ) -> CompressionResult>? { - // TODO: fix - return compress(data: data) { frequencies, codes, root in - var vBitsInLastByte:UInt8 = 8 - let stream = AsyncStream(bufferingPolicy: limit) { continuation in - if let (lastByte, validBitsInLastByte) = translate(data: data, codes: codes, closure: { continuation.yield($0) }) { - continuation.yield(lastByte) - vBitsInLastByte = validBitsInLastByte - } - continuation.finish() - } - return CompressionResult(data: stream, rootNode: root, frequencyTable: frequencies, validBitsInLastByte: vBitsInLastByte) - } - } - - public static func compress(data: some Sequence, closure: ([Int], [UInt8:String], Node) -> T) -> T? { - var frequencies = Array(repeating: 0, count: Int(UInt8.max-1)) - for byte in data { - frequencies[Int(byte)] += 1 - } - guard let root = buildTree(frequencies: frequencies) else { return nil } - var codes = [UInt8:String]() - generateCodes(node: root, codes: &codes) - return closure(frequencies, codes, root) - } - - /// - Complexity: O(_n_ + _m_) where _n_ is the length of `data` and _m_ is the sum of the code lengths. - public static func translate(data: some Sequence, codes: [UInt8:String], closure: (UInt8) -> Void) -> (lastByte: UInt8, validBits: UInt8)? { - var builder = ByteBuilder() - for byte in data { - if let tree = codes[byte] { - for char in tree { - if let wrote = builder.write(bit: char == "1") { - closure(wrote) - } - } - } - } - return builder.flush() - } -} - -// MARK: Decompress -extension CompressionTechnique.Huffman { - /// Decompress a sequence of bytes using the Huffman Coding technique. - /// - /// - Parameters: - /// - data: Sequence of bytes to decompress. - /// - root: The root Huffman Node. - /// - Complexity: O(_n_) where _n_ is the length of `data`. - public static func decompress(data: [UInt8], root: Node?) -> [UInt8] { - var result = [UInt8]() - decompress(data: data, root: root) { result.append($0) } - return result - } - - /// Decompress a sequence of bytes into a stream using the Huffman Coding technique. - /// - /// - Parameters: - /// - data: Sequence of bytes to decompress. - /// - root: The root Huffman Node. - /// - continuation: The `AsyncStream.Continuation`. - /// - Complexity: O(_n_) where _n_ is the length of `data`. - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) - public static func decompress( - data: [UInt8], - root: Node?, - continuation: AsyncStream.Continuation - ) { - decompress(data: data, root: root) { continuation.yield($0) } - } - - /// Decompress a sequence of bytes using the Huffman Coding technique. - /// - /// - Complexity: O(_n_) where _n_ is the length of `data`. - public static func decompress(data: [UInt8], root: Node?, closure: (UInt8) -> Void) { - let countMinusOne = data.count-1 - var node = root - var index = 1 - while index < countMinusOne { - let bits = data[index].bits - for bit in 0..<8 { - if bits[bit] { - node = node?.right - } else { - node = node?.left - } - if let char = node?.character { - closure(char) - node = root - } - } - index += 1 - } - let validBitsInLastByte = data[0] - let lastBits = data[countMinusOne].bits - for bit in 0.. [UInt8] { - guard let root = buildTree(frequencies: frequencyTable) else { return data } - return decompress(data: data, root: root) - } - - /// Decompress a sequence of bytes into a stream using the Huffman Coding technique. - /// - /// - Parameters: - /// - data: Sequence of bytes to decompress. - /// - frequencyTable: A Huffman frequency table of characters. - /// - continuation: The `AsyncStream.Continuation`. - // /// - Complexity: O(_n_ + _m_) where _n_ is the length of `data` and _m_ is the length of `frequencyTable`. // TODO: FIX - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) - public static func decompress( - data: [UInt8], - frequencyTable: [Int], - continuation: AsyncStream.Continuation - ) { - guard let root = buildTree(frequencies: frequencyTable) else { return } - decompress(data: data, root: root, continuation: continuation) - } - - public static func decompress(data: [UInt8], frequencyTable: [Int], closure: (UInt8) -> Void) { - guard let root = buildTree(frequencies: frequencyTable) else { return } - decompress(data: data, root: root, closure: closure) - } - - public static func decompress(data: [UInt8], codes: [[Bool]:UInt8], closure: (UInt8) -> Void) { - var code = [Bool]() - code.reserveCapacity(3) - for bit in data { - code.append(bit == 1) - if let char = codes[code] { - closure(char) - code.removeAll(keepingCapacity: true) - } - } - } -} - -// MARK: Node -extension CompressionTechnique.Huffman { - /// A Huffman Node. - public final class Node: Comparable, Hashable, Sendable { - public static func < (left: Node, right: Node) -> Bool { - return left.frequency < right.frequency - } - public static func == (left: Node, right: Node) -> Bool { - return left.frequency == right.frequency - } - - public let character:UInt8? - public let frequency:Int - public let left:Node? - public let right:Node? - - public init( - character: UInt8? = nil, - frequency: Int, - left: Node? = nil, - right: Node? = nil - ) { - self.character = character - self.frequency = frequency - self.left = left - self.right = right - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(character) - hasher.combine(frequency) - hasher.combine(left) - hasher.combine(right) - } - } -} - -// MARK: PriorityQueue -extension CompressionTechnique.Huffman { - public struct PriorityQueue { - public var heap:[T] - - public init(heap: [T] = []) { - self.heap = heap - } - - /// - Complexity: O(_n_) where _n_ is the length of the sequence. - mutating func push(_ element: T) { - heap.append(element) - siftUp(from: heap.count - 1) - } - - mutating func pop() -> T? { - guard !heap.isEmpty else { return nil } - if heap.count == 1 { - return heap.removeLast() - } - let root = heap[0] - heap[0] = heap.removeLast() - siftDown(from: 0) - return root - } - - /// - Complexity: O(_n_) where _n_ is the distance between `0` and `index`. - mutating func siftUp(from index: Int) { - var index = index - while index > 0 { - let parentIndex = (index - 1) / 2 - if heap[index] >= heap[parentIndex] { break } - heap.swapAt(index, parentIndex) - index = parentIndex - } - } - - mutating func siftDown(from index: Int) { - let element = heap[index] - let count = heap.count - var index = index - while true { - let leftIndex = (2 * index) + 1 - let rightIndex = leftIndex + 1 - var minIndex = index - - if leftIndex < count && heap[leftIndex] < heap[minIndex] { - minIndex = leftIndex - } - if rightIndex < count && heap[rightIndex] < heap[minIndex] { - minIndex = rightIndex - } - if minIndex == index { break } - - heap[index] = heap[minIndex] - index = minIndex - } - heap[index] = element - } - } -} - -// MARK: Logic -extension CompressionTechnique.Huffman { - /// Builds a Huffman tree. - /// - /// - Parameters: - /// - frequencies: A universal frequency table. - /// - Returns: The root node of the Huffman tree. - /// - Complexity: O(?) - static func buildTree(frequencies: [Int]) -> Node? { - var queue = PriorityQueue() - for (char, freq) in frequencies.enumerated() { - if freq != 0 { - queue.push(Node(character: UInt8(char), frequency: freq)) - } - } - while queue.heap.count > 1 { - let left = queue.pop()! - let right = queue.pop()! - let merged = Node(frequency: left.frequency + right.frequency, left: left, right: right) - queue.push(merged) - } - return queue.pop() - } - - /// Generates the binary codes for a node. - /// - /// - Complexity: O(1). - static func generateCodes(node: Node?, code: String = "", codes: inout [UInt8:String]) { - guard let node else { return } - if let char = node.character { - codes[char] = code - } else { - generateCodes(node: node.left, code: code + "0", codes: &codes) - generateCodes(node: node.right, code: code + "1", codes: &codes) - } - } -} - -// MARK: StringProtocol -extension StringProtocol { - /// - Returns: A Huffman frequency table for the characters. - /// - Complexity: O(_n_) where _n_ is the length of the collection. - public func huffmanFrequencyTable() -> [Int] { - var table = Array(repeating: 0, count: 255) - for char in self { - for byte in char.utf8 { - table[Int(byte)] += 1 - } - } - return table - } -} \ No newline at end of file diff --git a/Sources/Zlib/deflate/DEFLATE+Compress+Span.swift b/Sources/Zlib/deflate/DEFLATE+Compress+Span.swift new file mode 100644 index 0000000..ca9c44d --- /dev/null +++ b/Sources/Zlib/deflate/DEFLATE+Compress+Span.swift @@ -0,0 +1,16 @@ + +import ZlibShim + +extension Deflate { + public func compress( + span: Span, + configuration: ConcreteCompressionConfiguration = .default + ) -> ConcreteCompressionResult { + var stream = z_stream() + let status = deflateInit_(&stream, level, ZLIB_VERSION, Int32(MemoryLayout.size)) + guard status == Z_OK else { return nil } + return span.withUnsafeBufferPointer { + return compress(baseAddress: $0.baseAddress, count: $0.count, configuration: configuration, stream: &stream) + } + } +} \ No newline at end of file diff --git a/Sources/Zlib/deflate/DEFLATE+Compress.swift b/Sources/Zlib/deflate/DEFLATE+Compress.swift new file mode 100644 index 0000000..6c7fc5f --- /dev/null +++ b/Sources/Zlib/deflate/DEFLATE+Compress.swift @@ -0,0 +1,61 @@ + +import SwiftCompressionUtilities +import ZlibShim + +extension Deflate: Compressor { + public typealias ConcreteCompressionConfiguration = CompressConfiguration + public typealias ConcreteCompressionResult = [UInt8]? + + public func compress( + data: some Collection, + configuration: ConcreteCompressionConfiguration = .default + ) -> ConcreteCompressionResult { + var stream = z_stream() + let status = deflateInit_(&stream, level, ZLIB_VERSION, Int32(MemoryLayout.size)) + guard status == Z_OK else { return nil } + return data.withContiguousStorageIfAvailable { rawBuffer in + return compress(baseAddress: rawBuffer.baseAddress, count: data.count, configuration: configuration, stream: &stream) + } ?? nil + } + + package func compress( + baseAddress: UnsafePointer?, + count: Int, + configuration: ConcreteCompressionConfiguration, + stream: inout z_stream + ) -> ConcreteCompressionResult { + defer { deflateEnd(&stream) } + + var compressed = [UInt8]() + compressed.reserveCapacity(configuration.reserveCapacity) + var buffer = [UInt8](repeating: 0, count: bufferSize) + stream.next_in = UnsafeMutablePointer(mutating: baseAddress) + stream.avail_in = UInt32(count) + buffer.withUnsafeMutableBufferPointer { b in + while stream.avail_out == 0 { + stream.next_out = b.baseAddress + stream.avail_out = UInt32(bufferSize) + deflate(&stream, Z_FINISH) + + let count = bufferSize - Int(stream.avail_out) + compressed.append(contentsOf: b[0.., + configuration: ConcreteDecompressionConfiguration = .default + ) -> ConcreteDecompressionResult { + var stream = z_stream() + let status = inflateInit_(&stream, ZLIB_VERSION, Int32(MemoryLayout.size)) + guard status == Z_OK else { return nil } + return span.withUnsafeBufferPointer { + return decompress(baseAddress: $0.baseAddress, count: $0.count, configuration: configuration, stream: &stream) + } ?? nil + } +} \ No newline at end of file diff --git a/Sources/Zlib/deflate/DEFLATE+Decompress.swift b/Sources/Zlib/deflate/DEFLATE+Decompress.swift new file mode 100644 index 0000000..7df7546 --- /dev/null +++ b/Sources/Zlib/deflate/DEFLATE+Decompress.swift @@ -0,0 +1,61 @@ + +import SwiftCompressionUtilities +import ZlibShim + +extension Deflate: Decompressor { + public typealias ConcreteDecompressionConfiguration = DecompressConfiguration + public typealias ConcreteDecompressionResult = [UInt8]? + + public func decompress( + data: some Collection, + configuration: ConcreteDecompressionConfiguration = .default + ) -> ConcreteDecompressionResult { + var stream = z_stream() + let status = inflateInit_(&stream, ZLIB_VERSION, Int32(MemoryLayout.size)) + guard status == Z_OK else { return nil } + return data.withContiguousStorageIfAvailable { + return decompress(baseAddress: $0.baseAddress, count: $0.count, configuration: configuration, stream: &stream) + } ?? nil + } + + package func decompress( + baseAddress: UnsafePointer?, + count: Int, + configuration: ConcreteDecompressionConfiguration, + stream: inout z_stream + ) -> ConcreteDecompressionResult { + defer { inflateEnd(&stream) } + + var decompressedData = [UInt8]() + var buffer = [UInt8](repeating: 0, count: bufferSize) + stream.next_in = UnsafeMutablePointer(mutating: baseAddress) + stream.avail_in = UInt32(count) + + buffer.withUnsafeMutableBufferPointer { b in + while stream.avail_out == 0 { + stream.next_out = b.baseAddress + stream.avail_out = UInt32(bufferSize) + inflate(&stream, Z_NO_FLUSH) + + let count = bufferSize - Int(stream.avail_out) + decompressedData.append(contentsOf: b[0.., + configuration: ConcreteCompressionConfiguration = .default + ) -> ConcreteCompressionResult { + var stream = z_stream() + let windowBits:Int32 = 15 + 16 + let status = deflateInit2_( + &stream, + level, + Z_DEFLATED, + windowBits, + memLevel, + strategy, + ZLIB_VERSION, + Int32(MemoryLayout.size) + ) + guard status == Z_OK else { return nil } + return span.withUnsafeBufferPointer { + return Deflate( + bufferSize: bufferSize, + level: level + ).compress( + baseAddress: $0.baseAddress, + count: $0.count, + configuration: .init(reserveCapacity: configuration.reserveCapacity), + stream: &stream + ) + } ?? nil + } +} \ No newline at end of file diff --git a/Sources/Zlib/gzip/Gzip+Compress.swift b/Sources/Zlib/gzip/Gzip+Compress.swift new file mode 100644 index 0000000..10c5a90 --- /dev/null +++ b/Sources/Zlib/gzip/Gzip+Compress.swift @@ -0,0 +1,53 @@ + +import SwiftCompressionUtilities +import ZlibShim + +extension Gzip: Compressor { + public typealias ConcreteCompressionConfiguration = CompressConfiguration + public typealias ConcreteCompressionResult = [UInt8]? + + public func compress( + data: some Collection, + configuration: ConcreteCompressionConfiguration = .default + ) -> ConcreteCompressionResult { + var stream = z_stream() + let windowBits:Int32 = 15 + 16 + let status = deflateInit2_( + &stream, + level, + Z_DEFLATED, + windowBits, + memLevel, + strategy, + ZLIB_VERSION, + Int32(MemoryLayout.size) + ) + guard status == Z_OK else { return nil } + return data.withContiguousStorageIfAvailable { + return Deflate( + bufferSize: bufferSize, + level: level + ).compress( + baseAddress: $0.baseAddress, + count: $0.count, + configuration: .init(reserveCapacity: configuration.reserveCapacity), + stream: &stream + ) + } ?? nil + } +} + +// MARK: Configuration +extension Gzip { + public struct CompressConfiguration: CompressionConfiguration { + public static var `default`: Self { .init() } + + public let reserveCapacity:Int + + public init( + reserveCapacity: Int = 1024 + ) { + self.reserveCapacity = reserveCapacity + } + } +} \ No newline at end of file diff --git a/Sources/Zlib/gzip/Gzip+Decompress+Span.swift b/Sources/Zlib/gzip/Gzip+Decompress+Span.swift new file mode 100644 index 0000000..0e009ec --- /dev/null +++ b/Sources/Zlib/gzip/Gzip+Decompress+Span.swift @@ -0,0 +1,30 @@ + +import ZlibShim + +extension Gzip { + public func decompress( + span: Span, + configuration: DecompressConfiguration = .default + ) -> ConcreteDecompressionResult { + var stream = z_stream() + let windowBits:Int32 = 15 + 16 + let status = inflateInit2_( + &stream, + windowBits, + ZLIB_VERSION, + Int32(MemoryLayout.size) + ) + guard status == Z_OK else { return nil } + return span.withUnsafeBufferPointer { + return Deflate( + bufferSize: bufferSize, + level: level + ).decompress( + baseAddress: $0.baseAddress, + count: $0.count, + configuration: .init(reserveCapacity: configuration.reserveCapacity), + stream: &stream + ) + } ?? nil + } +} \ No newline at end of file diff --git a/Sources/Zlib/gzip/Gzip+Decompress.swift b/Sources/Zlib/gzip/Gzip+Decompress.swift new file mode 100644 index 0000000..e4b808f --- /dev/null +++ b/Sources/Zlib/gzip/Gzip+Decompress.swift @@ -0,0 +1,49 @@ + +import SwiftCompressionUtilities +import ZlibShim + +extension Gzip: Decompressor { + public typealias ConcreteDecompressionConfiguration = DecompressConfiguration + public typealias ConcreteDecompressionResult = [UInt8]? + + public func decompress( + data: some Collection, + configuration: ConcreteDecompressionConfiguration = .default + ) -> ConcreteDecompressionResult { + var stream = z_stream() + let windowBits:Int32 = 15 + 16 + let status = inflateInit2_( + &stream, + windowBits, + ZLIB_VERSION, + Int32(MemoryLayout.size) + ) + guard status == Z_OK else { return nil } + return data.withContiguousStorageIfAvailable { + return Deflate( + bufferSize: bufferSize, + level: level + ).decompress( + baseAddress: $0.baseAddress, + count: $0.count, + configuration: .init(reserveCapacity: configuration.reserveCapacity), + stream: &stream + ) + } ?? nil + } +} + +// MARK: Configuration +extension Gzip { + public struct DecompressConfiguration: DecompressionConfiguration { + public static var `default`: Self { .init() } + + public let reserveCapacity:Int + + public init( + reserveCapacity: Int = 1024 + ) { + self.reserveCapacity = reserveCapacity + } + } +} \ No newline at end of file diff --git a/Sources/Zlib/gzip/Gzip.swift b/Sources/Zlib/gzip/Gzip.swift new file mode 100644 index 0000000..8598e52 --- /dev/null +++ b/Sources/Zlib/gzip/Gzip.swift @@ -0,0 +1,35 @@ + +import SwiftCompressionUtilities +import ZlibShim + +// https://www.rfc-editor.org/rfc/rfc1952 + +/// The Gzip compression technique. +/// +/// https://en.wikipedia.org/wiki/Gzip +public struct Gzip: Sendable { + public let bufferSize:Int + public let level:Int32 + public let memLevel:Int32 + public let strategy:Int32 + + public init( + bufferSize: Int = 1024, + level: Int32 = Z_DEFAULT_COMPRESSION, + memLevel: Int32 = 8, + strategy: Int32 = Z_DEFAULT_STRATEGY + ) { + self.bufferSize = bufferSize + self.level = level + self.memLevel = memLevel + self.strategy = strategy + } + + public var algorithm: CompressionAlgorithm { + .gzip(bufferSize: bufferSize, level: level, memLevel: memLevel, strategy: strategy) + } + + public var compressionQuality: CompressionQuality { + .lossless + } +} \ No newline at end of file diff --git a/Sources/ZlibShim/module.modulemap b/Sources/ZlibShim/module.modulemap new file mode 100644 index 0000000..4f8723d --- /dev/null +++ b/Sources/ZlibShim/module.modulemap @@ -0,0 +1,5 @@ +module ZlibShim [system] { + header "/usr/include/zlib.h" + link "z" + export * +} \ No newline at end of file diff --git a/Tests/BrotliTests/BrotliTests.swift b/Tests/BrotliTests/BrotliTests.swift new file mode 100644 index 0000000..f1b132c --- /dev/null +++ b/Tests/BrotliTests/BrotliTests.swift @@ -0,0 +1,24 @@ + +#if canImport(FoundationEssentials) + +import FoundationEssentials +import Testing +@testable import Brotli + +struct BrotliTests { + @Test + func brotliTest() { + let brotli = Brotli() + let bro = "What in tarnation fornication trepidation what what what what what the the the the" + let broData = bro.data(using: .utf8)! + let compressed = brotli.compress(data: [UInt8](broData), configuration: .default)! + guard var decompressed = brotli.decompress(data: compressed, configuration: .default) else { + #expect(Bool(false)) + return + } + decompressed.append(0) + #expect(String(cString: decompressed) == bro) + } +} + +#endif \ No newline at end of file diff --git a/Tests/CSSTests/CSSTests.swift b/Tests/CSSTests/CSSTests.swift deleted file mode 100644 index 63386f1..0000000 --- a/Tests/CSSTests/CSSTests.swift +++ /dev/null @@ -1,96 +0,0 @@ - -#if compiler(>=6.0) - -import Testing -@testable import CompressionCSS -@testable import SwiftCompressionUtilities - -// MARK: CSSTests -struct CSSTests { -} - -// MARK: CSSMinifyTests -struct CSSMinifyTests { - private func minify(_ string: String) -> String { - return String(decoding: CompressionTechnique.css.minify(data: [UInt8](string.utf8), reserveCapacity: 0), as: UTF8.self) - } - @Test func minifyCSSNative() { - #expect(minify(""" - * { - box-sizing: border-box; - } - """) == "*{box-sizing:border-box;}") - } - - @Test func minifyCSSCommaSeparatedInputs() { - #expect(minify(""" - input[type=email], input[type=password], input[type=text], input[type=number], input[type=date], input[type=time] { - border: 1px solid #ccc; - border-radius: 3px; - } - """) == "input[type=email],input[type=password],input[type=text],input[type=number],input[type=date],input[type=time]{border:1px solid #ccc;border-radius:3px;}") - } - - @Test func minifyCSSNestedClass() { - #expect(minify(""" - div#notification_area .notification_popup .notification_popup_content { - } - """) == "div#notification_area .notification_popup .notification_popup_content{}") - } - - @Test func minifyCSSComment() { - #expect(minify(""" - div#notification_area { - position: fixed; - overflow-x: scroll; - max-height: 100%; - bottom: 10%; - right: 5%; - width: 25%; - z-index: 96; /* 1 above the footer */ - } - """) == "div#notification_area{position:fixed;overflow-x:scroll;max-height:100%;bottom:10%;right:5%;width:25%;z-index:96;}") - } - - @Test func minifyCSSKeyframeSpin() { - #expect(minify(""" - @keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } - } - """) == "@keyframes spin{0%{transform:rotate(0deg);}100%{transform:rotate(360deg);}}") - } - - @Test func minifyCSSAnimation1() { - #expect(minify(""" - .fade_750ms { - animation: fadeEffect 0.75s; - } - """) == ".fade_750ms{animation:fadeEffect 0.75s;}") - } - - @Test func minifyCSSAnimation2() { - #expect(minify(""" - @keyframes slidein { - 0% { - translate: 0px 2.5vw; - opacity: 0; - } - 100% { - translate: 0px 0px; - opacity: 1; - } - } - """) == "@keyframes slidein{0%{translate:0px 2.5vw;opacity:0;}100%{translate:0px 0px;opacity:1;}}") - } - - @Test func minifyCSSCalc1() { - #expect(minify(""" - header { - height: calc(100vh - 40px); - } - """) == "header{height:calc(100vh - 40px);}") - } -} - -#endif \ No newline at end of file diff --git a/Tests/DNATests/DNABinaryEncodingTests.swift b/Tests/DNATests/DNABinaryEncodingTests.swift index bbfd678..0c56cae 100644 --- a/Tests/DNATests/DNABinaryEncodingTests.swift +++ b/Tests/DNATests/DNABinaryEncodingTests.swift @@ -6,16 +6,16 @@ import Testing @testable import SwiftCompressionUtilities struct DNABinaryEncodingTests { - static let sequence:String = "TACTTGCTAAAAGTACATTGCTAAGATACACCGGCA" - static let data:[UInt8] = [UInt8](sequence.utf8) - static let compressed:CompressionResult = try! CompressionTechnique.dnaBinaryEncoding().compress(data: data) + static let sequence = "TACTTGCTAAAAGTACATTGCTAAGATACACCGGCA" + static let data = [UInt8](sequence.utf8) + static let compressed = DNABinaryEncoding().compress(data: data, configuration: .init()) @Test func compressDNABinaryEncoding() { - #expect(Self.compressed.data == [199, 231, 0, 177, 62, 112, 140, 69, 164]) + #expect(Self.compressed == [199, 231, 0, 177, 62, 112, 140, 69, 164]) } @Test func decompressDNABinaryEncoding() throws(DecompressionError) { - let result:[UInt8] = try CompressionTechnique.dnaBinaryEncoding().decompress(data: Self.compressed.data) + let result = DNABinaryEncoding().decompress(data: Self.compressed, configuration: .init()) #expect(result == Self.data) } } diff --git a/Tests/DNATests/DNASingleBlockEncodingTestsTests.swift b/Tests/DNATests/DNASingleBlockEncodingTestsTests.swift index ae484e4..dd45699 100644 --- a/Tests/DNATests/DNASingleBlockEncodingTestsTests.swift +++ b/Tests/DNATests/DNASingleBlockEncodingTestsTests.swift @@ -12,9 +12,9 @@ import Testing @testable import SwiftCompressionUtilities struct DNASingleBlockEncodingTests { - static let sequence:String = "TACTTGNCTAAAAGTACNATTGNCTAAGANTACACCGGCA" - static let data:[UInt8] = [UInt8](sequence.utf8) - static let binary:[UInt8:[UInt8]] = CompressionTechnique.DNASingleBlockEncoding.compressBinary(data: data) + static let sequence = "TACTTGNCTAAAAGTACNATTGNCTAAGANTACACCGGCA" + static let data = [UInt8](sequence.utf8) + static let binary = DNASingleBlockEncoding().compress(data: data, configuration: .init()) @Test func compressDNACSingleBlockEncodingPhase1() { #expect(Self.binary == [ @@ -30,7 +30,7 @@ struct DNASingleBlockEncodingTests { } @Test func compressDNACSingleBlockEncodingPhase2() { - let (result, controlBits):([UInt8], [UInt8]) = CompressionTechnique.DNASingleBlockEncoding.compressSBE(binaryData: Self.binary[65]!.prefix(7)) + let (result, controlBits):([UInt8], [UInt8]) = DNASingleBlockEncoding.compressSBE(binaryData: Self.binary[65]!.prefix(7)) #expect(result == [0, 1, 0]) #expect(controlBits == [0]) } diff --git a/Tests/JavaScriptTests/JavaScriptTests.swift b/Tests/JavaScriptTests/JavaScriptTests.swift deleted file mode 100644 index 2ef461a..0000000 --- a/Tests/JavaScriptTests/JavaScriptTests.swift +++ /dev/null @@ -1,217 +0,0 @@ - -#if compiler(>=6.0) - -import Testing -@testable import CompressionJavaScript -@testable import SwiftCompressionUtilities - -// MARK: JavaScriptTests -struct JavaScriptTests { -} - -// MARK: JavaScriptMinifyTests -struct JavaScriptMinifyTests { - private func minify(_ string: String) -> String { - return String(decoding: CompressionTechnique.javascript.minify(data: [UInt8](string.utf8)), as: UTF8.self) - } - - @Test func minifyJSVar() { - #expect(minify(""" - var i = 0; - var j = "easy clap"; - var k = false; - """) == """ - var i=0;var j="easy clap";var k=false; - """) - } - - @Test func minifyJSConst() { - #expect(minify(""" - const i = 0; - const j = "easy clap"; - const k = false; - """) == """ - const i=0;const j="easy clap";const k=false; - """) - } - - @Test func minifyJSForNumber() { - #expect(minify(""" - for (var i = 0; i < starting_time_elements.length - 1; i++) { - } - """) == """ - for(var i=0;i
Name " + division_name + " | Day of Week " + day_of_week_html + "
") - """) == """ - divisions_list_html+=("
Name "+division_name+" | Day of Week "+day_of_week_html+"
") - """) - } - @Test func minifyJSStringToken2() { - #expect(minify(""" - function get_cookie(name) { - return document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)')?.pop() || ''; - } - """) == "function get_cookie(name){return document.cookie.match('(^|;)\\s*'+name+'\\s*=\\s*([^;]+)')?.pop()||'';}") - } - - @Test func minifyJSStringToken3() { - #expect(minify(""" - var string = ` - - - - League Schedule Diagnostics: %schedule_id% - - - - - - -

League Schedule Diagnostics: %schedule_id%

- `; - """) == """ - var string=` - - - - League Schedule Diagnostics: %schedule_id% - - - - - - -

League Schedule Diagnostics: %schedule_id%

- `; - """) - } - - @Test func minifyJSSwitchCaseString() { - #expect(minify(""" - switch (key) { - case "opponents": - break; - case "home_away": - break; - case "time_slots": - break; - case "matchup_slots": - break; - default: - break; - } - """) == """ - switch(key){case"opponents":break;case"home_away":break;case"time_slots":break;case"matchup_slots":break;default:break;} - """) - } - - @Test func minifyJSSwitchCaseNumber() { - #expect(minify(""" - switch (brackets.size) { - case 2: - bracket_names.push("Winners Bracket"); - bracket_names.push("Losers Bracket"); - break; - case 3: - bracket_names.push("Winners Bracket"); - bracket_names.push("Losers Bracket"); - bracket_names.push("Elimination Bracket"); - break; - default: - for (var i = 0; i < brackets.size; i++) { - bracket_names.push("Bracket " + (i + 1).toString()); - } - break; - } - """) == """ - switch(brackets.size){case 2:bracket_names.push("Winners Bracket");bracket_names.push("Losers Bracket");break;case 3:bracket_names.push("Winners Bracket");bracket_names.push("Losers Bracket");bracket_names.push("Elimination Bracket");break;default:for(var i=0;i=6.0) -import Testing @testable import CompressionLZ @testable import SwiftCompressionUtilities +import Testing struct LZ77Tests { static let string:String = "abracadabra abracadabra" - static let lz77:CompressionTechnique.LZ77 = CompressionTechnique.lz77(windowSize: 10, bufferSize: 6) - static let compressed:[UInt8] = try! lz77.compress(data: [UInt8](string.utf8)).data + static let lz77 = LZ77(windowSize: 10, bufferSize: 6) + static let compressed = lz77.compress(data: [UInt8](string.utf8), configuration: .init(reserveCapacity: 1024)) @Test func compressLZ77() { #expect(Self.compressed == [ @@ -22,7 +16,7 @@ struct LZ77Tests { ]) } @Test func decompressLZ77() throws(DecompressionError) { - let result:[UInt8] = try Self.lz77.decompress(data: Self.compressed) + let result = Self.lz77.decompress(data: Self.compressed, configuration: .init(reserveCapacity: 1024)) #expect(result == [UInt8](Self.string.utf8)) } } diff --git a/Tests/SnappyTests/SnappyTests.swift b/Tests/SnappyTests/SnappyTests.swift index 67e0fe1..9d86d12 100644 --- a/Tests/SnappyTests/SnappyTests.swift +++ b/Tests/SnappyTests/SnappyTests.swift @@ -2,23 +2,33 @@ #if compiler(>=6.0) import Testing -@testable import CompressionSnappy +@testable import Snappy @testable import SwiftCompressionUtilities struct SnappyTests { static let wikipedia:String = "Wikipedia is a free, web-based, collaborative, multilingual encyclopedia project." - static let wikipediaHexadecimalLiteral:String = "51f04257696b697065646961206973206120667265652c207765622d62617365642c20636f6c6c61626f7261746976652c206d756c74696c696e6775616c20656e6379636c6f093f1c70726f6a6563742e" + static let wikipediaHexadecimalLiteral:String = "51F05057696B697065646961206973206120667265652C207765622D62617365642C20636F6C6C61626F7261746976652C206D756C74696C696E6775616C20656E6379636C6F70656469612070726F6A6563742E" static let wikipediaHexadecimal:UnfoldSequence = wikipediaHexadecimalLiteral.hexadecimal static let wikipediaCompressedData:[UInt8] = .init(wikipediaHexadecimal) @Test func compressSnappy() throws(CompressionError) { - /*let compressed:[UInt8] = try Self.wikipedia.compressed(using: CompressionTechnique.snappy()).data - #expect(compressed == Self.wikipediaCompressedData)*/ + guard let compressed:[UInt8] = Snappy().compress(span: Self.wikipedia.utf8Span.span, configuration: .init()) else { + #expect(Bool(false)) + return + } + #expect(compressed == Self.wikipediaCompressedData) + + guard var decompressed = try? Snappy().decompress(data: compressed) else { + #expect(Bool(false)) + return + } + decompressed.append(0) + #expect(String(cString: decompressed) == Self.wikipedia) } @Test func decompressSnappyLength() throws(DecompressionError) { - let snappy = CompressionTechnique.snappy() + let snappy = Snappy() var data:[UInt8] = [254, 255, 127] var index = data.startIndex var length:UInt32 = try snappy.decompressLength(data: data, index: &index) @@ -40,7 +50,7 @@ struct SnappyTests { } @Test func decompressSnappy() throws(DecompressionError) { - let decompressed = try CompressionTechnique.snappy().decompress(data: Self.wikipediaCompressedData) + let decompressed = try Snappy().decompress(data: Self.wikipediaCompressedData) #expect(decompressed == [UInt8](Self.wikipedia.utf8)) } } diff --git a/Sources/SwiftCompressionUtilities/extensions/StringExtensions.swift b/Tests/SnappyTests/StringProtocol+Hexadecimal.swift similarity index 78% rename from Sources/SwiftCompressionUtilities/extensions/StringExtensions.swift rename to Tests/SnappyTests/StringProtocol+Hexadecimal.swift index f75d27a..a87fdd9 100644 --- a/Sources/SwiftCompressionUtilities/extensions/StringExtensions.swift +++ b/Tests/SnappyTests/StringProtocol+Hexadecimal.swift @@ -1,6 +1,6 @@ extension StringProtocol { - package var hexadecimal: UnfoldSequence { // https://stackoverflow.com/a/43360864 + var hexadecimal: UnfoldSequence { // https://stackoverflow.com/a/43360864 sequence(state: startIndex) { startIndex in guard startIndex < endIndex else { return nil } let endIndex = index(startIndex, offsetBy: 2, limitedBy: endIndex) ?? endIndex diff --git a/Tests/SwiftCompressionTests/DataBuilderTests.swift b/Tests/SwiftCompressionTests/ByteBuilderTests.swift similarity index 73% rename from Tests/SwiftCompressionTests/DataBuilderTests.swift rename to Tests/SwiftCompressionTests/ByteBuilderTests.swift index 746bbf8..0b3d3bf 100644 --- a/Tests/SwiftCompressionTests/DataBuilderTests.swift +++ b/Tests/SwiftCompressionTests/ByteBuilderTests.swift @@ -1,12 +1,13 @@ #if compiler(>=6.0) +import ByteBuilder import Testing @testable import SwiftCompressionUtilities -struct DataBuilderTests { - @Test func dataBuilder() { - var builder:CompressionTechnique.DataBuilder = .init() +struct BytesBuilderTests { + @Test func bytesBuilder() { + var builder = BytesBuilder() builder.data.reserveCapacity(100) builder.write(bits: [true, false, true, false]) builder.write(bits: [true, true, true, true, true]) diff --git a/Tests/SwiftCompressionTests/HexadecimalTests.swift b/Tests/SwiftCompressionTests/HexadecimalTests.swift deleted file mode 100644 index ff15476..0000000 --- a/Tests/SwiftCompressionTests/HexadecimalTests.swift +++ /dev/null @@ -1,19 +0,0 @@ - -#if compiler(>=6.0) - -import Testing -@testable import SwiftCompressionUtilities - -struct HexadecimalTests { - - #if canImport(Foundation) - @Test func hexadecimal() { - let string = "Wikipedia is a free, web-based, collaborative, multilingual encyclopedia project." - let hex = string.utf8.hexadecimal() - #expect(hex == "57696B697065646961206973206120667265652C207765622D62617365642C20636F6C6C61626F7261746976652C206D756C74696C696E6775616C20656E6379636C6F70656469612070726F6A6563742E") - } - #endif - -} - -#endif \ No newline at end of file diff --git a/Tests/SwiftCompressionTests/HuffmanTests.swift b/Tests/SwiftCompressionTests/HuffmanTests.swift index 052b0d8..2166340 100644 --- a/Tests/SwiftCompressionTests/HuffmanTests.swift +++ b/Tests/SwiftCompressionTests/HuffmanTests.swift @@ -1,13 +1,13 @@ #if compiler(>=6.0) +import Huffman import Testing -@testable import SwiftCompressionUtilities struct HuffmanTests { static let scoobyDooString = "ruh roh raggy!" static let scoobyDoo:[UInt8] = .init(scoobyDooString.utf8) - static let scoobyDooCompressed = CompressionTechnique.Huffman.compress(data: scoobyDoo)! + static let scoobyDooCompressed = Huffman().compress(data: scoobyDoo, configuration: .default)! @Test func compressHuffman() { let result = Self.scoobyDooCompressed @@ -17,7 +17,7 @@ struct HuffmanTests { @Test func decompressHuffman() { let result = Self.scoobyDooCompressed - let decompressed = CompressionTechnique.Huffman.decompress(data: result.data, root: result.rootNode) + let decompressed = Huffman().decompress(data: result.data, configuration: .init(root: result.rootNode)) #expect(result.validBitsInLastByte == 4) #expect(decompressed == Self.scoobyDoo) } @@ -25,7 +25,7 @@ struct HuffmanTests { @Test func decompressHuffmanOnlyFrequencyTable() { let result = Self.scoobyDooCompressed let table = Self.scoobyDooString.huffmanFrequencyTable() - let decompressed = CompressionTechnique.Huffman.decompress(data: result.data, frequencyTable: table) + let decompressed = Huffman().decompress(data: result.data, frequencyTable: table) #expect(decompressed == Self.scoobyDoo) } } diff --git a/Tests/SwiftCompressionTests/RunLengthEncodingTests.swift b/Tests/SwiftCompressionTests/RunLengthEncodingTests.swift index 7344f31..3acd9c0 100644 --- a/Tests/SwiftCompressionTests/RunLengthEncodingTests.swift +++ b/Tests/SwiftCompressionTests/RunLengthEncodingTests.swift @@ -2,37 +2,37 @@ #if compiler(>=6.0) import Testing -@testable import SwiftCompressionUtilities +@testable import SwiftCompression // MARK: Compress struct RunLengthEncodingTests { - @Test func compressRLE() throws(CompressionError) { + @Test func compressRLE() { var data:[UInt8] = .init("AAAAABBBBBCCCCC".utf8) - var compressed = try CompressionTechnique.RunLengthEncoding(minRun: 3, alwaysIncludeRunCount: true).compress(data: data).data + var compressed = RunLengthEncoding(minRun: 3, alwaysIncludeRunCount: true).compress(data: data, configuration: .init()) var expected_result:[UInt8] = [196, 65, 196, 66, 196, 67] #expect(compressed == expected_result) - compressed = try CompressionTechnique.RunLengthEncoding(minRun: 5, alwaysIncludeRunCount: true).compress(data: data).data + compressed = RunLengthEncoding(minRun: 5, alwaysIncludeRunCount: true).compress(data: data, configuration: .init()) #expect(compressed == expected_result) - compressed = try CompressionTechnique.RunLengthEncoding(minRun: 6, alwaysIncludeRunCount: true).compress(data: data).data + compressed = RunLengthEncoding(minRun: 6, alwaysIncludeRunCount: true).compress(data: data, configuration: .init()) #expect(compressed == expected_result) - compressed = try CompressionTechnique.RunLengthEncoding(minRun: 6, alwaysIncludeRunCount: false).compress(data: data).data + compressed = RunLengthEncoding(minRun: 6, alwaysIncludeRunCount: false).compress(data: data, configuration: .init()) expected_result = data #expect(compressed == expected_result) data = .init(String(repeating: "A", count: 66).utf8) - compressed = try CompressionTechnique.RunLengthEncoding(minRun: 3, alwaysIncludeRunCount: true).compress(data: data).data + compressed = RunLengthEncoding(minRun: 3, alwaysIncludeRunCount: true).compress(data: data, configuration: .init()) expected_result = [255, 65, 193, 65] #expect(compressed == expected_result) data = [190, 191, 192] - compressed = try CompressionTechnique.RunLengthEncoding(minRun: 3, alwaysIncludeRunCount: false).compress(data: data).data + compressed = RunLengthEncoding(minRun: 3, alwaysIncludeRunCount: false).compress(data: data, configuration: .init()) expected_result = [190, 191, 192, 192] #expect(compressed == expected_result) - compressed = try CompressionTechnique.RunLengthEncoding(minRun: 3, alwaysIncludeRunCount: true).compress(data: data).data + compressed = RunLengthEncoding(minRun: 3, alwaysIncludeRunCount: true).compress(data: data, configuration: .init()) expected_result = [192, 190, 192, 191, 192, 192] #expect(compressed == expected_result) } @@ -127,8 +127,8 @@ extension RunLengthEncodingTests { @Test func decompressRLE() throws { let string = "AAAAABBBBBCCCCC" let data:[UInt8] = .init(string.utf8) - let compressed = try CompressionTechnique.RunLengthEncoding(minRun: 3, alwaysIncludeRunCount: true).compress(data: data).data - let decompressed = try CompressionTechnique.RunLengthEncoding(minRun: 3, alwaysIncludeRunCount: true).decompress(data: compressed) + let compressed = RunLengthEncoding(minRun: 3, alwaysIncludeRunCount: true).compress(data: data, configuration: .init()) + let decompressed = RunLengthEncoding(minRun: 3, alwaysIncludeRunCount: true).decompress(data: compressed, configuration: .init()) #expect(decompressed == data) } } diff --git a/Tests/SwiftCompressionTests/SwiftCompressionTests.swift b/Tests/SwiftCompressionTests/SwiftCompressionTests.swift deleted file mode 100644 index 5998e0b..0000000 --- a/Tests/SwiftCompressionTests/SwiftCompressionTests.swift +++ /dev/null @@ -1,10 +0,0 @@ - -#if compiler(>=6.0) - -import Testing -@testable import SwiftCompression - -struct SwiftCompressionTests { -} - -#endif \ No newline at end of file diff --git a/Tests/SwiftCompressionTests/programmingLanguage/SwiftLangTests.swift b/Tests/SwiftCompressionTests/programmingLanguage/SwiftLangTests.swift deleted file mode 100644 index 8d3987a..0000000 --- a/Tests/SwiftCompressionTests/programmingLanguage/SwiftLangTests.swift +++ /dev/null @@ -1,34 +0,0 @@ - -#if compiler(>=6.0) - -import Testing -@testable import SwiftCompression -@testable import SwiftCompressionUtilities - -struct SwiftLangTests { - - @Test - func minifySwiftCode() throws { - let code = """ - // - // AnyCompressor.swift - // - // - // Created by Evan Anderson on 12/26/24. - // - - // MARK: AnyCompressor - public protocol AnyCompressor: Sendable { - /// Compression algorithm this compressor uses. - public var algorithm: CompressionAlgorithm { get } - - /// Quality of the compressed data. - var quality: CompressionQuality { get } - } - """ - let result = try CompressionTechnique.swift.minify(swiftSourceCode: code) - #expect(result == "public protocol AnyCompressor:Sendable{public var algorithm:CompressionAlgorithm{get};var quality:CompressionQuality{get}}") - } -} - -#endif \ No newline at end of file diff --git a/Tests/ZlibTests/DeflateTests.swift b/Tests/ZlibTests/DeflateTests.swift new file mode 100644 index 0000000..e448615 --- /dev/null +++ b/Tests/ZlibTests/DeflateTests.swift @@ -0,0 +1,21 @@ + +#if canImport(FoundationEssentials) + +import FoundationEssentials +import Testing +@testable import Zlib + +struct DeflateTests { + @Test + func deflateTest() { + let deflate = Deflate() + let bro = "What in tarnation fornication trepidation what what what what what the the the the" + let broData = bro.data(using: .utf8)! + var compressed = deflate.compress(data: [UInt8](broData), configuration: .init(reserveCapacity: 1024))! + var decompressed = deflate.decompress(data: compressed, configuration: .init(reserveCapacity: 1024))! + decompressed.append(0) + #expect(String(cString: decompressed) == bro) + } +} + +#endif \ No newline at end of file diff --git a/Tests/ZlibTests/GzipTests.swift b/Tests/ZlibTests/GzipTests.swift new file mode 100644 index 0000000..7314cbd --- /dev/null +++ b/Tests/ZlibTests/GzipTests.swift @@ -0,0 +1,21 @@ + +#if canImport(FoundationEssentials) + +import FoundationEssentials +import Testing +@testable import Zlib + +struct GzipTests { + @Test + func gzipTest() { + let gzip = Gzip() + let bro = "What in tarnation fornication trepidation what what what what what the the the the" + let broData = bro.data(using: .utf8)! + var compressed = gzip.compress(data: [UInt8](broData))! + var decompressed = gzip.decompress(data: compressed)! + decompressed.append(0) + #expect(String(cString: decompressed) == bro) + } +} + +#endif \ No newline at end of file