Skip to content

Commit bff59ff

Browse files
committed
Initial implementation
1 parent 7a9fae7 commit bff59ff

31 files changed

+1148
-0
lines changed

.github/workflows/ci.yml

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
branches:
9+
- '*'
10+
workflow_dispatch:
11+
12+
concurrency:
13+
group: ci-${{ github.ref }}
14+
cancel-in-progress: true
15+
16+
jobs:
17+
lint:
18+
name: Lint
19+
runs-on: macos-15
20+
steps:
21+
- uses: actions/checkout@v4
22+
23+
- name: Install tools
24+
run: brew install swiftlint
25+
26+
- name: Run SwiftLint
27+
run: swiftlint lint --strict
28+
29+
spell:
30+
name: Spell
31+
runs-on: macos-15
32+
steps:
33+
- name: Checkout
34+
uses: actions/checkout@v4
35+
36+
- name: Install tools
37+
run: npm install -g git+https://github.com/streetsidesoftware/cspell-cli
38+
39+
- name: Run CSpell
40+
run: cspell-cli lint --no-progress
41+
42+
build:
43+
name: Build
44+
runs-on: macos-15
45+
strategy:
46+
matrix:
47+
config: ['debug', 'release']
48+
steps:
49+
- uses: actions/checkout@v4
50+
51+
- uses: maxim-lobanov/setup-xcode@v1
52+
with:
53+
xcode-version: latest-stable
54+
55+
- name: Build in ${{ matrix.config }}
56+
run: swift build -c ${{ matrix.config }}
57+
58+
test:
59+
name: Test
60+
runs-on: macos-15
61+
steps:
62+
- uses: actions/checkout@v4
63+
64+
- uses: maxim-lobanov/setup-xcode@v1
65+
with:
66+
xcode-version: latest-stable
67+
68+
- name: Run tests
69+
run: swift test

.gitignore

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Xcode
2+
#
3+
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4+
5+
## User settings
6+
xcuserdata/
7+
8+
## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
9+
*.xcscmblueprint
10+
*.xccheckout
11+
12+
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
13+
build/
14+
DerivedData/
15+
*.moved-aside
16+
*.pbxuser
17+
!default.pbxuser
18+
*.mode1v3
19+
!default.mode1v3
20+
*.mode2v3
21+
!default.mode2v3
22+
*.perspectivev3
23+
!default.perspectivev3
24+
25+
## Obj-C/Swift specific
26+
*.hmap
27+
28+
## App packaging
29+
*.ipa
30+
*.dSYM.zip
31+
*.dSYM
32+
33+
## Playgrounds
34+
timeline.xctimeline
35+
playground.xcworkspace
36+
37+
# Swift Package Manager
38+
#
39+
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40+
# Package.pins
41+
# Package.resolved
42+
# *.xcodeproj
43+
#
44+
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
45+
# hence it is not needed unless you have added a package configuration file to your project
46+
.swiftpm/
47+
.build/
48+
Packages/
49+
50+
# CocoaPods
51+
#
52+
# We recommend against adding the Pods directory to your .gitignore. However
53+
# you should judge for yourself, the pros and cons are mentioned at:
54+
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
55+
#
56+
# Pods/
57+
#
58+
# Add this line if you want to avoid checking in source code from the Xcode workspace
59+
# *.xcworkspace
60+
61+
# Carthage
62+
#
63+
# Add this line if you want to avoid checking in source code from Carthage dependencies.
64+
# Carthage/Checkouts
65+
66+
Carthage/Build/
67+
68+
# Accio dependency management
69+
.accio/
70+
71+
# fastlane
72+
#
73+
# It is recommended to not store the screenshots in the git repo.
74+
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
75+
# For more information about the recommended setup visit:
76+
# https://docs.fastlane.tools/best-practices/source-control/#source-control
77+
78+
fastlane/report.xml
79+
fastlane/Preview.html
80+
fastlane/screenshots/**/*.png
81+
fastlane/test_output
82+
83+
# Code Injection
84+
#
85+
# After new code Injection tools there's a generated folder /iOSInjectionProject
86+
# https://github.com/johnno1962/injectionforxcode
87+
88+
iOSInjectionProject/
89+
90+
.DS_Store

.swiftlint.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
included:
2+
- Sources
3+
- Tests
4+
- Package.swift
5+
excluded:
6+
- .build
7+
function_body_length:
8+
- 100
9+
- 200
10+
type_body_length:
11+
- 1000
12+
- 2000
13+
file_length:
14+
warning: 1000
15+
error: 2000
16+
large_tuple:
17+
warning: 5
18+
error: 10
19+
nesting:
20+
type_level:
21+
warning: 3
22+
function_level:
23+
warning: 3

Package.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// swift-tools-version: 6.0
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "swift-gzip",
7+
platforms: [
8+
.iOS(.v15),
9+
.tvOS(.v15),
10+
.visionOS(.v1),
11+
.macOS(.v12),
12+
.macCatalyst(.v15),
13+
.watchOS(.v8)
14+
],
15+
products: [
16+
.library(name: "SwiftGzip", targets: ["SwiftGzip"])
17+
],
18+
targets: [
19+
.target(
20+
name: "SwiftGzip",
21+
resources: [.copy("Resources/PrivacyInfo.xcprivacy")],
22+
swiftSettings: [.enableExperimentalFeature("StrictConcurrency")],
23+
linkerSettings: [.linkedLibrary("z")]
24+
),
25+
.testTarget(
26+
name: "SwiftGzipTests",
27+
dependencies: [.target(name: "SwiftGzip")],
28+
resources: [
29+
.copy("Resources/test.png"),
30+
.copy("Resources/test.png.gz")
31+
]
32+
)
33+
],
34+
swiftLanguageModes: [.v6]
35+
)

README.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# SwiftGzip
2+
3+
`SwiftGzip` is a framework that enables compressing / decompressing data, files and streams using `zlib`.
4+
5+
The framework uses `InputStream` and `OutputStream` for compression / decompression without the need to copy the whole data in memory. At most, the framework will allocate only 2 buffers of `256 kb` each for processing.
6+
7+
[![CI](https://github.com/mihai8804858/swift-gzip/actions/workflows/ci.yml/badge.svg)](https://github.com/mihai8804858/swift-gzip/actions/workflows/ci.yml)
8+
9+
## Installation
10+
11+
You can add `swift-gzip` to an Xcode project by adding it to your project as a package.
12+
13+
> https://github.com/mihai8804858/swift-gzip
14+
15+
If you want to use `swift-gzip` in a [SwiftPM](https://swift.org/package-manager/) project, it's as simple as adding it to your `Package.swift`:
16+
17+
``` swift
18+
dependencies: [
19+
.package(url: "https://github.com/mihai8804858/swift-gzip", branch: "main")
20+
]
21+
```
22+
23+
And then adding the product to any target that needs access to the library:
24+
25+
```swift
26+
.product(name: "SwiftGzip", package: "swift-gzip")
27+
```
28+
29+
## Quick Start
30+
31+
Just import `SwiftGzip` in your project to access the API:
32+
33+
```swift
34+
import SwiftGzip
35+
```
36+
37+
* `isGzipped`
38+
39+
Verify if given `Data`, `[UInt8]` or `URL` is compressed in gzip format:
40+
```swift
41+
[UInt8](...).isGzipped
42+
Data(...).isGzipped
43+
URL(...).isGzipped
44+
```
45+
46+
* `zip`
47+
48+
```swift
49+
let compressor = GzipCompressor(level: .bestCompression)
50+
51+
// Compress `Data`
52+
let zipped = try await compressor.zip(data: data)
53+
54+
// Compress `[UInt8]`
55+
let zipped = try await compressor.zip(bytes: bytes)
56+
57+
// Compress file
58+
let inputURL = URL(...)
59+
let outputURL = URL(...)
60+
try await compressor.zip(inputURL: inputURL, outputURL: outputURL)
61+
62+
// Compress data stream
63+
let inputStream = InputStream(...)
64+
let outputStream = OutputStream(...)
65+
try await compressor.zip(inputStream: inputStream, outputStream: outputStream)
66+
```
67+
68+
* `unzip`
69+
70+
```swift
71+
let decompressor = GzipDecompressor()
72+
73+
// Decompress `Data`
74+
let unzipped = try await compressor.unzip(data: data)
75+
76+
// Decompress `[UInt8]`
77+
let zipped = try await compressor.unzip(bytes: bytes)
78+
79+
// Decompress file
80+
let inputURL = URL(...)
81+
let outputURL = URL(...)
82+
try await compressor.unzip(inputURL: inputURL, outputURL: outputURL)
83+
84+
// Decompress data stream
85+
let inputStream = InputStream(...)
86+
let outputStream = OutputStream(...)
87+
try await compressor.unzip(inputStream: inputStream, outputStream: outputStream)
88+
```
89+
90+
## License
91+
92+
This library is released under the MIT license. See [LICENSE](LICENSE) for details.

Sources/Extensions/Data.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import Foundation
2+
3+
extension Data {
4+
/// Whether the data is compressed in gzip format.
5+
public var isGzipped: Bool {
6+
starts(with: GzipConstants.magicNumber)
7+
}
8+
}

Sources/Extensions/UInt8.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
extension [UInt8] {
2+
/// Whether the bytes are compressed in gzip format.
3+
public var isGzipped: Bool {
4+
starts(with: GzipConstants.magicNumber)
5+
}
6+
}

Sources/Extensions/URL.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import Foundation
2+
3+
extension URL {
4+
/// Whether the file at this `URL` is compressed in gzip format.
5+
public var isGzipped: Bool {
6+
guard let stream = InputStream(url: self) else { return false }
7+
stream.open()
8+
defer { stream.close() }
9+
let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: GzipConstants.magicNumber.count)
10+
defer { buffer.deallocate() }
11+
let read = stream.read(buffer, maxLength: GzipConstants.magicNumber.count)
12+
if read < 0 {
13+
debugPrint(stream.streamError.map { "\($0)" } ?? "Failure reading input stream")
14+
return false
15+
} else {
16+
return Data(bytes: buffer, count: read).isGzipped
17+
}
18+
}
19+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import Foundation
2+
import zlib
3+
4+
extension GzipCompressor {
5+
/// Asynchronously compress `data` using `zlib`.
6+
///
7+
/// - Parameter data: Data to compress.
8+
///
9+
/// - Returns: Gzip-compressed `Data` instance.
10+
/// - Throws: `GzipError`
11+
public func zip(data: Data) async throws -> Data {
12+
try await withCheckedThrowingContinuation { continuation in
13+
do {
14+
let data = try zip(data: data)
15+
continuation.resume(returning: data)
16+
} catch {
17+
continuation.resume(throwing: error)
18+
}
19+
}
20+
}
21+
22+
/// Compress `data` using `zlib`.
23+
///
24+
/// - Parameter data: Data to compress.
25+
///
26+
/// - Returns: Gzip-compressed `Data` instance.
27+
/// - Throws: `GzipError`
28+
public func zip(data: Data) throws -> Data {
29+
let input = InputStream(data: data)
30+
let output = OutputStream(toMemory: ())
31+
try zip(inputStream: input, outputStream: output)
32+
guard let outputData = output.property(forKey: .dataWrittenToMemoryStreamKey) as? Data else {
33+
throw GzipError(kind: .stream, message: "Cannot read data from output stream")
34+
}
35+
36+
return outputData
37+
}
38+
}

0 commit comments

Comments
 (0)