├── .github ├── FUNDING.yml └── workflows │ ├── ci.yml │ └── codeql.yml ├── Sources ├── system-zlib │ ├── anchor.c │ └── include │ │ └── module.modulemap └── Gzip │ └── Data+Gzip.swift ├── Tests └── GzipTests │ ├── test.txt.gz │ └── GzipTests.swift ├── .gitignore ├── Package.swift ├── Package@swift-5.10.swift ├── .swiftlint.yml ├── LICENSE ├── README.md ├── CHANGELOG.md └── CODE_OF_CONDUCT.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [1024jp] 2 | -------------------------------------------------------------------------------- /Sources/system-zlib/anchor.c: -------------------------------------------------------------------------------- 1 | // intentionally empty 2 | -------------------------------------------------------------------------------- /Tests/GzipTests/test.txt.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/threema-ch/GzipSwift/main/Tests/GzipTests/test.txt.gz -------------------------------------------------------------------------------- /Sources/system-zlib/include/module.modulemap: -------------------------------------------------------------------------------- 1 | module zlibLinux [system] { 2 | header "/usr/include/zlib.h" 3 | header "/usr/include/zconf.h" 4 | link "z" 5 | export * 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | 4 | # Xcode 5 | # 6 | build/ 7 | *.pbxuser 8 | !default.pbxuser 9 | *.mode1v3 10 | !default.mode1v3 11 | *.mode2v3 12 | !default.mode2v3 13 | *.perspectivev3 14 | !default.perspectivev3 15 | xcuserdata 16 | *.xccheckout 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | *.xcuserstate 22 | 23 | 24 | # SPM 25 | # 26 | .swiftpm 27 | .build 28 | Package.resolved 29 | Packages/ 30 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "GzipSwift", 7 | products: [ 8 | .library(name: "Gzip", targets: ["Gzip"]), 9 | ], 10 | targets: [ 11 | .target( 12 | name: "Gzip", 13 | dependencies: ["system-zlib"] 14 | ), 15 | .target( 16 | name: "system-zlib" 17 | ), 18 | .testTarget( 19 | name: "GzipTests", 20 | dependencies: ["Gzip"], 21 | resources: [.copy("test.txt.gz")] 22 | ), 23 | ], 24 | swiftLanguageVersions: [.v6] 25 | ) 26 | -------------------------------------------------------------------------------- /Package@swift-5.10.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.10 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "GzipSwift", 7 | products: [ 8 | .library(name: "Gzip", targets: ["Gzip"]), 9 | ], 10 | targets: [ 11 | .target( 12 | name: "Gzip", 13 | dependencies: ["system-zlib"], 14 | swiftSettings: [.enableExperimentalFeature("StrictConcurrency")] 15 | ), 16 | .target( 17 | name: "system-zlib" 18 | ), 19 | .testTarget( 20 | name: "GzipTests", 21 | dependencies: ["Gzip"], 22 | resources: [.copy("test.txt.gz")], 23 | swiftSettings: [.enableExperimentalFeature("StrictConcurrency")] 24 | ), 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | 2 | opt_in_rules: 3 | - attributes 4 | - closure_end_indentation 5 | - closure_spacing 6 | - conditional_returns_on_newline 7 | - empty_count 8 | - explicit_init 9 | - fatal_error_message 10 | - first_where 11 | - implicitly_unwrapped_optional 12 | - let_var_whitespace 13 | - multiline_parameters 14 | - nimble_operator 15 | - number_separator 16 | - object_literal 17 | - operator_usage_whitespace 18 | - private_outlet 19 | - redundant_nil_coalescing 20 | - strict_fileprivate 21 | - switch_case_on_newline 22 | - vertical_parameter_alignment_on_call 23 | 24 | trailing_comma: 25 | mandatory_comma: true 26 | 27 | trailing_whitespace: 28 | ignores_empty_lines: true 29 | 30 | vertical_whitespace: 31 | max_empty_lines: 2 32 | 33 | line_length: 200 34 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | macOS: 7 | name: Test on macOS 8 | runs-on: macOS-14 9 | env: 10 | DEVELOPER_DIR: /Applications/Xcode_16.0.app 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Show environments 14 | run: | 15 | swift --version 16 | xcodebuild -version 17 | - name: Lint Swift 18 | run: | 19 | if [[ ! -f "/usr/local/bin/swiftlint" ]]; then 20 | brew install swiftlint 21 | fi 22 | swiftlint 23 | - name: Build & test SwiftPM 24 | run: | 25 | swift build 26 | swift test --enable-swift-testing 27 | linux: 28 | runs-on: ubuntu-latest 29 | container: swift:latest 30 | steps: 31 | - uses: actions/checkout@v4 32 | - name: Unit Test 33 | run: swift test --enable-swift-testing 34 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | schedule: 9 | - cron: '25 8 * * 4' 10 | 11 | env: 12 | CODEQL_ENABLE_EXPERIMENTAL_FEATURES_SWIFT: true 13 | DEVELOPER_DIR: /Applications/Xcode_16.0.app 14 | 15 | jobs: 16 | analyze: 17 | name: Analyze 18 | runs-on: macos-14 19 | permissions: 20 | actions: read 21 | contents: read 22 | security-events: write 23 | 24 | strategy: 25 | fail-fast: false 26 | matrix: 27 | language: ['swift'] 28 | 29 | steps: 30 | - name: Checkout repository 31 | uses: actions/checkout@v4 32 | 33 | - name: Initialize CodeQL 34 | uses: github/codeql-action/init@v2 35 | with: 36 | languages: ${{ matrix.language }} 37 | 38 | - run: swift build 39 | if: matrix.language == 'swift' 40 | 41 | - name: Perform CodeQL Analysis 42 | uses: github/codeql-action/analyze@v2 43 | with: 44 | category: "/language:${{matrix.language}}" 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2024 1024jp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | GzipSwift 3 | ======================== 4 | 5 | [![platform](https://img.shields.io/badge/platform-macOS%20|%20iOS%20|%20watchOS%20|%20tvOS%20|%20Linux-blue.svg)]() 6 | [![CI Status](https://github.com/1024jp/GzipSwift/workflows/CI/badge.svg)](https://github.com/1024jp/GzipSwift/actions) 7 | [![SwiftPM-compatible](https://img.shields.io/badge/SwiftPM-✔-4BC51D.svg?style=flat)](https://swift.org/package-manager/) 8 | 9 | __GzipSwift__ is a framework with an extension of Data written in Swift. It enables compress/decompress gzip using zlib. 10 | 11 | GzipSwift requires no privacy manifests since it does not access to any privacy information. 12 | 13 | 14 | ## Usage 15 | 16 | ```swift 17 | import Gzip 18 | 19 | // gzip 20 | let compressedData: Data = try! data.gzipped() 21 | let optimizedData: Data = try! data.gzipped(level: .bestCompression) 22 | 23 | // gunzip 24 | let decompressedData: Data 25 | if data.isGzipped { 26 | decompressedData = try! data.gunzipped() 27 | } else { 28 | decompressedData = data 29 | } 30 | ``` 31 | 32 | 33 | ## Installation 34 | 35 | GzipSwift is SwiftPM-compatible. To install, add this package to your `Package.swift` or your Xcode project. 36 | 37 | ```swift 38 | dependencies: [ 39 | .package(name: "Gzip", url: "https://github.com/1024jp/GzipSwift", from: Version(6, 0, 0)), 40 | ], 41 | ``` 42 | 43 | #### For Linux 44 | 45 | 1. Install zlib if you haven't installed yet: 46 | 47 | ```bash 48 | $ apt-get install zlib-dev 49 | ``` 50 | 2. Add this package to your package.swift. 51 | 3. If Swift build failed with a linker error: 52 | * check if libz.so is in your /usr/local/lib 53 | * if no, reinstall zlib as step (1) 54 | * if yes, link the library manually by passing '-Xlinker -L/usr/local/lib' with `swift build` 55 | 56 | 57 | ## License 58 | 59 | © 2014-2024 1024jp 60 | 61 | GzipSwift is distributed under the terms of the __MIT License__. See [LICENSE](LICENSE) for details. 62 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | Change Log 3 | ========================== 4 | 5 | 6.2.0 6 | -------------------------- 7 | 8 | ### Changes 9 | 10 | - Support typed throw. 11 | 12 | 13 | 14 | 6.1.0 15 | -------------------------- 16 | 17 | ### Changes 18 | 19 | - Support Swift 6.0. 20 | 21 | 22 | 23 | 6.0.1 24 | -------------------------- 25 | 26 | ### Changes 27 | 28 | - Add `Sendable` to `GzipError` and `CompressionLevel`. 29 | 30 | 31 | 32 | 6.0.0 33 | -------------------------- 34 | 35 | ### New 36 | 37 | - Support decompression for combined compression. 38 | - Add `wBits` optional parameter to `gzipped(level:)` and `.gunzipped()` to support managing the size of the history buffer. 39 | 40 | 41 | ### Changes 42 | 43 | - Remove support for CocoaPods and Carthage. 44 | - Update minimum Swift version to 5.4. 45 | - Break immediately when inflate exactly reaches the end of the buffer. 46 | 47 | 48 | 49 | 5.2.0 50 | -------------------------- 51 | 52 | ### Changes 53 | 54 | - Enable “build library for distribution” flag. 55 | 56 | 57 | 58 | 5.1.1 59 | -------------------------- 60 | 61 | ### Fixes 62 | 63 | - Fix CocoaPods distribution. 64 | 65 | 66 | 67 | 5.1.0 68 | -------------------------- 69 | 70 | ### Fixes 71 | 72 | - Fix chunk size. 73 | 74 | 75 | 76 | 5.0.0 77 | -------------------------- 78 | 79 | ### Changes 80 | 81 | - Update for Swift 5.0 82 | 83 | 84 | 4.1.0 85 | -------------------------- 86 | 87 | ### Changes 88 | 89 | - Support Bitcode. 90 | - Update for Swift 4.2. 91 | - Conform GzipError to Equatable. 92 | 93 | 94 | 4.0.4 95 | -------------------------- 96 | 97 | ### Fixes 98 | 99 | - Fix CocoaPods distribution. 100 | 101 | 102 | 4.0.3 103 | -------------------------- 104 | 105 | ### Fixes 106 | 107 | - Fix running on Linux. 108 | - Fix a runtime crash with Xcode 9.1. 109 | 110 | 111 | 4.0.2 112 | -------------------------- 113 | 114 | ### Fixes 115 | 116 | - Fix disabling code coverage support. 117 | 118 | 119 | 4.0.1 120 | -------------------------- 121 | 122 | ### Changes 123 | 124 | - Disable code coverage test. 125 | 126 | 127 | 4.0.0 128 | -------------------------- 129 | 130 | No change since 4.0.0-beta.2. 131 | 132 | 133 | 134 | 4.0.0-beta.2 135 | -------------------------- 136 | 137 | ### Changes 138 | 139 | - Make `CompressionLevel` struct. 140 | - Make `GzipError` struct. 141 | 142 | 143 | 144 | 4.0.0-beta 145 | -------------------------- 146 | 147 | ### Changes 148 | 149 | - Update project for Xcode 9. 150 | 151 | 152 | 153 | 3.1.4 154 | -------------------------- 155 | 156 | ### Fixes 157 | 158 | - Fix a possible error on watchOS and tvOS. 159 | 160 | 161 | 162 | 3.1.3 163 | -------------------------- 164 | 165 | ### Changes 166 | 167 | - Update Xcode to 8.2. 168 | 169 | 170 | 171 | 3.1.2 172 | -------------------------- 173 | 174 | ### New 175 | 176 | - Support CocoaPods. 177 | 178 | 179 | 180 | 3.1.1 181 | -------------------------- 182 | 183 | ### Fixes 184 | 185 | - Fix the Swift Package Manager support. 186 | 187 | 188 | 189 | 3.1.0 190 | -------------------------- 191 | 192 | ### New 193 | 194 | - Support the Swift Package Manager. 195 | 196 | 197 | 198 | 3.0.1 199 | -------------------------- 200 | 201 | ### Changes 202 | 203 | - Update Swift to 3.0.1. 204 | 205 | 206 | 207 | 3.0.0 208 | -------------------------- 209 | 210 | ### Changes 211 | 212 | - Migrate code to Swift 3.0 213 | - Become framework. 214 | - Support watchOS and tvOS 215 | - Rename from "NSData+GZIP" to "Data+Gzip" 216 | - Add `level` option to `gzipped()` method. 217 | - Add `isGzipped` property (readonly). 218 | 219 | 220 | 221 | 2.0.0 222 | -------------------------- 223 | 224 | ### Changes 225 | 226 | - Migrate code to Swift 2.0 227 | - Change to throw error instead returning `nil`. 228 | - Use Modulemap for zlib instead of `Bridging-Header.h` (see README for how to install). 229 | 230 | 231 | 232 | 1.1.0 233 | -------------------------- 234 | 235 | ### Changes 236 | 237 | - Change to return just an empty NSData instead of nil if given data is empty 238 | - Log error message if compression/decompression is failed. 239 | - [Note] This is a temporaly improvement. 240 | I'll migrate functions to throw NSError when Swift 2.0 becomes stable. -> Done. 241 | -------------------------------------------------------------------------------- /Tests/GzipTests/GzipTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GzipTests.swift 3 | // GzipTests 4 | // 5 | // Created by 1024jp on 2015-05-11. 6 | 7 | /* 8 | The MIT License (MIT) 9 | 10 | © 2015-2024 1024jp 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining a copy 13 | of this software and associated documentation files (the "Software"), to deal 14 | in the Software without restriction, including without limitation the rights 15 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | copies of the Software, and to permit persons to whom the Software is 17 | furnished to do so, subject to the following conditions: 18 | 19 | The above copyright notice and this permission notice shall be included in 20 | all copies or substantial portions of the Software. 21 | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 28 | THE SOFTWARE. 29 | */ 30 | 31 | import Foundation 32 | import Testing 33 | import Gzip 34 | 35 | struct GzipTests { 36 | 37 | @Test 38 | func testGZip() throws { 39 | 40 | for _ in 0..<10 { 41 | let testSentence = String.lorem(length: Int.random(in: 1..<100_000)) 42 | 43 | let data = Data(testSentence.utf8) 44 | let gzipped = try data.gzipped() 45 | let uncompressed = try gzipped.gunzipped() 46 | let uncompressedSentence = String(data: uncompressed, encoding: .utf8) 47 | 48 | #expect(gzipped != data) 49 | #expect(uncompressedSentence == testSentence) 50 | 51 | #expect(gzipped.isGzipped) 52 | #expect(!data.isGzipped) 53 | #expect(!uncompressed.isGzipped) 54 | } 55 | } 56 | 57 | 58 | @Test 59 | func testZeroLength() throws { 60 | 61 | let zeroLengthData = Data() 62 | 63 | #expect(try zeroLengthData.gzipped() == zeroLengthData) 64 | #expect(try zeroLengthData.gunzipped() == zeroLengthData) 65 | #expect(!zeroLengthData.isGzipped) 66 | } 67 | 68 | 69 | @Test 70 | func testWrongUngzip() { 71 | 72 | // data not compressed 73 | let data = Data("testString".utf8) 74 | 75 | #expect { 76 | try data.gunzipped() 77 | } throws: { error in 78 | let gzipError = try #require(error as? GzipError) 79 | 80 | return (gzipError.kind == .data) && 81 | (gzipError.message == "incorrect header check") && 82 | (gzipError.message == gzipError.localizedDescription) 83 | } 84 | } 85 | 86 | 87 | @Test 88 | func testCompressionLevel() throws { 89 | 90 | let data = Data(String.lorem(length: 100_000).utf8) 91 | let bestSpeedData = try data.gzipped(level: .bestSpeed) 92 | let bestCompressionData = try data.gzipped(level: .bestCompression) 93 | 94 | #expect(bestSpeedData.count > bestCompressionData.count) 95 | } 96 | 97 | 98 | @Test 99 | func testFileDecompression() throws { 100 | 101 | let url = try #require(Bundle.module.url(forResource: "test.txt.gz", withExtension: nil)) 102 | let data = try Data(contentsOf: url) 103 | let uncompressed = try data.gunzipped() 104 | 105 | #expect(data.isGzipped) 106 | #expect(String(data: uncompressed, encoding: .utf8) == "test") 107 | } 108 | 109 | 110 | @Test 111 | func testDecompressionWithNoHeaderAndTrailer() throws { 112 | 113 | let encoded = """ 114 | 7ZOxCsIwEIbf5ea0JNerqdmdFeygFYciHYK0lTZOIe9u9AXMTTpkOQ\ 115 | h8hLv/7vNwmFfr7DyBuXho7Tisrh8fYAAlYiF1oWSr0EgyhCWRrpsa\ 116 | OxCwm9xihxWMB/UuR9e7Z3zCfmqX/naPyAmMFHD+1C7WIKBKRykdrd\ 117 | PRTTqqJINlZKAYkylOv006i4zZEBksY8HIyKFi5EuMf0kzroxzZowc\ 118 | dHIPIYjvjjbRUSTKjmZHs6N/6WhVStS01VnRrGhW9BeKXsML 119 | """ 120 | let data = try #require(Data(base64Encoded: encoded)) 121 | let uncompressed = try data.gunzipped(wBits: -Gzip.maxWindowBits) 122 | let json = try #require(String(data: uncompressed, encoding: .utf8)) 123 | 124 | #expect(json.first == "{") 125 | #expect(json.last == "}") 126 | } 127 | 128 | 129 | @Test 130 | func testDecompressionCompositedCompression() throws { 131 | 132 | let firstData = try Data("test".utf8).gzipped() 133 | let secondData = try Data("string".utf8).gzipped() 134 | 135 | let data = firstData + secondData 136 | 137 | let uncompressed = try data.gunzipped() 138 | 139 | #expect(data.isGzipped) 140 | #expect(String(data: uncompressed, encoding: .utf8) == "teststring") 141 | } 142 | } 143 | 144 | 145 | private extension String { 146 | 147 | /// Generate random letters string for test. 148 | static func lorem(length: Int) -> String { 149 | 150 | let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 " 151 | let characters = (0.. 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in 18 | all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | THE SOFTWARE. 27 | */ 28 | 29 | import struct Foundation.Data 30 | 31 | #if os(Linux) 32 | import zlibLinux 33 | #else 34 | import zlib 35 | #endif 36 | 37 | public enum Gzip { 38 | 39 | /// Maximum value for windowBits (`MAX_WBITS`) 40 | public static let maxWindowBits = MAX_WBITS 41 | } 42 | 43 | 44 | /// Compression level whose rawValue is based on the zlib's constants. 45 | public struct CompressionLevel: RawRepresentable, Sendable { 46 | 47 | /// Compression level in the range of `0` (no compression) to `9` (maximum compression). 48 | public let rawValue: Int32 49 | 50 | public static let noCompression = Self(Z_NO_COMPRESSION) 51 | public static let bestSpeed = Self(Z_BEST_SPEED) 52 | public static let bestCompression = Self(Z_BEST_COMPRESSION) 53 | 54 | public static let defaultCompression = Self(Z_DEFAULT_COMPRESSION) 55 | 56 | 57 | public init(rawValue: Int32) { 58 | 59 | self.rawValue = rawValue 60 | } 61 | 62 | 63 | public init(_ rawValue: Int32) { 64 | 65 | self.rawValue = rawValue 66 | } 67 | } 68 | 69 | 70 | /// Errors on gzipping/gunzipping based on the zlib error codes. 71 | public struct GzipError: Swift.Error, Sendable { 72 | // cf. http://www.zlib.net/manual.html 73 | 74 | public enum Kind: Equatable, Sendable { 75 | /// The stream structure was inconsistent. 76 | /// 77 | /// - underlying zlib error: `Z_STREAM_ERROR` (-2) 78 | case stream 79 | 80 | /// The input data was corrupted 81 | /// (input stream not conforming to the zlib format or incorrect check value). 82 | /// 83 | /// - underlying zlib error: `Z_DATA_ERROR` (-3) 84 | case data 85 | 86 | /// There was not enough memory. 87 | /// 88 | /// - underlying zlib error: `Z_MEM_ERROR` (-4) 89 | case memory 90 | 91 | /// No progress is possible or there was not enough room in the output buffer. 92 | /// 93 | /// - underlying zlib error: `Z_BUF_ERROR` (-5) 94 | case buffer 95 | 96 | /// The zlib library version is incompatible with the version assumed by the caller. 97 | /// 98 | /// - underlying zlib error: `Z_VERSION_ERROR` (-6) 99 | case version 100 | 101 | /// An unknown error occurred. 102 | /// 103 | /// - parameter code: return error by zlib 104 | case unknown(code: Int) 105 | } 106 | 107 | /// Error kind. 108 | public let kind: Kind 109 | 110 | /// Returned message by zlib. 111 | public let message: String 112 | 113 | 114 | internal init(code: Int32, msg: UnsafePointer?) { 115 | 116 | self.message = msg.flatMap(String.init(validatingUTF8:)) ?? "Unknown gzip error" 117 | self.kind = Kind(code: code) 118 | } 119 | 120 | 121 | public var localizedDescription: String { 122 | 123 | return self.message 124 | } 125 | } 126 | 127 | 128 | private extension GzipError.Kind { 129 | 130 | init(code: Int32) { 131 | 132 | switch code { 133 | case Z_STREAM_ERROR: 134 | self = .stream 135 | case Z_DATA_ERROR: 136 | self = .data 137 | case Z_MEM_ERROR: 138 | self = .memory 139 | case Z_BUF_ERROR: 140 | self = .buffer 141 | case Z_VERSION_ERROR: 142 | self = .version 143 | default: 144 | self = .unknown(code: Int(code)) 145 | } 146 | } 147 | } 148 | 149 | 150 | extension Data { 151 | 152 | /// Whether the receiver is compressed in gzip format. 153 | public var isGzipped: Bool { 154 | 155 | return self.starts(with: [0x1f, 0x8b]) // check magic number 156 | } 157 | 158 | 159 | /// Create a new `Data` instance by compressing the receiver using zlib. 160 | /// Throws an error if compression failed. 161 | /// 162 | /// The `wBits` parameter allows for managing the size of the history buffer. The possible values are: 163 | /// 164 | /// Value Window size logarithm Input 165 | /// +9 to +15 Base 2 Includes zlib header and trailer 166 | /// -9 to -15 Absolute value of wbits No header and trailer 167 | /// +25 to +31 Low 4 bits of the value Includes gzip header and trailing checksum 168 | /// 169 | /// - Parameter level: Compression level. 170 | /// - Parameter wBits: Manage the size of the history buffer. 171 | /// - Returns: Gzip-compressed `Data` instance. 172 | /// - Throws: `GzipError` 173 | public func gzipped(level: CompressionLevel = .defaultCompression, wBits: Int32 = Gzip.maxWindowBits + 16) throws(GzipError) -> Data { 174 | 175 | guard !self.isEmpty else { 176 | return Data() 177 | } 178 | 179 | var stream = z_stream() 180 | var status: Int32 181 | 182 | status = deflateInit2_(&stream, level.rawValue, Z_DEFLATED, wBits, MAX_MEM_LEVEL, Z_DEFAULT_STRATEGY, ZLIB_VERSION, Int32(DataSize.stream)) 183 | 184 | guard status == Z_OK else { 185 | // deflateInit2 returns: 186 | // Z_VERSION_ERROR The zlib library version is incompatible with the version assumed by the caller. 187 | // Z_MEM_ERROR There was not enough memory. 188 | // Z_STREAM_ERROR A parameter is invalid. 189 | 190 | throw GzipError(code: status, msg: stream.msg) 191 | } 192 | 193 | var data = Data(capacity: DataSize.chunk) 194 | repeat { 195 | if Int(stream.total_out) >= data.count { 196 | data.count += DataSize.chunk 197 | } 198 | 199 | let inputCount = self.count 200 | let outputCount = data.count 201 | 202 | self.withUnsafeBytes { (inputPointer: UnsafeRawBufferPointer) in 203 | stream.next_in = UnsafeMutablePointer(mutating: inputPointer.bindMemory(to: Bytef.self).baseAddress!).advanced(by: Int(stream.total_in)) 204 | stream.avail_in = uInt(inputCount) - uInt(stream.total_in) 205 | 206 | data.withUnsafeMutableBytes { (outputPointer: UnsafeMutableRawBufferPointer) in 207 | stream.next_out = outputPointer.bindMemory(to: Bytef.self).baseAddress!.advanced(by: Int(stream.total_out)) 208 | stream.avail_out = uInt(outputCount) - uInt(stream.total_out) 209 | 210 | status = deflate(&stream, Z_FINISH) 211 | 212 | stream.next_out = nil 213 | } 214 | 215 | stream.next_in = nil 216 | } 217 | 218 | } while stream.avail_out == 0 && status != Z_STREAM_END 219 | 220 | guard deflateEnd(&stream) == Z_OK, status == Z_STREAM_END else { 221 | throw GzipError(code: status, msg: stream.msg) 222 | } 223 | 224 | data.count = Int(stream.total_out) 225 | 226 | return data 227 | } 228 | 229 | 230 | /// Create a new `Data` instance by decompressing the receiver using zlib. 231 | /// Throws an error if decompression failed. 232 | /// 233 | /// The `wBits` parameter allows for managing the size of the history buffer. The possible values are: 234 | /// 235 | /// Value Window size logarithm Input 236 | /// +8 to +15 Base 2 Includes zlib header and trailer 237 | /// -8 to -15 Absolute value of wbits Raw stream with no header and trailer 238 | /// +24 to +31 = 16 + (8 to 15) Low 4 bits of the value Includes gzip header and trailer 239 | /// +40 to +47 = 32 + (8 to 15) Low 4 bits of the value zlib or gzip format 240 | /// 241 | /// - Parameter wBits: Manage the size of the history buffer. 242 | /// - Returns: Gzip-decompressed `Data` instance. 243 | /// - Throws: `GzipError` 244 | public func gunzipped(wBits: Int32 = Gzip.maxWindowBits + 32) throws(GzipError) -> Data { 245 | 246 | guard !self.isEmpty else { 247 | return Data() 248 | } 249 | 250 | var data = Data(capacity: self.count * 2) 251 | var totalIn: uLong = 0 252 | var totalOut: uLong = 0 253 | 254 | repeat { 255 | var stream = z_stream() 256 | var status: Int32 257 | 258 | status = inflateInit2_(&stream, wBits, ZLIB_VERSION, Int32(DataSize.stream)) 259 | 260 | guard status == Z_OK else { 261 | // inflateInit2 returns: 262 | // Z_VERSION_ERROR The zlib library version is incompatible with the version assumed by the caller. 263 | // Z_MEM_ERROR There was not enough memory. 264 | // Z_STREAM_ERROR A parameters are invalid. 265 | 266 | throw GzipError(code: status, msg: stream.msg) 267 | } 268 | 269 | repeat { 270 | if Int(totalOut + stream.total_out) >= data.count { 271 | data.count += self.count / 2 272 | } 273 | 274 | let inputCount = self.count 275 | let outputCount = data.count 276 | 277 | self.withUnsafeBytes { (inputPointer: UnsafeRawBufferPointer) in 278 | let inputStartPosition = totalIn + stream.total_in 279 | stream.next_in = UnsafeMutablePointer(mutating: inputPointer.bindMemory(to: Bytef.self).baseAddress!).advanced(by: Int(inputStartPosition)) 280 | stream.avail_in = uInt(inputCount) - uInt(inputStartPosition) 281 | 282 | data.withUnsafeMutableBytes { (outputPointer: UnsafeMutableRawBufferPointer) in 283 | let outputStartPosition = totalOut + stream.total_out 284 | stream.next_out = outputPointer.bindMemory(to: Bytef.self).baseAddress!.advanced(by: Int(outputStartPosition)) 285 | stream.avail_out = uInt(outputCount) - uInt(outputStartPosition) 286 | 287 | status = inflate(&stream, Z_SYNC_FLUSH) 288 | 289 | stream.next_out = nil 290 | } 291 | 292 | stream.next_in = nil 293 | } 294 | } while (status == Z_OK) 295 | 296 | totalIn += stream.total_in 297 | 298 | guard inflateEnd(&stream) == Z_OK, status == Z_STREAM_END else { 299 | // inflate returns: 300 | // Z_DATA_ERROR The input data was corrupted (input stream not conforming to the zlib format or incorrect check value). 301 | // Z_STREAM_ERROR The stream structure was inconsistent (for example if next_in or next_out was NULL). 302 | // Z_MEM_ERROR There was not enough memory. 303 | // Z_BUF_ERROR No progress is possible or there was not enough room in the output buffer when Z_FINISH is used. 304 | throw GzipError(code: status, msg: stream.msg) 305 | } 306 | 307 | totalOut += stream.total_out 308 | 309 | } while (totalIn < self.count) 310 | 311 | data.count = Int(totalOut) 312 | 313 | return data 314 | } 315 | } 316 | 317 | 318 | private enum DataSize { 319 | 320 | static let chunk = 1 << 14 321 | static let stream = MemoryLayout.size 322 | } 323 | --------------------------------------------------------------------------------