├── .gitignore
├── .gitmodules
├── .jazzy.yaml
├── CHANGELOG.md
├── Cartfile
├── LICENSE
├── Package.swift
├── README.md
├── SWCompression.podspec
├── SWCompression.xcodeproj
├── SWCompression.plist
├── SWCompression.xctestplan
├── TestSWCompression.plist
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── WorkspaceSettings.xcsettings
└── xcshareddata
│ └── xcschemes
│ └── SWCompression.xcscheme
├── Sources
├── 7-Zip
│ ├── 7zCoder+Equatable.swift
│ ├── 7zCoder.swift
│ ├── 7zCoderInfo.swift
│ ├── 7zContainer.swift
│ ├── 7zEntry.swift
│ ├── 7zEntryInfo.swift
│ ├── 7zError.swift
│ ├── 7zFileInfo.swift
│ ├── 7zFolder.swift
│ ├── 7zHeader.swift
│ ├── 7zPackInfo.swift
│ ├── 7zProperty.swift
│ ├── 7zStreamInfo.swift
│ ├── 7zSubstreamInfo.swift
│ ├── CompressionMethod+7z.swift
│ └── MsbBitReader+7z.swift
├── BZip2
│ ├── BZip2+BlockSize.swift
│ ├── BZip2+Compress.swift
│ ├── BZip2+Lengths.swift
│ ├── BZip2.swift
│ ├── BZip2Error.swift
│ ├── BurrowsWheeler.swift
│ └── SuffixArray.swift
├── Common
│ ├── Archive.swift
│ ├── CheckSums.swift
│ ├── CodingTree
│ │ ├── Code.swift
│ │ ├── CodeLength.swift
│ │ ├── DecodingTree.swift
│ │ └── EncodingTree.swift
│ ├── CompressionAlgorithm.swift
│ ├── CompressionMethod.swift
│ ├── Container
│ │ ├── Container.swift
│ │ ├── ContainerEntry.swift
│ │ ├── ContainerEntryInfo.swift
│ │ ├── ContainerEntryType.swift
│ │ ├── DosAttributes.swift
│ │ └── Permissions.swift
│ ├── DataError.swift
│ ├── DecompressionAlgorithm.swift
│ ├── DeltaFilter.swift
│ ├── Extensions.swift
│ └── FileSystemType.swift
├── Deflate
│ ├── Deflate+Compress.swift
│ ├── Deflate+Constants.swift
│ ├── Deflate+Lengths.swift
│ ├── Deflate.swift
│ └── DeflateError.swift
├── GZip
│ ├── FileSystemType+Gzip.swift
│ ├── GzipArchive.swift
│ ├── GzipError.swift
│ ├── GzipHeader+ExtraField.swift
│ └── GzipHeader.swift
├── LZ4
│ ├── LZ4+Compress.swift
│ ├── LZ4.swift
│ └── XxHash32.swift
├── LZMA
│ ├── LZMA.swift
│ ├── LZMABitTreeDecoder.swift
│ ├── LZMAConstants.swift
│ ├── LZMADecoder.swift
│ ├── LZMAError.swift
│ ├── LZMALenDecoder.swift
│ ├── LZMAProperties.swift
│ └── LZMARangeDecoder.swift
├── LZMA2
│ ├── LZMA2.swift
│ ├── LZMA2Decoder.swift
│ └── LZMA2Error.swift
├── PrivacyInfo.xcprivacy
├── TAR
│ ├── ContainerEntryType+Tar.swift
│ ├── Data+Tar.swift
│ ├── LittleEndianByteReader+Tar.swift
│ ├── TarContainer.swift
│ ├── TarCreateError.swift
│ ├── TarEntry.swift
│ ├── TarEntryInfo.swift
│ ├── TarError.swift
│ ├── TarExtendedHeader.swift
│ ├── TarHeader.swift
│ ├── TarParser.swift
│ ├── TarReader.swift
│ └── TarWriter.swift
├── XZ
│ ├── ByteReader+XZ.swift
│ ├── Sha256.swift
│ ├── XZArchive.swift
│ ├── XZBlock.swift
│ ├── XZError.swift
│ └── XZStreamHeader.swift
├── ZIP
│ ├── BuiltinExtraFields.swift
│ ├── CompressionMethod+Zip.swift
│ ├── FileSystemType+Zip.swift
│ ├── LittleEndianByteReader+Zip.swift
│ ├── ZipCentralDirectoryEntry.swift
│ ├── ZipContainer.swift
│ ├── ZipEndOfCentralDirectory.swift
│ ├── ZipEntry.swift
│ ├── ZipEntryInfo.swift
│ ├── ZipEntryInfoHelper.swift
│ ├── ZipError.swift
│ ├── ZipExtraField.swift
│ └── ZipLocalHeader.swift
├── Zlib
│ ├── ZlibArchive.swift
│ ├── ZlibError.swift
│ └── ZlibHeader.swift
└── swcomp
│ ├── Archives
│ ├── BZip2Command.swift
│ ├── GZipCommand.swift
│ ├── LZ4Command.swift
│ ├── LZMACommand.swift
│ └── XZCommand.swift
│ ├── Benchmarks
│ ├── Benchmark.swift
│ ├── BenchmarkGroup.swift
│ ├── BenchmarkMetadata.swift
│ ├── BenchmarkResult.swift
│ ├── Benchmarks.swift
│ ├── RunBenchmarkCommand.swift
│ ├── SaveFile.swift
│ ├── ShowBenchmarkCommand.swift
│ └── SpeedFormatter.swift
│ ├── Containers
│ ├── 7ZipCommand.swift
│ ├── CommonFunctions.swift
│ ├── ContainerCommand.swift
│ ├── TarCommand.swift
│ └── ZipCommand.swift
│ ├── Extensions
│ ├── CompressionMethod+CustomStringConvertible.swift
│ ├── ContainerEntryInfo+CustomStringConvertible.swift
│ ├── FileSystemType+CustomStringConvertible.swift
│ ├── GzipHeader+CustomStringConvertible.swift
│ ├── TarEntry+Create.swift
│ └── TarFormat+ConvertibleFromString.swift
│ ├── SwcompError.swift
│ └── main.swift
├── Tests
├── BZip2CompressionTests.swift
├── BZip2Tests.swift
├── Constants.swift
├── DeflateCompressionTests.swift
├── GzipTests.swift
├── LZ4CompressionTests.swift
├── LZ4Tests.swift
├── LzmaTests.swift
├── Results.md
├── SevenZipTests.swift
├── Sha256Tests.swift
├── TarCreateTests.swift
├── TarReaderTests.swift
├── TarTests.swift
├── TarWriterTests.swift
├── TestZipExtraField.swift
├── XxHash32Tests.swift
├── XzTests.swift
├── ZipTests.swift
└── ZlibTests.swift
├── azure-pipelines.yml
└── utils.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## Custom
6 | Tests/temp
7 |
8 | ## Build generated
9 | build/
10 | DerivedData/
11 |
12 | ## Various settings
13 | *.pbxuser
14 | !default.pbxuser
15 | *.mode1v3
16 | !default.mode1v3
17 | *.mode2v3
18 | !default.mode2v3
19 | *.perspectivev3
20 | !default.perspectivev3
21 | xcuserdata/
22 |
23 | ## Other
24 | *.moved-aside
25 | *.xcuserstate
26 | *.xccheckout
27 | *.xcscmblueprint
28 |
29 | ## Obj-C/Swift specific
30 | *.hmap
31 | *.ipa
32 | *.dSYM.zip
33 | *.dSYM
34 |
35 | ## Playgrounds
36 | timeline.xctimeline
37 | playground.xcworkspace
38 |
39 | # Swift Package Manager
40 | #
41 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
42 | # Packages/
43 | .build/
44 | Package.pins
45 |
46 | # CocoaPods
47 | Pods/
48 |
49 | # Carthage
50 | # Avoid checking in source code from Carthage dependencies.
51 | Carthage/Checkouts
52 | Carthage/Build
53 | Cartfile.resolved
54 |
55 | # fastlane
56 | #
57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
58 | # screenshots whenever they are needed.
59 | # For more information about the recommended setup visit:
60 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
61 |
62 | fastlane/report.xml
63 | fastlane/Preview.html
64 | fastlane/screenshots
65 | fastlane/test_output
66 |
67 | # Symbolic link to swcomp executable built by SPM
68 | /swcomp
69 |
70 | # Package.resolved, because we don't want to fix dependencies' versions
71 | Package.resolved
72 |
73 | # Docs generated by SourceKitten
74 | docs.json
75 |
76 | # Docs generated by Jazzy
77 | docs/
78 |
79 | # Vscode launch.json generated by Swift extension
80 | .vscode/launch.json
81 |
82 | # API baselines generate by swift package diagnose-api-breaking-changes
83 | api_baseline/
84 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "Tests/Test Files"]
2 | path = Tests/Test Files
3 | url = https://github.com/tsolomko/SWCompression-Test-Files.git
4 |
--------------------------------------------------------------------------------
/.jazzy.yaml:
--------------------------------------------------------------------------------
1 | # Run: sourcekitten doc --spm --module-name SWCompression > docs.json
2 | sourcekitten_sourcefile: docs.json
3 | clean: false
4 | author: Timofey Solomko
5 | module: SWCompression
6 | module_version: 4.8.6
7 | copyright: '© 2024 Timofey Solomko'
8 | readme: README.md
9 | github_url: https://github.com/tsolomko/SWCompression
10 | github_file_prefix: https://github.com/tsolomko/SWCompression/tree/4.8.6
11 | theme: fullwidth
12 |
13 | custom_categories:
14 | - name: Compression
15 | children:
16 | - BZip2
17 | - Deflate
18 | - LZMA
19 | - LZMAProperties
20 | - LZMA2
21 | - LZ4
22 | - name: Archives
23 | children:
24 | - GzipArchive
25 | - GzipHeader
26 | - XZArchive
27 | - ZlibArchive
28 | - ZlibHeader
29 | - name: 7-Zip
30 | children:
31 | - SevenZipContainer
32 | - SevenZipEntry
33 | - SevenZipEntryInfo
34 | - name: TAR
35 | children:
36 | - TarContainer
37 | - TarReader
38 | - TarWriter
39 | - TarEntry
40 | - TarEntryInfo
41 | - name: ZIP
42 | children:
43 | - ZipContainer
44 | - ZipEntry
45 | - ZipEntryInfo
46 | - ZipExtraField
47 | - ZipExtraFieldLocation
48 | - name: Errors
49 | children:
50 | - BZip2Error
51 | - DataError
52 | - DeflateError
53 | - GzipError
54 | - LZMAError
55 | - LZMA2Error
56 | - SevenZipError
57 | - TarCreateError
58 | - TarError
59 | - XZError
60 | - ZipError
61 | - ZlibError
62 | - name: Protocols
63 | children:
64 | - Archive
65 | - Container
66 | - ContainerEntry
67 | - ContainerEntryInfo
68 | - CompressionAlgorithm
69 | - DecompressionAlgorithm
70 | - name: Common Auxiliary Types
71 | children:
72 | - ContainerEntryType
73 | - DosAttributes
74 | - Permissions
75 | - CompressionMethod
76 | - FileSystemType
77 |
--------------------------------------------------------------------------------
/Cartfile:
--------------------------------------------------------------------------------
1 | github "tsolomko/BitByteData" ~> 2.0.0
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Timofey Solomko
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 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.7
2 | import PackageDescription
3 |
4 | var package = Package(
5 | name: "SWCompression",
6 | platforms: [
7 | .macOS(.v11),
8 | .iOS(.v14),
9 | .tvOS(.v14),
10 | .watchOS(.v7),
11 | // TODO: Enable after upgrading to Swift 5.9.
12 | // .visionOS(.v1)
13 | ],
14 | products: [
15 | .library(
16 | name: "SWCompression",
17 | targets: ["SWCompression"]),
18 | ],
19 | dependencies: [
20 | .package(url: "https://github.com/tsolomko/BitByteData", from: "2.0.0"),
21 | ],
22 | targets: [
23 | .target(
24 | name: "SWCompression",
25 | dependencies: ["BitByteData"],
26 | path: "Sources",
27 | exclude: ["swcomp"],
28 | sources: ["Common", "7-Zip", "BZip2", "Deflate", "GZip", "LZ4", "LZMA", "LZMA2", "TAR", "XZ", "ZIP", "Zlib"],
29 | resources: [.copy("PrivacyInfo.xcprivacy")]),
30 | ],
31 | swiftLanguageVersions: [.v5]
32 | )
33 |
34 | #if os(macOS)
35 | package.dependencies.append(.package(url: "https://github.com/jakeheis/SwiftCLI", from: "6.0.0"))
36 | package.targets.append(.executableTarget(name: "swcomp", dependencies: ["SWCompression", "SwiftCLI"], path: "Sources",
37 | exclude: ["Common", "7-Zip", "BZip2", "Deflate", "GZip", "LZ4", "LZMA", "LZMA2", "TAR", "XZ", "ZIP", "Zlib", "PrivacyInfo.xcprivacy"],
38 | sources: ["swcomp"]))
39 | #endif
40 |
--------------------------------------------------------------------------------
/SWCompression.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 |
3 | s.name = "SWCompression"
4 | s.version = "4.8.6"
5 | s.summary = "A framework with functions for working with compression, archives and containers."
6 |
7 | s.description = "A framework with (de)compression algorithms and functions for processing various archives and containers."
8 |
9 | s.homepage = "https://github.com/tsolomko/SWCompression"
10 | s.documentation_url = "http://tsolomko.github.io/SWCompression"
11 |
12 | s.license = { :type => "MIT", :file => "LICENSE" }
13 |
14 | s.author = { "Timofey Solomko" => "tsolomko@gmail.com" }
15 |
16 | s.source = { :git => "https://github.com/tsolomko/SWCompression.git", :tag => "#{s.version}" }
17 |
18 | s.ios.deployment_target = "14.0"
19 | s.osx.deployment_target = "11.0"
20 | s.tvos.deployment_target = "14.0"
21 | s.watchos.deployment_target = "7.0"
22 | # s.visionos.deployment_target = "1.0"
23 |
24 | s.swift_versions = ["5"]
25 |
26 | s.dependency "BitByteData", "~> 2.0"
27 |
28 | s.resource_bundles = {"SWCompression" => ["Sources/PrivacyInfo.xcprivacy"]}
29 |
30 | s.subspec "Deflate" do |sp|
31 | sp.source_files = "Sources/{Deflate/*,Common/*,Common/CodingTree/*}.swift"
32 | sp.pod_target_xcconfig = { "OTHER_SWIFT_FLAGS" => "-DSWCOMPRESSION_POD_DEFLATE" }
33 | end
34 |
35 | s.subspec "GZip" do |sp|
36 | sp.dependency "SWCompression/Deflate"
37 | sp.source_files = "Sources/{GZip/*,Common/*}.swift"
38 | end
39 |
40 | s.subspec "Zlib" do |sp|
41 | sp.dependency "SWCompression/Deflate"
42 | sp.source_files = "Sources/{Zlib/*,Common/*}.swift"
43 | end
44 |
45 | s.subspec "BZip2" do |sp|
46 | sp.source_files = "Sources/{BZip2/*,Common/*,Common/CodingTree/*}.swift"
47 | sp.pod_target_xcconfig = { "OTHER_SWIFT_FLAGS" => "-DSWCOMPRESSION_POD_BZ2" }
48 | end
49 |
50 | s.subspec "LZMA" do |sp|
51 | sp.source_files = "Sources/{LZMA/*,Common/*}.swift"
52 | sp.pod_target_xcconfig = { "OTHER_SWIFT_FLAGS" => "-DSWCOMPRESSION_POD_LZMA" }
53 | end
54 |
55 | s.subspec "LZMA2" do |sp|
56 | sp.dependency "SWCompression/LZMA"
57 | sp.source_files = "Sources/{LZMA2/*,Common/*}.swift"
58 | end
59 |
60 | s.subspec "LZ4" do |sp|
61 | sp.source_files = "Sources/{LZ4/*,Common/*}.swift"
62 | sp.pod_target_xcconfig = { "OTHER_SWIFT_FLAGS" => "-DSWCOMPRESSION_POD_LZ4" }
63 | end
64 |
65 | s.subspec "XZ" do |sp|
66 | sp.dependency "SWCompression/LZMA2"
67 | sp.source_files = "Sources/{XZ/*,Common/*}.swift"
68 | end
69 |
70 | s.subspec "ZIP" do |sp|
71 | sp.dependency "SWCompression/Deflate"
72 | sp.source_files = "Sources/{Zip/*,Common/*,Common/Container/*}.swift"
73 | sp.pod_target_xcconfig = { "OTHER_SWIFT_FLAGS" => "-DSWCOMPRESSION_POD_ZIP" }
74 | end
75 |
76 | s.subspec "TAR" do |sp|
77 | sp.source_files = "Sources/{TAR/*,Common/*,Common/Container/*}.swift"
78 | end
79 |
80 | s.subspec "SevenZip" do |sp|
81 | sp.dependency "SWCompression/LZMA2"
82 | sp.source_files = "Sources/{7-Zip/*,Common/*,Common/Container/*}.swift"
83 | sp.pod_target_xcconfig = { "OTHER_SWIFT_FLAGS" => "-DSWCOMPRESSION_POD_SEVENZIP" }
84 | end
85 |
86 | end
87 |
--------------------------------------------------------------------------------
/SWCompression.xcodeproj/SWCompression.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 4.8.6
19 | CFBundleVersion
20 | 91
21 | NSHumanReadableCopyright
22 | Copyright © 2024 Timofey Solomko
23 |
24 |
25 |
--------------------------------------------------------------------------------
/SWCompression.xcodeproj/SWCompression.xctestplan:
--------------------------------------------------------------------------------
1 | {
2 | "configurations" : [
3 | {
4 | "id" : "7E23A357-7F2B-40CE-A7EE-BA2D541E111E",
5 | "name" : "Default Configuration",
6 | "options" : {
7 |
8 | }
9 | }
10 | ],
11 | "defaultOptions" : {
12 | "codeCoverage" : false,
13 | "targetForVariableExpansion" : {
14 | "containerPath" : "container:SWCompression.xcodeproj",
15 | "identifier" : "06BE1AC71DB410F100EE0F59",
16 | "name" : "SWCompression"
17 | },
18 | "testExecutionOrdering" : "random"
19 | },
20 | "testTargets" : [
21 | {
22 | "parallelizable" : true,
23 | "target" : {
24 | "containerPath" : "container:SWCompression.xcodeproj",
25 | "identifier" : "06F065941FFB761600312A82",
26 | "name" : "TestSWCompression"
27 | }
28 | }
29 | ],
30 | "version" : 1
31 | }
32 |
--------------------------------------------------------------------------------
/SWCompression.xcodeproj/TestSWCompression.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 4.8.6
19 | CFBundleVersion
20 | 91
21 |
22 |
23 |
--------------------------------------------------------------------------------
/SWCompression.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/SWCompression.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/SWCompression.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | BuildSystemType
6 | Latest
7 |
8 |
9 |
--------------------------------------------------------------------------------
/SWCompression.xcodeproj/xcshareddata/xcschemes/SWCompression.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
43 |
44 |
45 |
46 |
50 |
56 |
57 |
58 |
59 |
60 |
70 |
71 |
77 |
78 |
79 |
80 |
86 |
87 |
93 |
94 |
95 |
96 |
98 |
99 |
102 |
103 |
104 |
--------------------------------------------------------------------------------
/Sources/7-Zip/7zCoder+Equatable.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | extension SevenZipCoder: Equatable {
9 |
10 | static func == (lhs: SevenZipCoder, rhs: SevenZipCoder) -> Bool {
11 | let propertiesEqual: Bool
12 | if lhs.properties == nil && rhs.properties == nil {
13 | propertiesEqual = true
14 | } else if lhs.properties != nil && rhs.properties != nil {
15 | propertiesEqual = lhs.properties! == rhs.properties!
16 | } else {
17 | propertiesEqual = false
18 | }
19 | return lhs.id == rhs.id && lhs.numInStreams == rhs.numInStreams &&
20 | lhs.numOutStreams == rhs.numOutStreams && propertiesEqual
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/7-Zip/7zCoder.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import BitByteData
8 |
9 | class SevenZipCoder {
10 |
11 | let idSize: Int
12 | let isComplex: Bool
13 | let hasAttributes: Bool
14 |
15 | let id: [UInt8]
16 | let compressionMethod: CompressionMethod
17 | let isEncryptionMethod: Bool
18 |
19 | let numInStreams: Int
20 | let numOutStreams: Int
21 |
22 | var propertiesSize: Int?
23 | var properties: [UInt8]?
24 |
25 | init(_ bitReader: MsbBitReader) throws {
26 | let flags = bitReader.byte()
27 | guard flags & 0xC0 == 0
28 | else { throw SevenZipError.internalStructureError }
29 | idSize = (flags & 0x0F).toInt()
30 | isComplex = flags & 0x10 != 0
31 | hasAttributes = flags & 0x20 != 0
32 |
33 | id = bitReader.bytes(count: idSize)
34 | compressionMethod = CompressionMethod(id)
35 | isEncryptionMethod = id[0] == 0x06
36 |
37 | numInStreams = isComplex ? bitReader.szMbd() : 1
38 | numOutStreams = isComplex ? bitReader.szMbd() : 1
39 |
40 | if hasAttributes {
41 | propertiesSize = bitReader.szMbd()
42 | properties = bitReader.bytes(count: propertiesSize!)
43 | }
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/7-Zip/7zCoderInfo.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import BitByteData
8 |
9 | class SevenZipCoderInfo {
10 |
11 | let numFolders: Int
12 |
13 | let external: UInt8
14 | private(set) var folders = [SevenZipFolder]()
15 |
16 | init() {
17 | numFolders = 0
18 | external = 0
19 | }
20 |
21 | init(_ bitReader: MsbBitReader) throws {
22 | var type = bitReader.byte()
23 | guard type == 0x0B else { throw SevenZipError.internalStructureError }
24 |
25 | numFolders = bitReader.szMbd()
26 | external = bitReader.byte()
27 |
28 | guard external == 0
29 | else { throw SevenZipError.externalNotSupported }
30 |
31 | for _ in 0..> 16)
68 | self.dosAttributes = DosAttributes(rawValue: 0xFF & attributes)
69 | } else {
70 | self.permissions = nil
71 | self.dosAttributes = nil
72 | }
73 |
74 | // Set entry type.
75 | if let attributes = self.winAttributes,
76 | let unixType = ContainerEntryType((0xF0000000 & attributes) >> 16) {
77 | self.type = unixType
78 | } else if let dosAttributes = self.dosAttributes {
79 | if dosAttributes.contains(.directory) {
80 | self.type = .directory
81 | } else {
82 | self.type = .regular
83 | }
84 | } else if file.isEmptyStream && !file.isEmptyFile {
85 | self.type = .directory
86 | } else {
87 | self.type = .regular
88 | }
89 |
90 | self.crc = crc
91 | self.size = size
92 | }
93 |
94 | }
95 |
--------------------------------------------------------------------------------
/Sources/7-Zip/7zError.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | /**
9 | Represents an error which happened while processing a 7-Zip container.
10 | It may indicate that either container is damaged or it might not be 7-Zip container at all.
11 | */
12 | public enum SevenZipError: Error {
13 | /// Wrong container's signature.
14 | case wrongSignature
15 | /// Unsupported version of container's format.
16 | case wrongFormatVersion
17 | /// CRC either of one of the files from the container or one of the container's strucutures is incorrect.
18 | case wrongCRC
19 | /// Size either of one of the files from the container or one of the container's strucutures is incorrect.
20 | case wrongSize
21 | /// Files have StartPos property. This feature isn't supported.
22 | case startPosNotSupported
23 | /// External feature isn't supported.
24 | case externalNotSupported
25 | /// Coders with multiple in and/or out streams aren't supported.
26 | case multiStreamNotSupported
27 | /// Additional streams feature isn't supported.
28 | case additionalStreamsNotSupported
29 | /// Entry is compressed using unsupported compression method.
30 | case compressionNotSupported
31 | /// Entry or container's header is encrypted. This feature isn't supported.
32 | case encryptionNotSupported
33 | /// Unknown/incorrect internal 7-Zip structure was encountered or a required internal structure is missing.
34 | case internalStructureError
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/7-Zip/7zHeader.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import BitByteData
8 |
9 | class SevenZipHeader {
10 |
11 | var archiveProperties: [SevenZipProperty]?
12 | var additionalStreams: SevenZipStreamInfo?
13 | var mainStreams: SevenZipStreamInfo?
14 | var fileInfo: SevenZipFileInfo?
15 |
16 | init(_ bitReader: MsbBitReader) throws {
17 | var type = bitReader.byte()
18 |
19 | if type == 0x02 {
20 | archiveProperties = try SevenZipProperty.getProperties(bitReader)
21 | type = bitReader.byte()
22 | }
23 |
24 | if type == 0x03 {
25 | throw SevenZipError.additionalStreamsNotSupported
26 | }
27 |
28 | if type == 0x04 {
29 | mainStreams = try SevenZipStreamInfo(bitReader)
30 | type = bitReader.byte()
31 | }
32 |
33 | if type == 0x05 {
34 | fileInfo = try SevenZipFileInfo(bitReader)
35 | type = bitReader.byte()
36 | }
37 |
38 | guard type == 0x00
39 | else { throw SevenZipError.internalStructureError }
40 | }
41 |
42 | convenience init(_ bitReader: MsbBitReader, using streamInfo: SevenZipStreamInfo) throws {
43 | let folder = streamInfo.coderInfo.folders[0]
44 | guard let packInfo = streamInfo.packInfo
45 | else { throw SevenZipError.internalStructureError }
46 |
47 | let folderOffset = SevenZipContainer.signatureHeaderSize + packInfo.packPosition
48 | bitReader.offset = folderOffset
49 |
50 | let packedHeaderData = Data(bitReader.bytes(count: packInfo.packSizes[0]))
51 | let headerData = try folder.unpack(data: packedHeaderData)
52 |
53 | guard headerData.count == folder.unpackSize()
54 | else { throw SevenZipError.wrongSize }
55 | if let crc = folder.crc {
56 | guard CheckSums.crc32(headerData) == crc
57 | else { throw SevenZipError.wrongCRC }
58 | }
59 |
60 | let headerBitReader = MsbBitReader(data: headerData)
61 |
62 | guard headerBitReader.byte() == 0x01
63 | else { throw SevenZipError.internalStructureError }
64 | try self.init(headerBitReader)
65 | }
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/Sources/7-Zip/7zPackInfo.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import BitByteData
8 |
9 | class SevenZipPackInfo {
10 |
11 | let packPosition: Int
12 | let numPackStreams: Int
13 | private(set) var packSizes = [Int]()
14 | private(set) var digests = [UInt32?]()
15 |
16 | init(_ bitReader: MsbBitReader) throws {
17 | packPosition = bitReader.szMbd()
18 | numPackStreams = bitReader.szMbd()
19 |
20 | var type = bitReader.byte()
21 |
22 | if type == 0x09 {
23 | for _ in 0.. [SevenZipProperty] {
22 | var properties = [SevenZipProperty]()
23 | while true {
24 | let propertyType = bitReader.byte()
25 | if propertyType == 0 {
26 | break
27 | }
28 | let propertySize = bitReader.szMbd()
29 | properties.append(SevenZipProperty(propertyType, propertySize, bitReader.bytes(count: propertySize)))
30 | }
31 | return properties
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/7-Zip/7zStreamInfo.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import BitByteData
8 |
9 | class SevenZipStreamInfo {
10 |
11 | var packInfo: SevenZipPackInfo?
12 | var coderInfo: SevenZipCoderInfo
13 | var substreamInfo: SevenZipSubstreamInfo?
14 |
15 | init(_ bitReader: MsbBitReader) throws {
16 | var type = bitReader.byte()
17 |
18 | if type == 0x06 {
19 | packInfo = try SevenZipPackInfo(bitReader)
20 | type = bitReader.byte()
21 | }
22 |
23 | if type == 0x07 {
24 | coderInfo = try SevenZipCoderInfo(bitReader)
25 | type = bitReader.byte()
26 | } else {
27 | coderInfo = SevenZipCoderInfo()
28 | }
29 |
30 | if type == 0x08 {
31 | substreamInfo = try SevenZipSubstreamInfo(bitReader, coderInfo)
32 | type = bitReader.byte()
33 | }
34 |
35 | guard type == 0x00
36 | else { throw SevenZipError.internalStructureError }
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/Sources/7-Zip/7zSubstreamInfo.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import BitByteData
8 |
9 | class SevenZipSubstreamInfo {
10 |
11 | var numUnpackStreamsInFolders = [Int]()
12 | var unpackSizes = [Int]()
13 | var digests = [UInt32?]()
14 |
15 | init(_ bitReader: MsbBitReader, _ coderInfo: SevenZipCoderInfo) throws {
16 | var totalUnpackStreams = coderInfo.folders.count
17 |
18 | var type = bitReader.byte()
19 |
20 | if type == 0x0D {
21 | totalUnpackStreams = 0
22 | for folder in coderInfo.folders {
23 | let numStreams = bitReader.szMbd()
24 | folder.numUnpackSubstreams = numStreams
25 | totalUnpackStreams += numStreams
26 | }
27 | type = bitReader.byte()
28 | }
29 |
30 | for folder in coderInfo.folders {
31 | if folder.numUnpackSubstreams == 0 {
32 | continue
33 | }
34 | var sum = 0
35 | if type == 0x09 {
36 | for _ in 0.. Int {
13 | self.align()
14 | let firstByte = self.byte().toInt()
15 | var mask = 0x80
16 | var value = 0
17 | for i in 0..<8 {
18 | if firstByte & mask == 0 {
19 | value |= ((firstByte & (mask &- 1)) << (8 * i))
20 | break
21 | }
22 | value |= self.byte().toInt() << (8 * i)
23 | mask >>= 1
24 | }
25 | return value
26 | }
27 |
28 | func defBits(count: Int) -> [UInt8] {
29 | self.align()
30 | let allDefined = self.byte()
31 | let definedBits: [UInt8]
32 | if allDefined == 0 {
33 | definedBits = self.bits(count: count)
34 | } else {
35 | definedBits = Array(repeating: 1, count: count)
36 | }
37 | return definedBits
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/Sources/BZip2/BZip2+BlockSize.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | extension BZip2 {
9 |
10 | /// Represents the size of the blocks in which data is split during BZip2 compression.
11 | public enum BlockSize: Int {
12 | /// 100 KB.
13 | case one = 1
14 | /// 200 KB.
15 | case two = 2
16 | /// 300 KB.
17 | case three = 3
18 | /// 400 KB.
19 | case four = 4
20 | /// 500 KB.
21 | case five = 5
22 | /// 600 KB.
23 | case six = 6
24 | /// 700 KB.
25 | case seven = 7
26 | /// 800 KB.
27 | case eight = 8
28 | /// 900 KB.
29 | case nine = 9
30 |
31 | init?(_ headerByte: UInt8) {
32 | switch headerByte {
33 | case 0x31:
34 | self = .one
35 | case 0x32:
36 | self = .two
37 | case 0x33:
38 | self = .three
39 | case 0x34:
40 | self = .four
41 | case 0x35:
42 | self = .five
43 | case 0x36:
44 | self = .six
45 | case 0x37:
46 | self = .seven
47 | case 0x38:
48 | self = .eight
49 | case 0x39:
50 | self = .nine
51 | default:
52 | return nil
53 | }
54 | }
55 |
56 | var headerByte: Int {
57 | return self.rawValue + 0x30
58 | }
59 |
60 | var sizeInKilobytes: Int {
61 | return self.rawValue * 100
62 | }
63 |
64 | }
65 |
66 | }
67 |
--------------------------------------------------------------------------------
/Sources/BZip2/BZip2+Lengths.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | // BZip2 specific function for generation of HuffmanLength array from stats.
9 | extension BZip2 {
10 |
11 | /**
12 | Based on "procedure for generating the lists which specify a Huffman code table" (annexes C and K)
13 | from Recommendation T.81 of ITU (aka JPEG specfications).
14 | */
15 | static func lengths(from stats: [Int]) -> [CodeLength] {
16 | // Handle redundant cases.
17 | if stats.count == 0 {
18 | return []
19 | } else if stats.count == 1 {
20 | return [CodeLength(symbol: 0, codeLength: 1)]
21 | }
22 |
23 | // Calculate code lengths based on stats.
24 | let codeLengths = calculateCodeLengths(from: stats)
25 |
26 | // Now we count code sizes.
27 | var bits = count(codeLengths)
28 |
29 | adjust(&bits)
30 |
31 | return generateSizeTable(from: bits)
32 | }
33 |
34 | private static func calculateCodeLengths(from stats: [Int]) -> [Int] {
35 | /// Mutable copy of input `stats`.
36 | var stats = stats
37 |
38 | var codeLengths = Array(repeating: 0, count: stats.count)
39 | var others = Array(repeating: -1, count: stats.count)
40 |
41 | while true {
42 | var c1 = -1
43 | var minFreq = Int.max
44 | for i in 0.. 0 && stats[i] <= minFreq {
46 | minFreq = stats[i]
47 | c1 = i
48 | }
49 | }
50 |
51 | var c2 = -1
52 | minFreq = Int.max
53 | for i in 0.. 0 && stats[i] <= minFreq && i != c1 {
55 | minFreq = stats[i]
56 | c2 = i
57 | }
58 | }
59 |
60 | guard c2 >= 0
61 | else { break }
62 |
63 | stats[c1] += stats[c2]
64 | stats[c2] = 0
65 |
66 | codeLengths[c1] += 1
67 | while others[c1] >= 0 {
68 | c1 = others[c1]
69 | codeLengths[c1] += 1
70 |
71 | }
72 | others[c1] = c2
73 |
74 | codeLengths[c2] += 1
75 | while others[c2] >= 0 {
76 | c2 = others[c2]
77 | codeLengths[c2] += 1
78 | }
79 | }
80 | return codeLengths
81 | }
82 |
83 | private static func count(_ codeLengths: [Int]) -> [Int] {
84 | var bits = Array(repeating: 0, count: codeLengths.count)
85 | for i in 0.. 0 {
95 | var j = i - 2
96 | while bits[j] == 0 {
97 | j -= 1
98 | }
99 | bits[i] -= 2
100 | bits[i - 1] += 1
101 | bits[j + 1] += 2
102 | bits[j] -= 1
103 | }
104 | }
105 | }
106 |
107 | private static func generateSizeTable(from bits: [Int]) -> [CodeLength] {
108 | var symbol = 0
109 | var lengths = [CodeLength]()
110 | for i in 1...min(20, bits.count - 1) {
111 | var j = 1
112 | while j <= bits[i] {
113 | lengths.append(CodeLength(symbol: symbol, codeLength: i))
114 | symbol += 1
115 | j += 1
116 | }
117 | }
118 | return lengths
119 | }
120 |
121 | }
122 |
--------------------------------------------------------------------------------
/Sources/BZip2/BZip2Error.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | /**
9 | Represents an error which happened during BZip2 decompression.
10 | It may indicate that either data is damaged or it might not be compressed with BZip2 at all.
11 | */
12 | public enum BZip2Error: Error {
13 | /// 'Magic' number is not 0x425a.
14 | case wrongMagic
15 | /// BZip version is not 2.
16 | case wrongVersion
17 | /// Unsupported block size (not from '0' to '9').
18 | case wrongBlockSize
19 | /// Unsupported block type (is neither 'pi' nor 'sqrt(pi)').
20 | case wrongBlockType
21 | /// Block is randomized.
22 | case randomizedBlock
23 | /// Wrong number of Huffman tables/groups (should be between 2 and 6).
24 | case wrongHuffmanGroups
25 | /// Selector is greater than the total number of Huffman tables/groups.
26 | case wrongSelector
27 | /// Wrong length of Huffman code (should be between 0 and 20).
28 | case wrongHuffmanCodeLength
29 | /// Symbol wasn't found in Huffman tree.
30 | case symbolNotFound
31 | /**
32 | Computed checksum of uncompressed data doesn't match the value stored in archive.
33 | Associated value of the error contains already decompressed data.
34 | */
35 | case wrongCRC(Data)
36 | }
37 |
--------------------------------------------------------------------------------
/Sources/BZip2/BurrowsWheeler.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | enum BurrowsWheeler {
9 |
10 | static func transform(bytes: [Int]) -> ([Int], Int) {
11 | let doubleBytes = bytes + bytes
12 | let suffixArray = SuffixArray.make(from: doubleBytes, with: 256)
13 | var bwt = [Int]()
14 | bwt.reserveCapacity(bytes.count)
15 | // Pointer is an index in the transformed array, `bwt`, where the EOF marker would have been if we had used it.
16 | var pointer = 0
17 | for i in 1.. 0 {
20 | bwt.append(bytes[suffixArray[i] - 1])
21 | } else {
22 | bwt.append(bytes.last!)
23 | }
24 | } else if suffixArray[i] == bytes.count {
25 | pointer = bwt.count
26 | }
27 | }
28 | return (bwt, pointer)
29 | }
30 |
31 | static func reverse(bytes: [UInt8], _ pointer: Int) -> [UInt8] {
32 | guard bytes.count > 0
33 | else { return [] }
34 |
35 | var counts = Array(repeating: 0, count: 256)
36 | for byte in bytes {
37 | counts[byte.toInt()] += 1
38 | }
39 |
40 | var base = Array(repeating: -1, count: 256)
41 | var sum = 0
42 | for byteType in 0..<256 {
43 | if counts[byteType] == 0 {
44 | continue
45 | } else {
46 | base[byteType] = sum
47 | sum += counts[byteType]
48 | }
49 | }
50 |
51 | var pointers = Array(repeating: -1, count: bytes.count)
52 | for (i, char) in bytes.enumerated() {
53 | pointers[base[char.toInt()]] = i
54 | base[char.toInt()] += 1
55 | }
56 |
57 | var resultBytes = [UInt8]()
58 | resultBytes.reserveCapacity(bytes.count)
59 |
60 | var end = pointer
61 | for _ in 0.. Data
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/Common/CodingTree/Code.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | struct Code {
9 |
10 | /// Number of bits used for `code`.
11 | let bits: Int
12 | let code: Int
13 | let symbol: Int
14 |
15 | /// `lengths` don't have to be sorted, but there must not be any 0 code lengths.
16 | static func huffmanCodes(from lengths: [CodeLength]) -> (codes: [Code], maxBits: Int) {
17 | // Sort `lengths` array to calculate canonical Huffman code.
18 | let sortedLengths = lengths.sorted()
19 |
20 | // Calculate maximum amount of leaves possible in a tree.
21 | let maxBits = sortedLengths.last!.codeLength
22 | var codes = [Code]()
23 | codes.reserveCapacity(sortedLengths.count)
24 |
25 | var loopBits = -1
26 | var symbol = -1
27 | for length in sortedLengths {
28 | precondition(length.codeLength > 0, "Code length must not be 0 during HuffmanTree construction.")
29 | symbol += 1
30 | // We sometimes need to make symbol to have length.bits bit length.
31 | let bits = length.codeLength
32 | if bits != loopBits {
33 | symbol <<= (bits - loopBits)
34 | loopBits = bits
35 | }
36 | // Then we need to reverse bit order of the symbol.
37 | let code = symbol.reversed(bits: loopBits)
38 | codes.append(Code(bits: length.codeLength, code: code, symbol: length.symbol))
39 | }
40 | return (codes, maxBits)
41 | }
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/Sources/Common/CodingTree/CodeLength.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | struct CodeLength: Equatable {
9 |
10 | let symbol: Int
11 | let codeLength: Int
12 |
13 | }
14 |
15 | extension CodeLength: Comparable {
16 |
17 | static func < (left: CodeLength, right: CodeLength) -> Bool {
18 | if left.codeLength == right.codeLength {
19 | return left.symbol < right.symbol
20 | } else {
21 | return left.codeLength < right.codeLength
22 | }
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/Common/CodingTree/DecodingTree.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import BitByteData
8 |
9 | final class DecodingTree {
10 |
11 | private let bitReader: BitReader
12 |
13 | private let tree: [Int]
14 | private let leafCount: Int
15 |
16 | init(codes: [Code], maxBits: Int, _ bitReader: BitReader) {
17 | self.bitReader = bitReader
18 |
19 | // Calculate maximum amount of leaves in a tree.
20 | self.leafCount = 1 << (maxBits + 1)
21 | var tree = Array(repeating: -1, count: leafCount)
22 |
23 | for code in codes {
24 | // Put code in its place in the tree.
25 | var treeCode = code.code
26 | var index = 0
27 | for _ in 0..>= 1
31 | }
32 | tree[index] = code.symbol
33 | }
34 | self.tree = tree
35 | }
36 |
37 | func findNextSymbol() -> Int {
38 | var bitsLeft = bitReader.bitsLeft
39 | var index = 0
40 | while bitsLeft > 0 {
41 | let bit = bitReader.bit()
42 | index = bit == 0 ? 2 * index + 1 : 2 * index + 2
43 | bitsLeft -= 1
44 | guard index < self.leafCount
45 | else { return -1 }
46 | if self.tree[index] > -1 {
47 | return self.tree[index]
48 | }
49 | }
50 | return -1
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/Sources/Common/CodingTree/EncodingTree.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import BitByteData
8 |
9 | fileprivate struct CodingIndex {
10 |
11 | let treeCode: Int
12 | let bitSize: Int
13 |
14 | }
15 |
16 | final class EncodingTree {
17 |
18 | private let bitWriter: BitWriter
19 |
20 | private let codingIndices: [CodingIndex]
21 |
22 | init(codes: [Code], _ bitWriter: BitWriter, reverseCodes: Bool = false) {
23 | self.bitWriter = bitWriter
24 |
25 | var codingIndices = Array(repeating: CodingIndex(treeCode: -1, bitSize: -1), count: codes.count)
26 |
27 | for code in codes {
28 | // Codes have already been reversed.
29 | // TODO: This assumption may be only correct for Huffman codes.
30 | let treeCode = reverseCodes ? code.code : code.code.reversed(bits: code.bits)
31 | codingIndices[code.symbol] = CodingIndex(treeCode: treeCode, bitSize: code.bits)
32 | }
33 | self.codingIndices = codingIndices
34 | }
35 |
36 | func code(symbol: Int) {
37 | guard symbol < self.codingIndices.count
38 | else { fatalError("Symbol is not found.") }
39 |
40 | let codingIndex = self.codingIndices[symbol]
41 |
42 | guard codingIndex.treeCode > -1
43 | else { fatalError("Symbol is not found.") }
44 |
45 | self.bitWriter.write(number: codingIndex.treeCode, bitsCount: codingIndex.bitSize)
46 | }
47 |
48 | func bitSize(for stats: [Int]) -> Int {
49 | var totalSize = 0
50 | for (symbol, count) in stats.enumerated() where count > 0 {
51 | guard symbol < self.codingIndices.count
52 | else { fatalError("Symbol is not found.") }
53 | let codingIndex = self.codingIndices[symbol]
54 | guard codingIndex.treeCode > -1
55 | else { fatalError("Symbol is not found.") }
56 |
57 | totalSize += count * codingIndex.bitSize
58 | }
59 | return totalSize
60 | }
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/Sources/Common/CompressionAlgorithm.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | /// A type that provides an implementation of a particular compression algorithm.
9 | public protocol CompressionAlgorithm {
10 |
11 | /// Compress data with particular algorithm.
12 | static func compress(data: Data) throws -> Data
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/Common/CompressionMethod.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | /// Represents a (de)compression method.
9 | public enum CompressionMethod {
10 | /// BZip2.
11 | case bzip2
12 | /// Copy (no compression).
13 | case copy
14 | /// Deflate.
15 | case deflate
16 | /// LZMA.
17 | case lzma
18 | /// LZMA 2.
19 | case lzma2
20 | /// Other/unknown method.
21 | case other
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/Common/Container/Container.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | /// A type that represents a container with files, directories and/or other data.
9 | public protocol Container {
10 |
11 | /// A type that represents an entry from this container.
12 | associatedtype Entry: ContainerEntry
13 |
14 | /// Retrieve all container entries with their data.
15 | static func open(container: Data) throws -> [Entry]
16 |
17 | /// Retrieve information about all container entries (without their data).
18 | static func info(container: Data) throws -> [Entry.Info]
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/Common/Container/ContainerEntry.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | /// A type that represents an entry from the container with its data and information.
9 | public protocol ContainerEntry {
10 |
11 | /// A type that provides information about an entry.
12 | associatedtype Info: ContainerEntryInfo
13 |
14 | /// Provides access to information about the entry.
15 | var info: Info { get }
16 |
17 | /**
18 | Entry's data (`nil` if entry is a directory or data isn't available).
19 |
20 | - Note: It is assumed that the compression provided by the container is yet to be applied to data.
21 | */
22 | var data: Data? { get }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/Common/Container/ContainerEntryInfo.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | /// A type that provides access to information about an entry from the container.
9 | public protocol ContainerEntryInfo {
10 |
11 | /// Entry's name.
12 | var name: String { get }
13 |
14 | /// Entry's type.
15 | var type: ContainerEntryType { get }
16 |
17 | /// Entry's data size (can be `nil` if either data or size aren't available).
18 | var size: Int? { get }
19 |
20 | /// Entry's last access time (`nil`, if not available).
21 | var accessTime: Date? { get }
22 |
23 | /// Entry's creation time (`nil`, if not available).
24 | var creationTime: Date? { get }
25 |
26 | /// Entry's last modification time (`nil`, if not available).
27 | var modificationTime: Date? { get }
28 |
29 | /// Entry's permissions in POSIX format (`nil`, if not available).
30 | var permissions: Permissions? { get }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/Sources/Common/Container/ContainerEntryType.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | /// Represents the type of a container entry.
9 | public enum ContainerEntryType {
10 | /// Block special file.
11 | case blockSpecial
12 | /// Character special file.
13 | case characterSpecial
14 | /// Contiguous file.
15 | case contiguous
16 | /// Directory.
17 | case directory
18 | /// FIFO special file.
19 | case fifo
20 | /// Hard link.
21 | case hardLink
22 | /// Regular file.
23 | case regular
24 | /// Socket.
25 | case socket
26 | /// Symbolic link.
27 | case symbolicLink
28 | /// Entry type is unknown.
29 | case unknown
30 |
31 | /// This initalizer's semantics assume conversion from UNIX type, which, by definition, doesn't have `unknown` type.
32 | init?(_ unixType: UInt32) {
33 | switch unixType {
34 | case 0o010000:
35 | self = .fifo
36 | case 0o020000:
37 | self = .characterSpecial
38 | case 0o040000:
39 | self = .directory
40 | case 0o060000:
41 | self = .blockSpecial
42 | case 0o100000:
43 | self = .regular
44 | case 0o120000:
45 | self = .symbolicLink
46 | case 0o140000:
47 | self = .socket
48 | default:
49 | return nil
50 | }
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/Sources/Common/Container/DosAttributes.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | /// Represents file attributes in DOS format.
9 | public struct DosAttributes: OptionSet {
10 |
11 | /// Raw bit flags value.
12 | public let rawValue: UInt32
13 |
14 | /// Initializes attributes with bit flags.
15 | public init(rawValue: UInt32) {
16 | self.rawValue = rawValue
17 | }
18 |
19 | /// File is archive or archived.
20 | public static let archive = DosAttributes(rawValue: 0b00100000)
21 |
22 | /// File is a directory.
23 | public static let directory = DosAttributes(rawValue: 0b00010000)
24 |
25 | /// File is a volume.
26 | public static let volume = DosAttributes(rawValue: 0b00001000)
27 |
28 | /// File is a system file.
29 | public static let system = DosAttributes(rawValue: 0b00000100)
30 |
31 | /// File is hidden.
32 | public static let hidden = DosAttributes(rawValue: 0b00000010)
33 |
34 | /// File is read-only.
35 | public static let readOnly = DosAttributes(rawValue: 0b00000001)
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/Common/Container/Permissions.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | /// Represents file access permissions in UNIX format.
9 | public struct Permissions: OptionSet {
10 |
11 | /// Raw bit flags value (in decimal).
12 | public let rawValue: UInt32
13 |
14 | /// Initializes permissions with bit flags in decimal.
15 | public init(rawValue: UInt32) {
16 | self.rawValue = rawValue
17 | }
18 |
19 | /// Set UID.
20 | public static let setuid = Permissions(rawValue: 0o4000)
21 |
22 | /// Set GID.
23 | public static let setgid = Permissions(rawValue: 0o2000)
24 |
25 | /// Sticky bit.
26 | public static let sticky = Permissions(rawValue: 0o1000)
27 |
28 | /// Owner can read.
29 | public static let readOwner = Permissions(rawValue: 0o0400)
30 |
31 | /// Owner can write.
32 | public static let writeOwner = Permissions(rawValue: 0o0200)
33 |
34 | /// Owner can execute.
35 | public static let executeOwner = Permissions(rawValue: 0o0100)
36 |
37 | /// Group can read.
38 | public static let readGroup = Permissions(rawValue: 0o0040)
39 |
40 | /// Group can write.
41 | public static let writeGroup = Permissions(rawValue: 0o0020)
42 |
43 | /// Group can execute.
44 | public static let executeGroup = Permissions(rawValue: 0o0010)
45 |
46 | /// Others can read.
47 | public static let readOther = Permissions(rawValue: 0o0004)
48 |
49 | /// Others can write.
50 | public static let writeOther = Permissions(rawValue: 0o0002)
51 |
52 | /// Others can execute.
53 | public static let executeOther = Permissions(rawValue: 0o0001)
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/Sources/Common/DataError.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | /// Represents an error which happened during processing input data.
9 | public enum DataError: Error, Equatable {
10 | /// Indicates that input data is likely truncated or incomplete.
11 | case truncated
12 | /**
13 | Indicates that input data is corrupted, e.g. does not conform to the format specifications or contains other
14 | invalid values.
15 | */
16 | case corrupted
17 | /**
18 | Indicates that the computed checksum of the output data does not match the stored checksum. While usually the
19 | associated value contains the output from all processed inputs up to and including the point when this error was
20 | thrown, it is still recommended to check the documenation of a function to confirm this.
21 | */
22 | case checksumMismatch([Data])
23 | /// Indicates that input data was created using a feature that is not supported by the processing function.
24 | case unsupportedFeature
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/Common/DecompressionAlgorithm.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | /// A type that provides an implementation of a particular decompression algorithm.
9 | public protocol DecompressionAlgorithm {
10 |
11 | /// Decompress data compressed with particular algorithm.
12 | static func decompress(data: Data) throws -> Data
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/Common/DeltaFilter.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import BitByteData
8 |
9 | enum DeltaFilter {
10 |
11 | static func decode(_ byteReader: LittleEndianByteReader, _ distance: Int) -> Data {
12 | var out = [UInt8]()
13 |
14 | var pos = 0
15 | var delta = Array(repeating: 0 as UInt8, count: 256)
16 |
17 | while !byteReader.isFinished {
18 | let byte = byteReader.byte()
19 |
20 | var tmp = delta[(distance + pos) % 256]
21 | tmp = byte &+ tmp
22 | delta[pos] = tmp
23 |
24 | out.append(tmp)
25 | if pos == 0 {
26 | pos = 255
27 | } else {
28 | pos -= 1
29 | }
30 | }
31 |
32 | return Data(out)
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/Common/Extensions.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | extension UnsignedInteger {
9 |
10 | @inlinable @inline(__always)
11 | func toInt() -> Int {
12 | return Int(truncatingIfNeeded: self)
13 | }
14 |
15 | }
16 |
17 | extension Int {
18 |
19 | @inlinable @inline(__always)
20 | func toUInt8() -> UInt8 {
21 | return UInt8(truncatingIfNeeded: UInt(self))
22 | }
23 |
24 | @inlinable @inline(__always)
25 | func roundTo512() -> Int {
26 | if self >= Int.max - 510 {
27 | return Int.max
28 | } else {
29 | return (self + 511) & (~511)
30 | }
31 | }
32 |
33 | /// Returns an integer with reversed order of bits.
34 | func reversed(bits count: Int) -> Int {
35 | var a = 1 << 0
36 | var b = 1 << (count - 1)
37 | var z = 0
38 | for i in Swift.stride(from: count - 1, to: -1, by: -2) {
39 | z |= (self >> i) & a
40 | z |= (self << i) & b
41 | a <<= 1
42 | b >>= 1
43 | }
44 | return z
45 | }
46 |
47 | }
48 |
49 | extension Date {
50 |
51 | private static let ntfsReferenceDate = DateComponents(calendar: Calendar(identifier: .iso8601),
52 | timeZone: TimeZone(abbreviation: "UTC"),
53 | year: 1601, month: 1, day: 1,
54 | hour: 0, minute: 0, second: 0).date!
55 |
56 | init(_ ntfsTime: UInt64) {
57 | self.init(timeInterval: TimeInterval(ntfsTime) / 10_000_000, since: .ntfsReferenceDate)
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/Sources/Common/FileSystemType.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | /**
9 | Represents the type of the file system on which an archive or container was created. File system determines the meaning
10 | of file attributes.
11 | */
12 | public enum FileSystemType {
13 | /// FAT filesystem.
14 | case fat
15 | /// Filesystem of older Macintosh systems.
16 | case macintosh
17 | /// NTFS.
18 | case ntfs
19 | /// Other/unknown file system.
20 | case other
21 | /**
22 | One of many file systems of UNIX-like OS.
23 |
24 | - Note: Modern macOS systems also fall into this category.
25 | */
26 | case unix
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/Deflate/Deflate+Lengths.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | // Deflate specific functions for generation of HuffmanLength arrays from different inputs.
9 | extension Deflate {
10 |
11 | /// - Note: Skips zero codeLengths.
12 | static func lengths(from orderedCodeLengths: [Int]) -> [CodeLength] {
13 | var lengths = [CodeLength]()
14 | for (i, codeLength) in orderedCodeLengths.enumerated() where codeLength > 0 {
15 | lengths.append(CodeLength(symbol: i, codeLength: codeLength))
16 | }
17 | return lengths
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/Deflate/DeflateError.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | /**
9 | Represents an error which happened during Deflate compression or decompression.
10 | It may indicate that either the data is damaged or it might not be compressed with Deflate at all.
11 | */
12 | public enum DeflateError: Error {
13 | /// Uncompressed block's `length` and `nlength` bytes isn't consistent with each other.
14 | case wrongUncompressedBlockLengths
15 | /// Unknown block type (not 0, 1 or 2).
16 | case wrongBlockType
17 | /// Decoded symbol was found in Huffman tree but is unknown.
18 | case wrongSymbol
19 | /// Symbol wasn't found in Huffman tree.
20 | case symbolNotFound
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/GZip/FileSystemType+Gzip.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | extension FileSystemType {
9 |
10 | init(_ gzipOS: UInt8) {
11 | switch gzipOS {
12 | case 0:
13 | self = .fat
14 | case 3:
15 | self = .unix
16 | case 7:
17 | self = .macintosh
18 | case 11:
19 | self = .ntfs
20 | default:
21 | self = .other
22 | }
23 | }
24 |
25 | var osTypeByte: UInt8 {
26 | switch self {
27 | case .fat:
28 | return 0
29 | case .unix:
30 | return 3
31 | case .macintosh:
32 | return 7
33 | case .ntfs:
34 | return 11
35 | default:
36 | return 255
37 | }
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/Sources/GZip/GzipError.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | /**
9 | Represents an error which happened while processing a GZip archive.
10 | It may indicate that either archive is damaged or it might not be GZip archive at all.
11 | */
12 | public enum GzipError: Error {
13 | /// First two bytes ('magic' number) of archive isn't 31 and 139.
14 | case wrongMagic
15 | /// Compression method used in archive is different from Deflate, which is the only supported one.
16 | case wrongCompressionMethod
17 | /**
18 | One of the reserved fields in archive has an unexpected value, which can also mean (apart from damaged archive),
19 | that archive uses a newer version of GZip format.
20 | */
21 | case wrongFlags
22 | /// Computed CRC of archive's header doesn't match the value stored in archive.
23 | case wrongHeaderCRC
24 | /**
25 | Computed checksum of uncompressed data doesn't match the value stored in the archive.
26 | Associated value of the error contains `GzipArchive.Member`s for all already decompressed data:
27 | + if `unarchive` function was called then associated array will have only one element,
28 | since this function always processes only first member of archive.
29 | + if `multiUnarchive` function was called then associated array will have an element
30 | for each already decompressed member, including the one with mismatching checksum.
31 | */
32 | case wrongCRC([GzipArchive.Member])
33 | /// Computed 'isize' didn't match the value stored in the archive.
34 | case wrongISize
35 | /// Either specified file name or comment cannot be encoded using ISO Latin-1 encoding.
36 | case cannotEncodeISOLatin1
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/GZip/GzipHeader+ExtraField.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | extension GzipHeader {
7 |
8 | /// Represents an extra field in the header of a GZip archive.
9 | public struct ExtraField {
10 |
11 | /// First byte of the extra field (subfield) ID.
12 | public let si1: UInt8
13 |
14 | /// Second byte of the extra field (subfield) ID.
15 | public let si2: UInt8
16 |
17 | /// Binary content of the extra field.
18 | public var bytes: [UInt8]
19 |
20 | /// Initializes an extra field with the specified extra field (subfield) ID bytes and its binary content.
21 | public init(_ si1: UInt8, _ si2: UInt8, _ bytes: [UInt8]) {
22 | self.si1 = si1
23 | self.si2 = si2
24 | self.bytes = bytes
25 | }
26 |
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/LZ4/XxHash32.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | infix operator <<<
9 |
10 | @inline(__always)
11 | fileprivate func <<< (num: UInt32, count: Int) -> UInt32 {
12 | // This implementation assumes without checking that `count` is in the 1...31 range.
13 | return (num << count) | (num >> (32 - count))
14 | }
15 |
16 | enum XxHash32 {
17 |
18 | private static let prime1: UInt32 = 0x9E3779B1
19 | private static let prime2: UInt32 = 0x85EBCA77
20 | private static let prime3: UInt32 = 0xC2B2AE3D
21 | private static let prime4: UInt32 = 0x27D4EB2F
22 | private static let prime5: UInt32 = 0x165667B1
23 |
24 | static func hash(data: Data, seed: UInt32 = 0) -> UInt32 {
25 | if data.count < 16 {
26 | return hashSmall(data, seed)
27 | } else {
28 | return hashBig(data, seed)
29 | }
30 | }
31 |
32 | @inline(__always)
33 | private static func hashSmall(_ data: Data, _ seed: UInt32) -> UInt32 {
34 | let acc = seed &+ prime5
35 | return finalize(data, data.startIndex, acc)
36 | }
37 |
38 | @inline(__always)
39 | private static func hashBig(_ data: Data, _ seed: UInt32) -> UInt32 {
40 | var accs = [seed &+ prime1 &+ prime2, seed &+ prime2, seed &+ 0, seed &- prime1]
41 | var i = data.startIndex
42 | while data.endIndex - i >= 16 { // Loop over stripes.
43 | for j in 0..<4 { // Loop over lanes.
44 | var lane = 0 as UInt32
45 | for k: UInt32 in 0..<4 {
46 | lane &+= UInt32(truncatingIfNeeded: data[i + j * 4 + k.toInt()]) << (k * 8)
47 | }
48 | accs[j] &+= lane &* prime2
49 | accs[j] = accs[j] <<< 13
50 | accs[j] &*= prime1
51 | }
52 | i += 16
53 | }
54 |
55 | let acc = (accs[0] <<< 1) &+ (accs[1] <<< 7) &+ (accs[2] <<< 12) &+ (accs[3] <<< 18)
56 | return finalize(data, i, acc)
57 | }
58 |
59 | private static func finalize(_ data: Data, _ ptr: Int, _ acc: UInt32) -> UInt32 {
60 | var acc = acc &+ UInt32(truncatingIfNeeded: data.count)
61 | var i = ptr
62 | while data.endIndex - i >= 4 {
63 | var lane = 0 as UInt32
64 | for k: UInt32 in 0..<4 {
65 | lane &+= UInt32(truncatingIfNeeded: data[i]) << (k * 8)
66 | i += 1
67 | }
68 | acc &+= lane &* prime3
69 | acc = (acc <<< 17) &* prime4
70 | }
71 | while data.endIndex - i >= 1 {
72 | let lane = UInt32(truncatingIfNeeded: data[i])
73 | i += 1
74 | acc &+= lane &* prime5
75 | acc = (acc <<< 11) &* prime1
76 | }
77 | acc ^= acc >> 15
78 | acc &*= prime2
79 | acc ^= acc >> 13
80 | acc &*= prime3
81 | acc ^= acc >> 16
82 | return acc
83 | }
84 |
85 | }
86 |
--------------------------------------------------------------------------------
/Sources/LZMA/LZMA.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import BitByteData
8 |
9 | /// Provides decompression function for LZMA.
10 | public class LZMA: DecompressionAlgorithm {
11 |
12 | /**
13 | Decompresses `data` using LZMA.
14 |
15 | - Note: It is assumed that the first nine bytes of `data` represent standard LZMA properties (so called "lc", "lp"
16 | and "pb"), dictionary size, and uncompressed size all encoded with standard encoding scheme of LZMA format.
17 |
18 | - Parameter data: Data compressed with LZMA.
19 |
20 | - Throws: `LZMAError` if unexpected byte (bit) sequence was encountered in `data`. It may indicate that either data
21 | is damaged or it might not be compressed with LZMA at all.
22 |
23 | - Returns: Decompressed data.
24 | */
25 | public static func decompress(data: Data) throws -> Data {
26 | // Valid LZMA "archive" must contain at least 13 bytes of data with properties and uncompressed size.
27 | guard data.count >= 13
28 | else { throw LZMAError.wrongProperties }
29 |
30 | let byteReader = LittleEndianByteReader(data: data)
31 | let properties = try LZMAProperties(byteReader)
32 | let uncompSize = byteReader.int(fromBytes: 8)
33 | return try decompress(byteReader, properties, uncompSize)
34 | }
35 |
36 | /**
37 | Decompresses `data` using LZMA with specified algorithm's `properties`, and, optionally, output's
38 | `uncompressedSize`. If `uncompressedSize` is nil, then `data` must contain finish marker.
39 |
40 | - Note: It is assumed that `data` begins immediately with LZMA compressed bytes with no LZMA properties at the
41 | beginning.
42 |
43 | - Warning: There is no validation performed for properties of `properties` argument. This API is intended to be
44 | used by advanced users.
45 |
46 | - Parameter data: Data compressed with LZMA.
47 | - Parameter properties: Properties of LZMA (such as lc, lp, etc.)
48 | - Parameter uncompressedSize: Size of uncompressed data; `nil` if it is unknown. In case of `nil`, finish marker
49 | must be present in `data`.
50 |
51 | - Throws: `LZMAError` if unexpected byte (bit) sequence was encountered in `data`. It may indicate that either data
52 | is damaged or it might not be compressed with LZMA at all.
53 |
54 | - Returns: Decompressed data.
55 | */
56 | public static func decompress(data: Data,
57 | properties: LZMAProperties,
58 | uncompressedSize: Int? = nil) throws -> Data {
59 | let byteReader = LittleEndianByteReader(data: data)
60 | return try decompress(byteReader, properties, uncompressedSize)
61 | }
62 |
63 | static func decompress(_ byteReader: LittleEndianByteReader,
64 | _ properties: LZMAProperties,
65 | _ uncompSize: Int?) throws -> Data {
66 | var decoder = LZMADecoder(byteReader)
67 | decoder.properties = properties
68 | decoder.resetStateAndDecoders()
69 | decoder.uncompressedSize = uncompSize ?? -1
70 |
71 | try decoder.decode()
72 | return Data(decoder.out)
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/Sources/LZMA/LZMABitTreeDecoder.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | /// Used to decode symbols that need several bits for storing.
9 | struct LZMABitTreeDecoder {
10 |
11 | var probs: [Int]
12 | let numBits: Int
13 |
14 | init(numBits: Int) {
15 | self.probs = Array(repeating: LZMAConstants.probInitValue,
16 | count: 1 << numBits)
17 | self.numBits = numBits
18 | }
19 |
20 | mutating func decode(with rangeDecoder: inout LZMARangeDecoder) -> Int {
21 | var m = 1
22 | for _ in 0.. Int {
29 | return LZMABitTreeDecoder.bitTreeReverseDecode(probs: &self.probs,
30 | startIndex: 0,
31 | bits: self.numBits, &rangeDecoder)
32 | }
33 |
34 | static func bitTreeReverseDecode(probs: inout [Int], startIndex: Int, bits: Int,
35 | _ rangeDecoder: inout LZMARangeDecoder) -> Int {
36 | var m = 1
37 | var symbol = 0
38 | for i in 0..> 1)
21 | static let matchMinLen = 2
22 | // LZMAConstants.numStates << LZMAConstants.numPosBitsMax = 192
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/LZMA/LZMAError.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | /**
9 | Represents an error which happened during LZMA decompression.
10 | It may indicate that either data is damaged or it might not be compressed with LZMA at all.
11 | */
12 | public enum LZMAError: Error {
13 | /// Properties' byte is greater than 225.
14 | case wrongProperties
15 | /// Unable to initialize RanderDecorer.
16 | case rangeDecoderInitError
17 | /// Size of uncompressed data hit specified limit in the middle of decoding.
18 | case exceededUncompressedSize
19 | /// Unable to perfrom repeat-distance decoding because there is nothing to repeat.
20 | case windowIsEmpty
21 | /// End of stream marker is reached, but range decoder is in incorrect state.
22 | case rangeDecoderFinishError
23 | /// The number of bytes to repeat is greater than the amount bytes that is left to decode.
24 | case repeatWillExceed
25 | /// The amount of already decoded bytes is smaller than repeat length.
26 | case notEnoughToRepeat
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/LZMA/LZMALenDecoder.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | struct LZMALenDecoder {
9 |
10 | private var choice: Int = LZMAConstants.probInitValue
11 | private var choice2: Int = LZMAConstants.probInitValue
12 | private var lowCoder: [LZMABitTreeDecoder] = []
13 | private var midCoder: [LZMABitTreeDecoder] = []
14 | private var highCoder: LZMABitTreeDecoder
15 |
16 | init() {
17 | self.highCoder = LZMABitTreeDecoder(numBits: 8)
18 | for _ in 0..<(1 << LZMAConstants.numPosBitsMax) {
19 | self.lowCoder.append(LZMABitTreeDecoder(numBits: 3))
20 | self.midCoder.append(LZMABitTreeDecoder(numBits: 3))
21 | }
22 | }
23 |
24 | /// Decodes zero-based match length.
25 | mutating func decode(with rangeDecoder: inout LZMARangeDecoder, posState: Int) -> Int {
26 | // There can be one of three options.
27 | // We need one or two bits to find out which decoding scheme to use.
28 | // `choice` is used to decode first bit.
29 | // `choice2` is used to decode second bit.
30 | // If binary sequence starts with 0 then:
31 | if rangeDecoder.decode(bitWithProb: &self.choice) == 0 {
32 | return self.lowCoder[posState].decode(with: &rangeDecoder)
33 | }
34 | // If binary sequence starts with 1 0 then:
35 | if rangeDecoder.decode(bitWithProb: &self.choice2) == 0 {
36 | return 8 + self.midCoder[posState].decode(with: &rangeDecoder)
37 | }
38 | // If binary sequence starts with 1 1 then:
39 | return 16 + self.highCoder.decode(with: &rangeDecoder)
40 | }
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/Sources/LZMA/LZMAProperties.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import BitByteData
7 |
8 | /// Properties of LZMA. This API is intended to be used by advanced users.
9 | public struct LZMAProperties {
10 |
11 | /// Number of bits used for the literal encoding context. Default value is 3.
12 | public var lc: Int = 3
13 |
14 | /// Number of bits to include in "literal position state". Default value is 0.
15 | public var lp: Int = 0
16 |
17 | /// Number of bits to include in "position state". Default value is 2.
18 | public var pb: Int = 2
19 |
20 | /**
21 | Size of the dictionary. Default value is 1 << 24.
22 |
23 | - Note: Dictionary size cannot be less than 4096. In case of attempt to set it to the value less than 4096 it will
24 | be automatically set to 4096 instead.
25 | */
26 | public var dictionarySize: Int = 1 << 24 {
27 | didSet {
28 | if dictionarySize < 1 << 12 {
29 | dictionarySize = 1 << 12
30 | }
31 | }
32 | }
33 |
34 | /**
35 | Initializes LZMA properties with values of lc, lp, pb, and dictionary size.
36 |
37 | - Note: It is not tested if values of lc, lp, and pb are valid.
38 | */
39 | public init(lc: Int, lp: Int, pb: Int, dictionarySize: Int) {
40 | self.lc = lc
41 | self.lp = lp
42 | self.pb = pb
43 | self.dictionarySize = dictionarySize
44 | }
45 |
46 | /// Initializes LZMA properties with default values of lc, lp, pb, and dictionary size.
47 | public init() { }
48 |
49 | init(lzmaByte: UInt8, _ dictSize: Int) throws {
50 | guard lzmaByte < 9 * 5 * 5
51 | else { throw LZMAError.wrongProperties }
52 |
53 | let intByte = lzmaByte.toInt()
54 |
55 | self.lc = intByte % 9
56 | self.pb = (intByte / 9) / 5
57 | self.lp = (intByte / 9) % 5
58 |
59 | self.dictionarySize = dictSize
60 | }
61 |
62 | init(_ byteReader: LittleEndianByteReader) throws {
63 | try self.init(lzmaByte: byteReader.byte(), byteReader.int(fromBytes: 4))
64 | }
65 |
66 | }
67 |
--------------------------------------------------------------------------------
/Sources/LZMA/LZMARangeDecoder.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import BitByteData
8 |
9 | struct LZMARangeDecoder {
10 |
11 | private let byteReader: LittleEndianByteReader
12 |
13 | private var range = 0xFFFFFFFF as UInt32
14 | private var code = 0 as UInt32
15 | private(set) var isCorrupted = false
16 |
17 | var isFinishedOK: Bool {
18 | return self.code == 0
19 | }
20 |
21 | init(_ byteReader: LittleEndianByteReader) throws {
22 | // To initialize range decoder at least 5 bytes are necessary.
23 | guard byteReader.bytesLeft >= 5
24 | else { throw LZMAError.rangeDecoderInitError }
25 |
26 | self.byteReader = byteReader
27 |
28 | let byte = self.byteReader.byte()
29 | self.code = self.byteReader.uint32().byteSwapped
30 | guard byte == 0 && self.code != self.range
31 | else { throw LZMAError.rangeDecoderInitError }
32 | }
33 |
34 | init() {
35 | self.byteReader = LittleEndianByteReader(data: Data())
36 | }
37 |
38 | /// `range` property cannot be smaller than `(1 << 24)`. This function keeps it bigger.
39 | mutating func normalize() {
40 | if self.range < LZMAConstants.topValue {
41 | self.range <<= 8
42 | self.code = (self.code << 8) | UInt32(byteReader.byte())
43 | }
44 | }
45 |
46 | /// Decodes sequence of direct bits (binary symbols with fixed and equal probabilities).
47 | mutating func decode(directBits: Int) -> Int {
48 | var res: UInt32 = 0
49 | var count = directBits
50 | repeat {
51 | self.range >>= 1
52 | self.code = self.code &- self.range
53 | let t = 0 &- (self.code >> 31)
54 | self.code = self.code &+ (self.range & t)
55 |
56 | if self.code == self.range {
57 | self.isCorrupted = true
58 | }
59 |
60 | self.normalize()
61 |
62 | res <<= 1
63 | res = res &+ (t &+ 1)
64 | count -= 1
65 | } while count > 0
66 | return res.toInt()
67 | }
68 |
69 | /// Decodes binary symbol (bit) with predicted (estimated) probability.
70 | mutating func decode(bitWithProb prob: inout Int) -> Int {
71 | let bound = (self.range >> UInt32(LZMAConstants.numBitModelTotalBits)) * UInt32(prob)
72 | let symbol: Int
73 | if self.code < bound {
74 | prob += ((1 << LZMAConstants.numBitModelTotalBits) - prob) >> LZMAConstants.numMoveBits
75 | self.range = bound
76 | symbol = 0
77 | } else {
78 | prob -= prob >> LZMAConstants.numMoveBits
79 | self.code -= bound
80 | self.range -= bound
81 | symbol = 1
82 | }
83 | self.normalize()
84 | return symbol
85 | }
86 |
87 | }
88 |
--------------------------------------------------------------------------------
/Sources/LZMA2/LZMA2.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import BitByteData
8 |
9 | /// Provides decompression function for LZMA2 algorithm.
10 | public class LZMA2: DecompressionAlgorithm {
11 |
12 | /**
13 | Decompresses `data` using LZMA2 algortihm.
14 |
15 | - Note: It is assumed that the first byte of `data` is a dictionary size encoded with standard encoding scheme of
16 | LZMA2 format.
17 |
18 | - Parameter data: Data compressed with LZMA2.
19 |
20 | - Throws: `LZMAError` or `LZMA2Error` if unexpected byte (bit) sequence was encountered in `data`.
21 | It may indicate that either data is damaged or it might not be compressed with LZMA2 at all.
22 |
23 | - Returns: Decompressed data.
24 | */
25 | public static func decompress(data: Data) throws -> Data {
26 | let byteReader = LittleEndianByteReader(data: data)
27 | guard byteReader.bytesLeft >= 1
28 | else { throw LZMAError.rangeDecoderInitError }
29 | return try decompress(byteReader, byteReader.byte())
30 | }
31 |
32 | static func decompress(_ byteReader: LittleEndianByteReader, _ dictSizeByte: UInt8) throws -> Data {
33 | var decoder = try LZMA2Decoder(byteReader, dictSizeByte)
34 | try decoder.decode()
35 | return Data(decoder.out)
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/LZMA2/LZMA2Decoder.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import BitByteData
8 |
9 | struct LZMA2Decoder {
10 |
11 | private let byteReader: LittleEndianByteReader
12 | private var decoder: LZMADecoder
13 |
14 | var out: [UInt8] {
15 | return self.decoder.out
16 | }
17 |
18 | init(_ byteReader: LittleEndianByteReader, _ dictSizeByte: UInt8) throws {
19 | self.byteReader = byteReader
20 | self.decoder = LZMADecoder(byteReader)
21 |
22 | guard dictSizeByte & 0xC0 == 0
23 | else { throw LZMA2Error.wrongDictionarySize }
24 | let bits = (dictSizeByte & 0x3F).toInt()
25 | guard bits < 40
26 | else { throw LZMA2Error.wrongDictionarySize }
27 |
28 | let dictSize = bits == 40 ? UInt32.max :
29 | (UInt32(truncatingIfNeeded: 2 | (bits & 1)) << UInt32(truncatingIfNeeded: bits / 2 + 11))
30 |
31 | self.decoder.properties.dictionarySize = dictSize.toInt()
32 | }
33 |
34 | /// Main LZMA2 decoder function.
35 | mutating func decode() throws {
36 | mainLoop: while true {
37 | let controlByte = byteReader.byte()
38 | switch controlByte {
39 | case 0:
40 | break mainLoop
41 | case 1:
42 | self.decoder.resetDictionary()
43 | self.decodeUncompressed()
44 | case 2:
45 | self.decodeUncompressed()
46 | case 3...0x7F:
47 | throw LZMA2Error.wrongControlByte
48 | case 0x80...0xFF:
49 | try self.dispatch(controlByte)
50 | default:
51 | fatalError("Incorrect control byte.") // This statement is never executed.
52 | }
53 | }
54 | }
55 |
56 | /// Function which dispatches LZMA2 decoding process based on `controlByte`.
57 | private mutating func dispatch(_ controlByte: UInt8) throws {
58 | let uncompressedSizeBits = controlByte & 0x1F
59 | let reset = (controlByte & 0x60) >> 5
60 | let unpackSize = (uncompressedSizeBits.toInt() << 16) +
61 | self.byteReader.byte().toInt() << 8 + self.byteReader.byte().toInt() + 1
62 | let compressedSize = self.byteReader.byte().toInt() << 8 + self.byteReader.byte().toInt() + 1
63 | switch reset {
64 | case 0:
65 | break
66 | case 1:
67 | self.decoder.resetStateAndDecoders()
68 | case 2:
69 | try self.updateProperties()
70 | case 3:
71 | try self.updateProperties()
72 | self.decoder.resetDictionary()
73 | default:
74 | throw LZMA2Error.wrongReset
75 | }
76 | self.decoder.uncompressedSize = unpackSize
77 | let outStartIndex = self.decoder.out.count
78 | let inStartIndex = self.byteReader.offset
79 | try self.decoder.decode()
80 | guard unpackSize == self.decoder.out.count - outStartIndex &&
81 | self.byteReader.offset - inStartIndex == compressedSize
82 | else { throw LZMA2Error.wrongSizes }
83 | }
84 |
85 | private mutating func decodeUncompressed() {
86 | let dataSize = self.byteReader.byte().toInt() << 8 + self.byteReader.byte().toInt() + 1
87 | for _ in 0..
2 |
3 |
4 |
5 | NSPrivacyTracking
6 |
7 | NSPrivacyTrackingDomains
8 |
9 | NSPrivacyCollectedDataTypes
10 |
11 | NSPrivacyAccessedAPITypes
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/Sources/TAR/ContainerEntryType+Tar.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | extension ContainerEntryType {
9 |
10 | init(_ fileTypeIndicator: UInt8) {
11 | switch fileTypeIndicator {
12 | case 0, 48: // "0"
13 | self = .regular
14 | case 49: // "1"
15 | self = .hardLink
16 | case 50: // "2"
17 | self = .symbolicLink
18 | case 51: // "3"
19 | self = .characterSpecial
20 | case 52: // "4"
21 | self = .blockSpecial
22 | case 53: // "5"
23 | self = .directory
24 | case 54: // "6"
25 | self = .fifo
26 | case 55: // "7"
27 | self = .contiguous
28 | default:
29 | self = .unknown
30 | }
31 | }
32 |
33 | var fileTypeIndicator: UInt8 {
34 | switch self {
35 | case .regular:
36 | return 48
37 | case .hardLink:
38 | return 49
39 | case .symbolicLink:
40 | return 50
41 | case .characterSpecial:
42 | return 51
43 | case .blockSpecial:
44 | return 52
45 | case .directory:
46 | return 53
47 | case .fifo:
48 | return 54
49 | case .contiguous:
50 | return 55
51 | case .socket:
52 | return 0
53 | case .unknown:
54 | return 0
55 | }
56 | }
57 |
58 | }
59 |
--------------------------------------------------------------------------------
/Sources/TAR/Data+Tar.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | extension Data {
9 |
10 | @inline(__always)
11 | mutating func appendAsTarBlock(_ data: Data) {
12 | self.append(data)
13 | let paddingSize = data.count.roundTo512() - data.count
14 | self.append(Data(count: paddingSize))
15 | }
16 |
17 | mutating func append(tarInt value: Int?, maxLength: Int) {
18 | guard var value = value else {
19 | // No value; fill field with NULLs.
20 | self.append(Data(count: maxLength))
21 | return
22 | }
23 |
24 | let maxOctalValue = (1 << (maxLength * 3)) - 1
25 | guard value > maxOctalValue || value < 0 else {
26 | // Normal octal encoding.
27 | self.append(Data(String(value, radix: 8).utf8).zeroPad(maxLength))
28 | return
29 | }
30 |
31 | // Base-256 encoding.
32 | // As long as we have at least 8 bytes for our value, conversion to base-256 will always succeed, since (64-bit)
33 | // Int.max neatly fits into 8 bytes of 256-base encoding.
34 | assert(maxLength >= 8 && Int.bitWidth <= 64)
35 | var buffer = Array(repeating: 0 as UInt8, count: maxLength)
36 | for i in stride(from: maxLength - 1, to: 0, by: -1) {
37 | buffer[i] = UInt8(truncatingIfNeeded: value & 0xFF)
38 | value >>= 8
39 | }
40 | buffer[0] |= 0x80 // Highest bit indicates base-256 encoding.
41 | self.append(Data(buffer))
42 | }
43 |
44 | mutating func append(tarString string: String?, maxLength: Int) {
45 | guard let string = string else {
46 | // No value; fill field with NULLs.
47 | self.append(Data(count: maxLength))
48 | return
49 | }
50 | self.append(Data(string.utf8).zeroPad(maxLength))
51 | }
52 |
53 | /// This should work in the same way as `String.padding(toLength: length, withPad: "\0", startingAt: 0)`.
54 | @inline(__always)
55 | private func zeroPad(_ length: Int) -> Data {
56 | var out = length < self.count ? self.prefix(upTo: length) : self
57 | out.append(Data(count: length - out.count))
58 | return out
59 | }
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/Sources/TAR/LittleEndianByteReader+Tar.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import BitByteData
8 |
9 | extension LittleEndianByteReader {
10 |
11 | /**
12 | Reads a `String` field from TAR container. The end of the field is defined by either:
13 | 1. NULL character (thus CString in the name of the function).
14 | 2. Reaching specified maximum length.
15 |
16 | Strings are encoded in TAR using ASCII encoding. We are treating them as UTF-8 encoded instead since UTF-8 is
17 | backwards compatible with ASCII.
18 |
19 | We use `String(cString:)` initalizer because TAR's NULL-ending ASCII fields are basically CStrings (especially,
20 | since we are treating them as UTF-8 strings). As a bonus, this initializer is not failable: it replaces unparsable
21 | as UTF-8 sequences of bytes with UTF-8 Replacement Character, so we don't need to throw any error.
22 | */
23 | func tarCString(maxLength: Int) -> String {
24 | var buffer = self.bytes(count: maxLength)
25 | if buffer.last != 0 {
26 | buffer.append(0)
27 | }
28 | return buffer.withUnsafeBufferPointer { String(cString: $0.baseAddress!) }
29 | }
30 |
31 | /**
32 | Reads an `Int` field from TAR container. The end of the field is defined by either:
33 | 1. NULL or SPACE (in containers created by certain old implementations) character.
34 | 2. Reaching specified maximum length.
35 |
36 | Integers are encoded in TAR as ASCII text. We are treating them as UTF-8 encoded strings since UTF-8 is backwards
37 | compatible with ASCII.
38 | */
39 | func tarInt(maxLength: Int) -> Int? {
40 | guard maxLength > 0
41 | else { return nil }
42 |
43 | var buffer = [UInt8]()
44 | buffer.reserveCapacity(maxLength)
45 |
46 | let firstByte = self.byte()
47 | self.offset -= 1
48 |
49 | if firstByte & 0x80 != 0 { // Base-256 encoding; used for big numeric fields.
50 | buffer = self.bytes(count: maxLength)
51 | /// Inversion mask for handling negative numbers.
52 | let invMask = buffer[0] & 0x40 != 0 ? 0xFF : 0x00
53 | var result = 0
54 | for i in 0..> (Int.bitWidth - 8) > 0 {
60 | return nil // Integer overflow
61 | }
62 | result = (result << 8) | byte
63 | }
64 | if result >> (Int.bitWidth - 1) > 0 {
65 | return nil // Integer overflow
66 | }
67 | return invMask == 0xFF ? ~result : result
68 | }
69 |
70 | // Normal, octal encoding.
71 | let startOffset = self.offset
72 |
73 | // Skip leading NULLs and whitespaces (used by some implementations).
74 | var byte: UInt8
75 | var actualStartIndex = 0
76 | repeat {
77 | byte = self.byte()
78 | actualStartIndex += 1
79 | } while (byte == 0 || byte == 0x20) && (actualStartIndex < maxLength)
80 | self.offset -= 1
81 |
82 | for _ in actualStartIndex.. ParsingResult {
36 | if reader.isFinished {
37 | return .finished
38 | } else if reader.bytesLeft >= 1024 && reader.data[reader.offset..= 0)
49 | let dataStartIndex = header.blockStartIndex + 512
50 |
51 | if case .special(let specialEntryType) = header.type {
52 | switch specialEntryType {
53 | case .globalExtendedHeader:
54 | let dataEndIndex = dataStartIndex + header.size
55 | lastGlobalExtendedHeader = try TarExtendedHeader(reader.data[dataStartIndex.. Int {
12 | var i = 1
13 | var result = self.byte().toInt()
14 | if result <= 127 {
15 | return result
16 | }
17 | result &= 0x7F
18 | while true {
19 | let byte = self.byte()
20 | if i >= 9 || byte == 0x00 {
21 | throw XZError.multiByteIntegerError
22 | }
23 | result += (byte.toInt() & 0x7F) << (7 * i)
24 | i += 1
25 |
26 | if byte & 0x80 == 0 {
27 | break
28 | }
29 | }
30 | return result
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/XZ/XZBlock.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import BitByteData
8 |
9 | struct XZBlock {
10 |
11 | let data: Data
12 | let unpaddedSize: Int
13 |
14 | var uncompressedSize: Int {
15 | return data.count
16 | }
17 |
18 | init(_ blockHeaderSize: UInt8, _ byteReader: LittleEndianByteReader, _ checkSize: Int) throws {
19 | let blockHeaderStartIndex = byteReader.offset - 1
20 | let realBlockHeaderSize = (blockHeaderSize.toInt() + 1) * 4
21 |
22 | let blockFlags = byteReader.byte()
23 | /**
24 | Bit values 00, 01, 10, 11 indicate filters number from 1 to 4,
25 | so we actually need to add 1 to get filters' number.
26 | */
27 | let filtersCount = blockFlags & 0x03 + 1
28 | guard blockFlags & 0x3C == 0
29 | else { throw XZError.wrongField }
30 |
31 | /// Should match size of compressed data.
32 | let compressedSize = blockFlags & 0x40 != 0 ? try byteReader.multiByteDecode() : -1
33 |
34 | /// Should match the size of data after decompression.
35 | let uncompressedSize = blockFlags & 0x80 != 0 ? try byteReader.multiByteDecode() : -1
36 |
37 | var filters: [(LittleEndianByteReader) throws -> Data] = []
38 | for _ in 0.. 8 {
116 | byteReader.offset += uidSize
117 | } else {
118 | self.uid = byteReader.int(fromBytes: uidSize)
119 | }
120 |
121 | let gidSize = byteReader.byte().toInt()
122 | if gidSize > 8 {
123 | byteReader.offset += gidSize
124 | } else {
125 | self.gid = byteReader.int(fromBytes: gidSize)
126 | }
127 | }
128 |
129 | }
130 |
--------------------------------------------------------------------------------
/Sources/ZIP/CompressionMethod+Zip.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | extension CompressionMethod {
9 |
10 | init(_ compression: UInt16) {
11 | switch compression {
12 | case 0:
13 | self = .copy
14 | case 8:
15 | self = .deflate
16 | case 12:
17 | self = .bzip2
18 | case 14:
19 | self = .lzma
20 | default:
21 | self = .other
22 | }
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/ZIP/FileSystemType+Zip.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | extension FileSystemType {
9 |
10 | init(_ versionMadeBy: UInt16) {
11 | switch (versionMadeBy & 0xFF00) >> 8 {
12 | case 0, 14:
13 | self = .fat
14 | case 3:
15 | self = .unix
16 | case 7, 19:
17 | self = .macintosh
18 | case 10:
19 | self = .ntfs
20 | default:
21 | self = .other
22 | }
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/ZIP/LittleEndianByteReader+Zip.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import BitByteData
8 |
9 | extension LittleEndianByteReader {
10 |
11 | func zipString(_ length: Int, _ useUtf8: Bool) -> String? {
12 | guard length > 0
13 | else { return "" }
14 | let stringData = self.data[self.offset.. Bool {
45 | // UTF-8 can have BOM.
46 | if self.count >= 3 {
47 | if self[self.startIndex] == 0xEF && self[self.startIndex + 1] == 0xBB && self[self.startIndex + 2] == 0xBF {
48 | return true
49 | }
50 | }
51 |
52 | var index = self.startIndex
53 | while index < self.endIndex {
54 | let byte = self[index]
55 | if byte <= 0x7F { // This simple byte can exist both in CP437 and UTF-8.
56 | index += 1
57 | continue
58 | }
59 |
60 | // Otherwise, it has to be correct code sequence in case of UTF-8.
61 | // If code sequence is incorrect, then it is CP437.
62 | let codeLength: Int
63 | if byte >= 0xC2 && byte <= 0xDF {
64 | codeLength = 2
65 | } else if byte >= 0xE0 && byte <= 0xEF {
66 | codeLength = 3
67 | } else if byte >= 0xF0 && byte <= 0xF4 {
68 | codeLength = 4
69 | } else {
70 | return false
71 | }
72 |
73 | if index + codeLength - 1 >= self.endIndex {
74 | return false
75 | }
76 |
77 | for i in 1..> 11 == 0x1B {
88 | return false
89 | }
90 | } else if codeLength == 4 {
91 | let ch = (UInt32(truncatingIfNeeded: self[index]) & 0x07) << 18 +
92 | (UInt32(truncatingIfNeeded: self[index + 1]) & 0x3F) << 12 +
93 | (UInt32(truncatingIfNeeded: self[index + 2]) & 0x3F) << 6 +
94 | UInt32(truncatingIfNeeded: self[index + 3]) & 0x3F
95 | if ch < 0x10000 || ch > 0x10FFFF {
96 | return false
97 | }
98 | }
99 | return true
100 | }
101 | // All bytes were in range 0...0x7F, which can be both in CP437 and UTF-8.
102 | // We solve this ambiguity in favor of CP437.
103 | return false
104 | }
105 |
106 | }
107 |
--------------------------------------------------------------------------------
/Sources/ZIP/ZipEndOfCentralDirectory.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import BitByteData
8 |
9 | struct ZipEndOfCentralDirectory {
10 |
11 | /// Number of the current disk.
12 | private(set) var currentDiskNumber: UInt32
13 |
14 | private(set) var cdEntries: UInt64
15 | private(set) var cdOffset: UInt64
16 |
17 | // There are two fields in the EndOfCD that aren't currently used anywhere except in the initalizer.
18 | // /// Number of the disk with the start of CD.
19 | // private var cdDiskNumber: UInt32
20 | // private var cdSize: UInt64
21 |
22 | init(_ byteReader: LittleEndianByteReader) throws {
23 | /// Indicates if Zip64 records should be present.
24 | var zip64RecordExists = false
25 |
26 | self.currentDiskNumber = byteReader.uint32(fromBytes: 2)
27 | var cdDiskNumber = byteReader.uint32(fromBytes: 2)
28 | guard self.currentDiskNumber == cdDiskNumber
29 | else { throw ZipError.multiVolumesNotSupported }
30 |
31 | /// Number of CD entries on the current disk.
32 | var cdEntriesCurrentDisk = byteReader.uint64(fromBytes: 2)
33 | /// Total number of CD entries.
34 | self.cdEntries = byteReader.uint64(fromBytes: 2)
35 | guard cdEntries == cdEntriesCurrentDisk
36 | else { throw ZipError.multiVolumesNotSupported }
37 |
38 | /// Size of Central Directory.
39 | var cdSize = byteReader.uint64(fromBytes: 4)
40 | /// Offset to the start of Central Directory.
41 | self.cdOffset = byteReader.uint64(fromBytes: 4)
42 |
43 | // There is also a .ZIP file comment, but we don't need it.
44 | // Here's how it can be processed:
45 | // let zipCommentLength = byteReader.int(fromBytes: 2)
46 | // let zipComment = String(data: Data(byteReader.bytes(count: zipCommentLength)),
47 | // encoding: .utf8)
48 |
49 | // Check if zip64 records are present.
50 | if self.currentDiskNumber == 0xFFFF || cdDiskNumber == 0xFFFF ||
51 | cdEntriesCurrentDisk == 0xFFFF || self.cdEntries == 0xFFFF ||
52 | cdSize == 0xFFFFFFFF || self.cdOffset == 0xFFFFFFFF {
53 | zip64RecordExists = true
54 | }
55 |
56 | if zip64RecordExists { // We need to find Zip64 end of CD locator.
57 | // Back to start of end of CD record.
58 | byteReader.offset -= 20
59 | // Zip64 locator takes exactly 20 bytes.
60 | byteReader.offset -= 20
61 |
62 | // Check signature.
63 | guard byteReader.uint32() == 0x07064b50
64 | else { throw ZipError.wrongSignature }
65 |
66 | let zip64CDStartDisk = byteReader.uint32()
67 | guard self.currentDiskNumber == zip64CDStartDisk
68 | else { throw ZipError.multiVolumesNotSupported }
69 |
70 | let zip64CDEndOffset = byteReader.int(fromBytes: 8)
71 | let totalDisks = byteReader.uint32()
72 | guard totalDisks == 1
73 | else { throw ZipError.multiVolumesNotSupported }
74 |
75 | // Now we need to move to Zip64 End of CD.
76 | byteReader.offset = zip64CDEndOffset
77 |
78 | // Check signature.
79 | guard byteReader.uint32() == 0x06064b50
80 | else { throw ZipError.wrongSignature }
81 |
82 | // Following 8 bytes are size of end of zip64 CD, but we don't need it.
83 | _ = byteReader.uint64()
84 |
85 | // Next two bytes are version of compressor, but we don't need it.
86 | _ = byteReader.uint16()
87 | let versionNeeded = byteReader.uint16()
88 | guard versionNeeded & 0xFF <= 63
89 | else { throw ZipError.wrongVersion }
90 |
91 | // Update values read from basic End of CD with the ones from Zip64 End of CD.
92 | self.currentDiskNumber = byteReader.uint32()
93 | cdDiskNumber = byteReader.uint32()
94 | guard currentDiskNumber == cdDiskNumber
95 | else { throw ZipError.multiVolumesNotSupported }
96 |
97 | cdEntriesCurrentDisk = byteReader.uint64()
98 | self.cdEntries = byteReader.uint64()
99 | guard cdEntries == cdEntriesCurrentDisk
100 | else { throw ZipError.multiVolumesNotSupported }
101 |
102 | cdSize = byteReader.uint64()
103 | self.cdOffset = byteReader.uint64()
104 |
105 | // Then, there might be 'zip64 extensible data sector' with 'special purpose data'.
106 | // But we don't need them currently, so let's skip them.
107 |
108 | // To find the size of these data:
109 | // let specialPurposeDataSize = zip64EndCDSize - 56
110 | }
111 | }
112 |
113 | }
114 |
--------------------------------------------------------------------------------
/Sources/ZIP/ZipEntry.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | /// Represents an entry from the ZIP container.
9 | public struct ZipEntry: ContainerEntry {
10 |
11 | public let info: ZipEntryInfo
12 |
13 | public let data: Data?
14 |
15 | init(_ entryInfo: ZipEntryInfo, _ data: Data?) {
16 | self.info = entryInfo
17 | self.data = data
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/ZIP/ZipEntryInfoHelper.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import BitByteData
8 |
9 | /**
10 | The purpose of this struct is to accompany `ZipEntryInfo` instances while processing a `ZipContainer` and to store
11 | information which is necessary for reading entry's data later.
12 | */
13 | struct ZipEntryInfoHelper {
14 |
15 | let entryInfo: ZipEntryInfo
16 |
17 | let hasDataDescriptor: Bool
18 | let zip64FieldsArePresent: Bool
19 | let nextCdEntryOffset: Int
20 | let dataOffset: Int
21 | let compSize: UInt64
22 | let uncompSize: UInt64
23 |
24 | init(_ byteReader: LittleEndianByteReader, _ currentDiskNumber: UInt32) throws {
25 | // Read Central Directory entry.
26 | let cdEntry = try ZipCentralDirectoryEntry(byteReader)
27 |
28 | // Move to the location of Local Header.
29 | byteReader.offset = cdEntry.localHeaderOffset.toInt()
30 | // Read Local Header.
31 | let localHeader = try ZipLocalHeader(byteReader)
32 | try localHeader.validate(with: cdEntry, currentDiskNumber)
33 |
34 | // If file has data descriptor, then some properties are only present in CD entry.
35 | self.hasDataDescriptor = localHeader.generalPurposeBitFlags & 0x08 != 0
36 |
37 | self.entryInfo = ZipEntryInfo(byteReader, cdEntry, localHeader, hasDataDescriptor)
38 |
39 | // Save some properties from CD entry and Local Header.
40 | self.zip64FieldsArePresent = localHeader.zip64FieldsArePresent
41 | self.nextCdEntryOffset = cdEntry.nextEntryOffset
42 | self.dataOffset = localHeader.dataOffset
43 | self.compSize = hasDataDescriptor ? cdEntry.compSize : localHeader.compSize
44 | self.uncompSize = hasDataDescriptor ? cdEntry.uncompSize : localHeader.uncompSize
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/Sources/ZIP/ZipError.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | /**
9 | Represents an error which happened while processing a ZIP container.
10 | It may indicate that either container is damaged or it might not be ZIP container at all.
11 | */
12 | public enum ZipError: Error {
13 | /// End of Central Directoty record wasn't found.
14 | case notFoundCentralDirectoryEnd
15 | /// Wrong signature of one of container's structures.
16 | case wrongSignature
17 | /// Wrong either compressed or uncompressed size of a container's entry.
18 | case wrongSize
19 | /// Version needed to process container is unsupported.
20 | case wrongVersion
21 | /// Container is either spanned or consists of several volumes. These features aren't supported.
22 | case multiVolumesNotSupported
23 | /// Entry or record is encrypted. This feature isn't supported.
24 | case encryptionNotSupported
25 | /// Entry contains patched data. This feature isn't supported.
26 | case patchingNotSupported
27 | /// Entry is compressed using unsupported compression method.
28 | case compressionNotSupported
29 | /// Local header of an entry is inconsistent with Central Directory.
30 | case wrongLocalHeader
31 | /**
32 | Computed checksum of entry's data doesn't match the value stored in the archive.
33 | Associated value of the error contains `ZipEntry` objects for all already processed entries:
34 | */
35 | case wrongCRC([ZipEntry])
36 | /// Either entry's comment or file name cannot be processed using UTF-8 encoding.
37 | case wrongTextField
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/ZIP/ZipExtraField.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import BitByteData
7 |
8 | /// A type that represents an extra field from a ZIP container.
9 | public protocol ZipExtraField {
10 |
11 | /**
12 | ID of extra field. Must be equal to the key of extra field in `ZipContainer.customExtraFields` dictionary and
13 | instance `id` property
14 | */
15 | static var id: UInt16 { get }
16 |
17 | /// Location of extra field. Must be equal to the value of `location` argument of `init?(_:_:location:)`.
18 | var location: ZipExtraFieldLocation { get }
19 |
20 | /// Size of extra field's data. Must be equal to the value of the second argument of `init?(_:_:location:)`.
21 | var size: Int { get }
22 |
23 | /**
24 | Creates an extra field instance reading `size` amount of data from `byteReader`.
25 |
26 | It is guaranteed that the offset of `byteReader` is equal to the position right after extra field header ID and
27 | length of extra field data. It is also guaranteed that header ID matches conforming type's static `id` property.
28 |
29 | Following conditions are checked after execution of this initializer. Failure to satisfy them in conforming type
30 | will result in runtime error.
31 |
32 | - Postcondition: `location` property of a created instance must be equal to the `location` argument.
33 | - Postcondition: `size` property of a created instance must be equal to the second argument.
34 | - Postcondition: exactly `size` amount of bytes must be read by initializer from `byteReader`.
35 | */
36 | init?(_ byteReader: LittleEndianByteReader, _ size: Int, location: ZipExtraFieldLocation)
37 |
38 | }
39 |
40 | extension ZipExtraField {
41 |
42 | /**
43 | ID of extra field. Must be equal to the key of extra field in `ZipContainer.customExtraFields` dictionary and
44 | static `id` property
45 | */
46 | public var id: UInt16 {
47 | return Self.id
48 | }
49 |
50 | }
51 |
52 | /// Location of ZIP extra field inside a container.
53 | public enum ZipExtraFieldLocation {
54 | /// ZIP extra field is located in container's Central Directory.
55 | case centralDirectory
56 | /// ZIP extra field is located in one of container's Local Headers.
57 | case localHeader
58 | }
59 |
--------------------------------------------------------------------------------
/Sources/Zlib/ZlibArchive.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import BitByteData
8 |
9 | /// Provides unarchive and archive functions for Zlib archives.
10 | public class ZlibArchive: Archive {
11 |
12 | /**
13 | Unarchives Zlib archive.
14 |
15 | - Note: This function is specification compliant.
16 |
17 | - Parameter archive: Data archived with Zlib.
18 |
19 | - Throws: `DeflateError` or `ZlibError` depending on the type of the problem.
20 | It may indicate that either archive is damaged or it might not be archived with Zlib
21 | or compressed with Deflate at all.
22 |
23 | - Returns: Unarchived data.
24 | */
25 | public static func unarchive(archive data: Data) throws -> Data {
26 | // Valid Zlib archive must contain at least 8 bytes of data.
27 | guard data.count >= 8
28 | else { throw ZlibError.wrongCompressionMethod }
29 |
30 | /// Object with input data which supports convenient work with bit shifts.
31 | let bitReader = LsbBitReader(data: data)
32 |
33 | _ = try ZlibHeader(bitReader)
34 |
35 | let out = try Deflate.decompress(bitReader)
36 | bitReader.align()
37 |
38 | let adler32 = bitReader.uint32().byteSwapped
39 | guard CheckSums.adler32(out) == adler32
40 | else { throw ZlibError.wrongAdler32(out) }
41 |
42 | return out
43 | }
44 |
45 | /**
46 | Archives `data` into Zlib archive. Data will be also compressed with Deflate algorithm.
47 | It will also be specified in archive's header that the compressor used the slowest Deflate algorithm.
48 |
49 | - Note: This function is specification compliant.
50 |
51 | - Parameter data: Data to compress and archive.
52 |
53 | - Returns: Resulting archive's data.
54 | */
55 | public static func archive(data: Data) -> Data {
56 | let out: [UInt8] = [
57 | 120, // CM (Compression Method) = 8 (DEFLATE), CINFO (Compression Info) = 7 (32K window size).
58 | 218 // Flags: slowest algorithm, no preset dictionary.
59 | ]
60 | var outData = Data(out)
61 | outData.append(Deflate.compress(data: data))
62 |
63 | let adler32 = CheckSums.adler32(data)
64 | var adlerBytes = [UInt8]()
65 | for i in 0..<4 {
66 | adlerBytes.append(UInt8(truncatingIfNeeded: (adler32 & (0xFF << ((3 - i) * 8))) >> ((3 - i) * 8)))
67 | }
68 | outData.append(Data(adlerBytes))
69 |
70 | return outData
71 | }
72 |
73 | }
74 |
--------------------------------------------------------------------------------
/Sources/Zlib/ZlibError.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | /**
9 | Represents an error which happened while processing a Zlib archive.
10 | It may indicate that either archive is damaged or it might not be Zlib archive at all.
11 | */
12 | public enum ZlibError: Error {
13 | /// Compression method used in archive is different from Deflate, which is the only supported one.
14 | case wrongCompressionMethod
15 | /// Compression info has value incompatible with Deflate compression method.
16 | case wrongCompressionInfo
17 | /// First two bytes of archive's flags are inconsistent with each other.
18 | case wrongFcheck
19 | /// Compression level has value, which is different from the supported ones.
20 | case wrongCompressionLevel
21 | /**
22 | Computed checksum of uncompressed data doesn't match the value stored in archive.
23 | Associated value of the error contains already decompressed data.
24 | */
25 | case wrongAdler32(Data)
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/Zlib/ZlibHeader.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import BitByteData
8 |
9 | /// Represents the header of a Zlib archive.
10 | public struct ZlibHeader {
11 |
12 | /// Levels of compression which can be used to create Zlib archive.
13 | public enum CompressionLevel: Int {
14 | /// Fastest algorithm.
15 | case fastestAlgorithm = 0
16 | /// Fast algorithm.
17 | case fastAlgorithm = 1
18 | /// Default algorithm.
19 | case defaultAlgorithm = 2
20 | /// Slowest algorithm but with maximum compression.
21 | case slowAlgorithm = 3
22 | }
23 |
24 | /// Compression method of archive. Always `.deflate` for Zlib archives.
25 | public let compressionMethod: CompressionMethod = .deflate
26 |
27 | /// Level of compression used in archive.
28 | public let compressionLevel: CompressionLevel
29 |
30 | /// Size of 'window': moving interval of data which was used to make archive.
31 | public let windowSize: Int
32 |
33 | /**
34 | Initializes the structure with the values from Zlib `archive`.
35 |
36 | If data passed is not actually a Zlib archive, `ZlibError` will be thrown.
37 |
38 | - Parameter archive: Data archived with zlib.
39 |
40 | - Throws: `ZlibError`. It may indicate that either archive is damaged or it might not be archived with Zlib at all.
41 | */
42 | public init(archive data: Data) throws {
43 | let reader = LsbBitReader(data: data)
44 | try self.init(reader)
45 | }
46 |
47 | init(_ reader: LsbBitReader) throws {
48 | // Valid Zlib header must contain at least 2 bytes of data.
49 | guard reader.bytesLeft >= 2
50 | else { throw ZlibError.wrongCompressionMethod }
51 |
52 | // compressionMethod and compressionInfo combined are needed later for integrity check.
53 | let cmf = reader.byte()
54 | // First four bits are compression method.
55 | // Only compression method = 8 (DEFLATE) is supported.
56 | let compressionMethod = cmf & 0xF
57 | guard compressionMethod == 8
58 | else { throw ZlibError.wrongCompressionMethod }
59 |
60 | // Remaining four bits indicate window size.
61 | // For Deflate it must not be more than 7.
62 | let compressionInfo = (cmf & 0xF0) >> 4
63 | guard compressionInfo <= 7
64 | else { throw ZlibError.wrongCompressionInfo }
65 |
66 | let windowSize = 1 << (compressionInfo.toInt() + 8)
67 | self.windowSize = windowSize
68 |
69 | // fcheck, fdict and compresionLevel together make flags byte which is used in integrity check.
70 | let flags = reader.byte()
71 |
72 | // First five bits are fcheck bits which are used for integrity check:
73 | // let fcheck = flags & 0x1F
74 |
75 | // Sixth bit indicate if archive contain Adler-32 checksum of preset dictionary.
76 | let fdict = (flags & 0x20) >> 5
77 |
78 | // Remaining bits indicate compression level.
79 | guard let compressionLevel = ZlibHeader.CompressionLevel(rawValue: (flags.toInt() & 0xC0) >> 6)
80 | else { throw ZlibError.wrongCompressionLevel }
81 | self.compressionLevel = compressionLevel
82 |
83 | guard (UInt(cmf) * 256 + UInt(flags)) % 31 == 0
84 | else { throw ZlibError.wrongFcheck }
85 |
86 | // If preset dictionary is present 4 bytes will be skipped.
87 | if fdict == 1 {
88 | reader.offset += 4
89 | }
90 | }
91 |
92 | }
93 |
--------------------------------------------------------------------------------
/Sources/swcomp/Archives/BZip2Command.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import SWCompression
8 | import SwiftCLI
9 |
10 | // This extension allows to use BZip2.BlockSize as a Key option.
11 | // The extension is empty because there is a default implementation for a ConvertibleFromString when the RawValue type
12 | // of the enum (Int, in this case) is ConvertibleFromString itself.
13 | extension BZip2.BlockSize: ConvertibleFromString {}
14 |
15 | final class BZip2Command: Command {
16 |
17 | let name = "bz2"
18 | let shortDescription = "Creates or extracts a BZip2 archive"
19 |
20 | @Flag("-c", "--compress", description: "Compress an input file into a BZip2 archive")
21 | var compress: Bool
22 |
23 | @Flag("-d", "--decompress", description: "Decompress a BZip2 archive")
24 | var decompress: Bool
25 |
26 | @Key("-b", "--block-size", description: "Set the block size for compression to a multiple of 100k bytes; possible " +
27 | "values are from '1' (default) to '9'")
28 | var blockSize: BZip2.BlockSize?
29 |
30 | var optionGroups: [OptionGroup] {
31 | return [.exactlyOne($compress, $decompress)]
32 | }
33 |
34 | @Param var input: String
35 | @Param var output: String?
36 |
37 | func execute() throws {
38 | if decompress {
39 | let inputURL = URL(fileURLWithPath: self.input)
40 |
41 | let outputURL: URL
42 | if let outputPath = output {
43 | outputURL = URL(fileURLWithPath: outputPath)
44 | } else if inputURL.pathExtension == "bz2" {
45 | outputURL = inputURL.deletingPathExtension()
46 | } else {
47 | swcompExit(.noOutputPath)
48 | }
49 |
50 | let fileData = try Data(contentsOf: inputURL, options: .mappedIfSafe)
51 | let decompressedData = try BZip2.decompress(data: fileData)
52 | try decompressedData.write(to: outputURL)
53 | } else if compress {
54 | let inputURL = URL(fileURLWithPath: self.input)
55 |
56 | let outputURL: URL
57 | if let outputPath = output {
58 | outputURL = URL(fileURLWithPath: outputPath)
59 | } else {
60 | outputURL = inputURL.appendingPathExtension("bz2")
61 | }
62 |
63 | let fileData = try Data(contentsOf: inputURL, options: .mappedIfSafe)
64 |
65 | let compressedData = BZip2.compress(data: fileData, blockSize: blockSize ?? .one)
66 | try compressedData.write(to: outputURL)
67 | }
68 | }
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/Sources/swcomp/Archives/GZipCommand.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import SWCompression
8 | import SwiftCLI
9 |
10 | final class GZipCommand: Command {
11 |
12 | let name = "gz"
13 | let shortDescription = "Creates or extracts a GZip archive"
14 |
15 | @Flag("-c", "--compress", description: "Compress an input file into a GZip archive")
16 | var compress: Bool
17 |
18 | @Flag("-d", "--decompress", description: "Decompress a GZip archive")
19 | var decompress: Bool
20 |
21 | @Flag("-i", "--info", description: "Print information from a GZip header")
22 | var info: Bool
23 |
24 | @Flag("-n", "--use-gzip-name", description: "Use the name saved inside a GZip archive as an output path, if possible")
25 | var useGZipName: Bool
26 |
27 | var optionGroups: [OptionGroup] {
28 | return [.exactlyOne($compress, $decompress, $info)]
29 | }
30 |
31 | @Param var input: String
32 | @Param var output: String?
33 |
34 | func execute() throws {
35 | if decompress {
36 | let inputURL = URL(fileURLWithPath: self.input)
37 |
38 | var outputURL: URL?
39 | if let outputPath = output {
40 | outputURL = URL(fileURLWithPath: outputPath)
41 | } else if inputURL.pathExtension == "gz" {
42 | outputURL = inputURL.deletingPathExtension()
43 | }
44 |
45 | let fileData = try Data(contentsOf: inputURL, options: .mappedIfSafe)
46 |
47 | if useGZipName {
48 | let header = try GzipHeader(archive: fileData)
49 | if let fileName = header.fileName {
50 | outputURL = inputURL.deletingLastPathComponent()
51 | .appendingPathComponent(fileName, isDirectory: false)
52 | }
53 | }
54 |
55 | guard outputURL != nil
56 | else { swcompExit(.noOutputPath) }
57 |
58 | let decompressedData = try GzipArchive.unarchive(archive: fileData)
59 | try decompressedData.write(to: outputURL!)
60 | } else if compress {
61 | let inputURL = URL(fileURLWithPath: self.input)
62 | let fileName = inputURL.lastPathComponent
63 |
64 | let outputURL: URL
65 | if let outputPath = output {
66 | outputURL = URL(fileURLWithPath: outputPath)
67 | } else {
68 | outputURL = inputURL.appendingPathExtension("gz")
69 | }
70 |
71 | let fileData = try Data(contentsOf: inputURL, options: .mappedIfSafe)
72 | let compressedData = try GzipArchive.archive(data: fileData,
73 | fileName: fileName.isEmpty ? nil : fileName,
74 | writeHeaderCRC: true)
75 | try compressedData.write(to: outputURL)
76 | } else if info {
77 | let inputURL = URL(fileURLWithPath: self.input)
78 | let fileData = try Data(contentsOf: inputURL, options: .mappedIfSafe)
79 |
80 | let header = try GzipHeader(archive: fileData)
81 | print(header)
82 | }
83 | }
84 |
85 | }
86 |
--------------------------------------------------------------------------------
/Sources/swcomp/Archives/LZ4Command.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import SWCompression
8 | import SwiftCLI
9 |
10 | final class LZ4Command: Command {
11 |
12 | let name = "lz4"
13 | let shortDescription = "Creates or extracts a LZ4 archive"
14 |
15 | @Flag("-c", "--compress", description: "Compress an input file into a LZ4 archive")
16 | var compress: Bool
17 |
18 | @Flag("-d", "--decompress", description: "Decompress a LZ4 archive")
19 | var decompress: Bool
20 |
21 | @Flag("--dependent-blocks", description: "(Compression only) Use dependent blocks")
22 | var dependentBlocks: Bool
23 |
24 | @Flag("--block-checksums", description: "(Compression only) Save checksums for compressed blocks")
25 | var blockChecksums: Bool
26 |
27 | @Flag("--no-content-checksum", description: "(Compression only) Don't save the checksum of the uncompressed data")
28 | var noContentChecksum: Bool
29 |
30 | @Flag("--content-size", description: "(Compression only) Save the size of the uncompressed data")
31 | var contentSize: Bool
32 |
33 | @Key("--block-size", description: "(Compression only) Use specified block size (in bytes; default and max: 4194304)")
34 | var blockSize: Int?
35 |
36 | @Key("-D", "--dict", description: "Path to a dictionary to use in decompression or compression")
37 | var dictionary: String?
38 |
39 | @Key("--dictID", description: "Optional dictionary ID (max: 4294967295)")
40 | var dictionaryID: Int?
41 |
42 | var optionGroups: [OptionGroup] {
43 | return [.exactlyOne($compress, $decompress)]
44 | }
45 |
46 | @Param var input: String
47 | @Param var output: String?
48 |
49 | func execute() throws {
50 | let dictID: UInt32?
51 | if let dictionaryID = dictionaryID {
52 | guard dictionaryID <= UInt32.max
53 | else { swcompExit(.lz4BigDictId) }
54 | dictID = UInt32(truncatingIfNeeded: dictionaryID)
55 | } else {
56 | dictID = nil
57 | }
58 |
59 | let dictData: Data?
60 | if let dictionary = dictionary {
61 | dictData = try Data(contentsOf: URL(fileURLWithPath: dictionary), options: .mappedIfSafe)
62 | } else {
63 | dictData = nil
64 | }
65 |
66 | guard dictID == nil || dictData != nil
67 | else { swcompExit(.lz4NoDict) }
68 |
69 | if decompress {
70 | let inputURL = URL(fileURLWithPath: self.input)
71 |
72 | let outputURL: URL
73 | if let outputPath = output {
74 | outputURL = URL(fileURLWithPath: outputPath)
75 | } else if inputURL.pathExtension == "lz4" {
76 | outputURL = inputURL.deletingPathExtension()
77 | } else {
78 | swcompExit(.noOutputPath)
79 | }
80 |
81 | let fileData = try Data(contentsOf: inputURL, options: .mappedIfSafe)
82 | let decompressedData = try LZ4.decompress(data: fileData, dictionary: dictData, dictionaryID: dictID)
83 | try decompressedData.write(to: outputURL)
84 | } else if compress {
85 | let inputURL = URL(fileURLWithPath: self.input)
86 |
87 | let outputURL: URL
88 | if let outputPath = output {
89 | outputURL = URL(fileURLWithPath: outputPath)
90 | } else {
91 | outputURL = inputURL.appendingPathExtension("lz4")
92 | }
93 |
94 | let bs: Int
95 | if let blockSize = blockSize {
96 | guard blockSize < 4194304
97 | else { swcompExit(.lz4BigBlockSize) }
98 | bs = blockSize
99 | } else {
100 | bs = 4 * 1024 * 1024
101 | }
102 |
103 | let fileData = try Data(contentsOf: inputURL, options: .mappedIfSafe)
104 | let compressedData = LZ4.compress(data: fileData, independentBlocks: !dependentBlocks,
105 | blockChecksums: blockChecksums, contentChecksum: !noContentChecksum,
106 | contentSize: contentSize, blockSize: bs, dictionary: dictData, dictionaryID: dictID)
107 | try compressedData.write(to: outputURL)
108 | }
109 | }
110 |
111 | }
112 |
--------------------------------------------------------------------------------
/Sources/swcomp/Archives/LZMACommand.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import SWCompression
8 | import SwiftCLI
9 |
10 | final class LZMACommand: Command {
11 |
12 | let name = "lzma"
13 | let shortDescription = "Extracts a LZMA archive"
14 |
15 | @Param var input: String
16 | @Param var output: String?
17 |
18 | func execute() throws {
19 | let fileData = try Data(contentsOf: URL(fileURLWithPath: self.input),
20 | options: .mappedIfSafe)
21 | let outputPath = self.output ?? FileManager.default.currentDirectoryPath
22 | let decompressedData = try LZMA.decompress(data: fileData)
23 | try decompressedData.write(to: URL(fileURLWithPath: outputPath))
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/swcomp/Archives/XZCommand.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import SWCompression
8 | import SwiftCLI
9 |
10 | final class XZCommand: Command {
11 |
12 | let name = "xz"
13 | let shortDescription = "Extracts a XZ archive"
14 |
15 | @Param var input: String
16 | @Param var output: String?
17 |
18 | func execute() throws {
19 | let inputURL = URL(fileURLWithPath: self.input)
20 |
21 | let outputURL: URL
22 | if let outputPath = self.output {
23 | outputURL = URL(fileURLWithPath: outputPath)
24 | } else if inputURL.pathExtension == "xz" {
25 | outputURL = inputURL.deletingPathExtension()
26 | } else {
27 | swcompExit(.noOutputPath)
28 | }
29 |
30 | let fileData = try Data(contentsOf: inputURL, options: .mappedIfSafe)
31 | let decompressedData = try XZArchive.unarchive(archive: fileData)
32 | try decompressedData.write(to: outputURL)
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/swcomp/Benchmarks/Benchmark.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | protocol Benchmark {
7 |
8 | var defaultIterationCount: Int { get }
9 |
10 | init(_ input: String)
11 |
12 | func warmupIteration()
13 |
14 | func measure() -> Double
15 |
16 | func format(_ value: Double) -> String
17 |
18 | }
19 |
20 | extension Benchmark {
21 |
22 | var defaultIterationCount: Int {
23 | return 10
24 | }
25 |
26 | func warmupIteration() {
27 | _ = measure()
28 | }
29 |
30 | func format(_ value: Double) -> String {
31 | return SpeedFormatter.default.string(from: value)
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/swcomp/Benchmarks/BenchmarkGroup.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import SWCompression
8 | import SwiftCLI
9 |
10 | final class BenchmarkGroup: CommandGroup {
11 |
12 | let name = "benchmark"
13 | let shortDescription = "Benchmark-related commands"
14 |
15 | let children: [Routable] = [
16 | RunBenchmarkCommand(),
17 | ShowBenchmarkCommand()
18 | ]
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/swcomp/Benchmarks/BenchmarkMetadata.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | struct BenchmarkMetadata: Codable, Equatable {
9 |
10 | var timestamp: TimeInterval?
11 | var osInfo: String
12 | var swiftVersion: String
13 | var swcVersion: String
14 | var description: String?
15 |
16 | private static func run(command: URL, arguments: [String] = []) throws -> String {
17 | let task = Process()
18 | let pipe = Pipe()
19 |
20 | task.standardOutput = pipe
21 | task.standardError = pipe
22 | task.executableURL = command
23 | task.arguments = arguments
24 | task.standardInput = nil
25 |
26 | try task.run()
27 | task.waitUntilExit()
28 | let data = pipe.fileHandleForReading.readDataToEndOfFile()
29 | let output = String(data: data, encoding: .utf8)!
30 | return output
31 | }
32 |
33 | private static func getExecURL(for command: String) throws -> URL {
34 | let args = ["-c", "which \(command)"]
35 | #if os(Windows)
36 | swcompExit(.benchmarkCannotGetSubcommandPathWindows)
37 | #else
38 | let output = try BenchmarkMetadata.run(command: URL(fileURLWithPath: "/bin/sh"), arguments: args)
39 | #endif
40 | return URL(fileURLWithPath: String(output.dropLast()))
41 | }
42 |
43 | private static func getOsInfo() throws -> String {
44 | #if os(Linux)
45 | return try BenchmarkMetadata.run(command: BenchmarkMetadata.getExecURL(for: "uname"), arguments: ["-a"])
46 | #else
47 | #if os(Windows)
48 | return "Unknown Windows OS"
49 | #else
50 | return try BenchmarkMetadata.run(command: BenchmarkMetadata.getExecURL(for: "sw_vers"))
51 | #endif
52 | #endif
53 | }
54 |
55 | init(_ description: String?, _ preserveTimestamp: Bool) throws {
56 | self.timestamp = preserveTimestamp ? Date.timeIntervalSinceReferenceDate : nil
57 | self.osInfo = try BenchmarkMetadata.getOsInfo()
58 | #if os(Windows)
59 | self.swiftVersion = "Unknown Swift version on Windows"
60 | #else
61 | self.swiftVersion = try BenchmarkMetadata.run(command: BenchmarkMetadata.getExecURL(for: "swift"),
62 | arguments: ["-version"])
63 | #endif
64 | self.swcVersion = _SWC_VERSION
65 | self.description = description
66 | }
67 |
68 | func print() {
69 | Swift.print("OS Info: \(self.osInfo)", terminator: "")
70 | Swift.print("Swift version: \(self.swiftVersion)", terminator: "")
71 | Swift.print("SWC version: \(self.swcVersion)")
72 | if let timestamp = self.timestamp {
73 | Swift.print("Timestamp: " +
74 | DateFormatter.localizedString(from: Date(timeIntervalSinceReferenceDate: timestamp),
75 | dateStyle: .short, timeStyle: .short))
76 | }
77 | if let description = self.description {
78 | Swift.print("Description: \(description)")
79 | }
80 | Swift.print()
81 | }
82 |
83 | }
84 |
--------------------------------------------------------------------------------
/Sources/swcomp/Benchmarks/BenchmarkResult.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | struct BenchmarkResult: Codable {
9 |
10 | var name: String
11 | var input: String
12 | var iterCount: Int
13 | var avg: Double
14 | var std: Double
15 |
16 | var id: String {
17 | return [self.name, self.input, String(self.iterCount)].joined(separator: "<#>")
18 | }
19 |
20 | func printComparison(with other: BenchmarkResult) {
21 | let diff = (self.avg / other.avg - 1) * 100
22 | let comparison = self.compare(with: other)
23 | if diff < 0 {
24 | switch comparison {
25 | case 1:
26 | print(String(format: "OK %f%% (p-value > 0.05)", diff))
27 | case nil:
28 | print("Cannot compare due to unsupported iteration count.")
29 | case -1:
30 | print(String(format: "REG %f%% (p-value < 0.05)", diff))
31 | case 0:
32 | print(String(format: "REG %f%% (p-value = 0.05)", diff))
33 | default:
34 | swcompExit(.benchmarkUnknownCompResult)
35 | }
36 | }
37 | else if diff > 0 {
38 | switch comparison {
39 | case 1:
40 | print(String(format: "OK %f%% (p-value > 0.05)", diff))
41 | case nil:
42 | print("Cannot compare due to unsupported iteration count.")
43 | case -1:
44 | print(String(format: "IMP %f%% (p-value < 0.05)", diff))
45 | case 0:
46 | print(String(format: "IMP %f%% (p-value = 0.05)", diff))
47 | default:
48 | swcompExit(.benchmarkUnknownCompResult)
49 | }
50 | } else {
51 | print("OK (exact match of averages)")
52 | }
53 | }
54 |
55 | private func compare(with other: BenchmarkResult) -> Int? {
56 | let degreesOfFreedom = Double(self.iterCount + other.iterCount - 2)
57 | let t1: Double = Double(self.iterCount - 1) * pow(self.std, 2)
58 | let t2: Double = Double(other.iterCount - 1) * pow(other.std, 2)
59 | let pooledStd = ((t1 + t2) / degreesOfFreedom).squareRoot()
60 | let se = pooledStd * (1 / Double(self.iterCount) + 1 / Double(other.iterCount)).squareRoot()
61 | let tStat = (self.avg - other.avg ) / se
62 | if degreesOfFreedom == 18 {
63 | if abs(tStat) > 2.101 {
64 | // p-value < 0.05
65 | return -1
66 | } else if abs(tStat) == 2.101 {
67 | // p-value = 0.05
68 | return 0
69 | } else {
70 | // p-value > 0.05
71 | return 1
72 | }
73 | } else {
74 | return nil
75 | }
76 | }
77 |
78 | }
79 |
--------------------------------------------------------------------------------
/Sources/swcomp/Benchmarks/SaveFile.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | struct SaveFile: Codable {
9 |
10 | struct Run: Codable {
11 |
12 | var metadataUUID: UUID
13 | var results: [BenchmarkResult]
14 |
15 | }
16 |
17 | var metadatas: [UUID: BenchmarkMetadata]
18 |
19 | var runs: [Run]
20 |
21 | static func load(from path: String) throws -> SaveFile {
22 | let decoder = JSONDecoder()
23 | let data = try Data(contentsOf: URL(fileURLWithPath: path))
24 | return try decoder.decode(SaveFile.self, from: data)
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/swcomp/Benchmarks/ShowBenchmarkCommand.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | #if os(Linux)
7 | import CoreFoundation
8 | #endif
9 |
10 | import Foundation
11 | import SwiftCLI
12 |
13 | final class ShowBenchmarkCommand: Command {
14 |
15 | let name = "show"
16 | let shortDescription = "Print saved benchmarks results"
17 |
18 | @Key("-c", "--compare", description: "Compare with other saved benchmarks results")
19 | var comparePath: String?
20 |
21 | @Param var path: String
22 |
23 | func execute() throws {
24 | let newSaveFile = try SaveFile.load(from: self.path)
25 | var newMetadatas = Dictionary(uniqueKeysWithValues: zip(newSaveFile.metadatas.keys, (1...newSaveFile.metadatas.count).map { "(\($0))" }))
26 | if newMetadatas.count == 1 {
27 | newMetadatas[newMetadatas.first!.key] = ""
28 | }
29 | for (metadataUUID, index) in newMetadatas.sorted(by: { $0.value < $1.value }) {
30 | print("NEW\(index) Metadata")
31 | print("---------------")
32 | newSaveFile.metadatas[metadataUUID]!.print()
33 | }
34 |
35 | var newResults = [String: [(BenchmarkResult, UUID)]]()
36 | for newRun in newSaveFile.runs {
37 | newResults.merge(Dictionary(grouping: newRun.results.map { ($0, newRun.metadataUUID) }, by: { $0.0.id }),
38 | uniquingKeysWith: { $0 + $1 })
39 | }
40 |
41 | var baseResults = [String: [(BenchmarkResult, UUID)]]()
42 | var baseMetadatas = [UUID: String]()
43 | if let comparePath = comparePath {
44 | let baseSaveFile = try SaveFile.load(from: comparePath)
45 |
46 | baseMetadatas = Dictionary(uniqueKeysWithValues: zip(baseSaveFile.metadatas.keys, (1...baseSaveFile.metadatas.count).map { "(\($0))" }))
47 | if baseMetadatas.count == 1 {
48 | baseMetadatas[baseMetadatas.first!.key] = ""
49 | }
50 | for (metadataUUID, index) in baseMetadatas.sorted(by: { $0.value < $1.value }) {
51 | print("BASE\(index) Metadata")
52 | print("----------------")
53 | baseSaveFile.metadatas[metadataUUID]!.print()
54 | }
55 |
56 | for baseRun in baseSaveFile.runs {
57 | baseResults.merge(Dictionary(grouping: baseRun.results.map { ($0, baseRun.metadataUUID) }, by: { $0.0.id }),
58 | uniquingKeysWith: { $0 + $1 })
59 | }
60 | }
61 |
62 | for resultId in newResults.keys.sorted() {
63 | let results = newResults[resultId]!
64 | for (result, metadataUUID) in results {
65 | let benchmark = Benchmarks(rawValue: result.name)?.initialized(result.input)
66 |
67 | print("\(result.name) => \(result.input), iterations = \(result.iterCount)")
68 |
69 | print("NEW\(newMetadatas[metadataUUID]!): average = \(benchmark.format(result.avg)), standard deviation = \(benchmark.format(result.std))")
70 | if let baseResults = baseResults[resultId] {
71 | for (other, baseUUID) in baseResults {
72 | print("BASE\(baseMetadatas[baseUUID]!): average = \(benchmark.format(other.avg)), standard deviation = \(benchmark.format(other.std))")
73 | result.printComparison(with: other)
74 | }
75 | }
76 |
77 | print()
78 | }
79 | }
80 | }
81 |
82 | }
83 |
84 | fileprivate extension Optional where Wrapped == Benchmark {
85 |
86 | func format(_ value: Double) -> String {
87 | switch self {
88 | case .some(let benchmark):
89 | return benchmark.format(value)
90 | case .none:
91 | return String(value)
92 | }
93 | }
94 |
95 | }
96 |
--------------------------------------------------------------------------------
/Sources/swcomp/Benchmarks/SpeedFormatter.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | struct SpeedFormatter {
7 |
8 | static let `default`: SpeedFormatter = SpeedFormatter()
9 |
10 | enum Units {
11 | case bytes
12 | case kB
13 | case MB
14 | case GB
15 | case TB
16 |
17 | init(_ speed: Double) {
18 | if speed > 1_000_000_000_000 {
19 | self = .TB
20 | } else if speed > 1_000_000_000 {
21 | self = .GB
22 | } else if speed > 1_000_000 {
23 | self = .MB
24 | } else if speed > 1000 {
25 | self = .kB
26 | } else {
27 | self = .bytes
28 | }
29 | }
30 |
31 | fileprivate func unitsString() -> String {
32 | switch self {
33 | case .bytes:
34 | return "B/s"
35 | case .kB:
36 | return "kB/s"
37 | case .MB:
38 | return "MB/s"
39 | case .GB:
40 | return "GB/s"
41 | case .TB:
42 | return "TB/s"
43 | }
44 | }
45 | }
46 |
47 | var units: Units?
48 | var hideUnits: Bool
49 | var fractionDigits: Int
50 |
51 | init() {
52 | self.units = nil
53 | self.hideUnits = false
54 | self.fractionDigits = 3
55 | }
56 |
57 | func string(from speed: Double, units: Units? = nil, hideUnits: Bool? = nil, fractionDigits: Int? = nil) -> String {
58 | let actualUnits = units ?? self.units ?? Units(speed)
59 | let speedInUnits: Double
60 | switch actualUnits {
61 | case .bytes:
62 | speedInUnits = speed
63 | case .kB:
64 | speedInUnits = speed / 1000
65 | case .MB:
66 | speedInUnits = speed / 1_000_000
67 | case .GB:
68 | speedInUnits = speed / 1_000_000_000
69 | case .TB:
70 | speedInUnits = speed / 1_000_000_000_000
71 | }
72 |
73 | var formatString = "%.\(fractionDigits ?? self.fractionDigits)f"
74 | if !(hideUnits ?? self.hideUnits) {
75 | formatString += " " + actualUnits.unitsString()
76 | }
77 | return String(format: formatString, speedInUnits)
78 | }
79 |
80 | }
81 |
--------------------------------------------------------------------------------
/Sources/swcomp/Containers/7ZipCommand.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import SWCompression
8 | import SwiftCLI
9 |
10 | final class SevenZipCommand: ContainerCommand {
11 |
12 | typealias ContainerType = SevenZipContainer
13 |
14 | let name = "7z"
15 | let shortDescription = "Extracts a 7-Zip container"
16 |
17 | @Flag("-i", "--info", description: "Print the information about of the entries in the container including their attributes")
18 | var info: Bool
19 |
20 | @Flag("-l", "--list", description: "Print the list of names of the entries in the container")
21 | var list: Bool
22 |
23 | @Key("-e", "--extract", description: "Extract a container into the specified directory")
24 | var extract: String?
25 |
26 | @Flag("-v", "--verbose", description: "Print the list of extracted files and directories.")
27 | var verbose: Bool
28 |
29 | @Param var input: String
30 |
31 | var optionGroups: [OptionGroup] {
32 | return [.exactlyOne($info, $list, $extract)]
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/swcomp/Containers/ContainerCommand.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import SWCompression
8 | import SwiftCLI
9 |
10 | protocol ContainerCommand: Command {
11 |
12 | associatedtype ContainerType: Container
13 |
14 | var info: Bool { get }
15 | var list: Bool { get }
16 | var extract: String? { get }
17 | var verbose: Bool { get }
18 | var input: String { get }
19 |
20 | }
21 |
22 | extension ContainerCommand {
23 |
24 | func execute() throws {
25 | let fileData = try Data(contentsOf: URL(fileURLWithPath: self.input),
26 | options: .mappedIfSafe)
27 | if info {
28 | let entries = try ContainerType.info(container: fileData)
29 | swcomp.printInfo(entries)
30 | } else if list {
31 | let entries = try ContainerType.info(container: fileData)
32 | swcomp.printList(entries)
33 | } else if let outputPath = self.extract {
34 | guard try isValidOutputDirectory(outputPath, create: true)
35 | else { swcompExit(.containerOutPathExistsNotDir) }
36 |
37 | let entries = try ContainerType.open(container: fileData)
38 | try swcomp.write(entries, outputPath, verbose)
39 | }
40 | }
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/Sources/swcomp/Containers/ZipCommand.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import SWCompression
8 | import SwiftCLI
9 |
10 | final class ZipCommand: ContainerCommand {
11 |
12 | typealias ContainerType = ZipContainer
13 |
14 | let name = "zip"
15 | let shortDescription = "Extracts a ZIP container"
16 |
17 | @Flag("-i", "--info", description: "Print the information about of the entries in the container including their attributes")
18 | var info: Bool
19 |
20 | @Flag("-l", "--list", description: "Print the list of names of the entries in the container")
21 | var list: Bool
22 |
23 | @Key("-e", "--extract", description: "Extract a container into the specified directory")
24 | var extract: String?
25 |
26 | @Flag("-v", "--verbose", description: "Print the list of extracted files and directories.")
27 | var verbose: Bool
28 |
29 | @Param var input: String
30 |
31 | var optionGroups: [OptionGroup] {
32 | return [.exactlyOne($info, $list, $extract)]
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/swcomp/Extensions/CompressionMethod+CustomStringConvertible.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import SWCompression
8 |
9 | extension CompressionMethod: CustomStringConvertible {
10 |
11 | public var description: String {
12 | switch self {
13 | case .bzip2:
14 | return "BZip2"
15 | case .copy:
16 | return "none"
17 | case .deflate:
18 | return "deflate"
19 | case .lzma:
20 | return "LZMA"
21 | case .lzma2:
22 | return "LZMA2"
23 | case .other:
24 | return "other/unknown"
25 | }
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/swcomp/Extensions/ContainerEntryInfo+CustomStringConvertible.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import SWCompression
8 |
9 | extension ContainerEntryInfo where Self: CustomStringConvertible {
10 |
11 | public var description: String {
12 | var output = "Name: \(self.name)\n"
13 |
14 | switch self.type {
15 | case .blockSpecial:
16 | output += "Type: block device file\n"
17 | case .characterSpecial:
18 | output += "Type: character device file\n"
19 | case .contiguous:
20 | output += "Type: contiguous file\n"
21 | case .directory:
22 | output += "Type: directory\n"
23 | case .fifo:
24 | output += "Type: fifo file\n"
25 | case .hardLink:
26 | output += "Type: hard link\n"
27 | case .regular:
28 | output += "Type: regular file\n"
29 | case .socket:
30 | output += "Type: socket\n"
31 | case .symbolicLink:
32 | output += "Type: symbolic link\n"
33 | case .unknown:
34 | output += "Type: unknown\n"
35 | }
36 |
37 | if let tarEntry = self as? TarEntryInfo {
38 | if tarEntry.type == .symbolicLink {
39 | output += "Linked path: \(tarEntry.linkName)\n"
40 | }
41 | if let ownerID = tarEntry.ownerID {
42 | output += "Uid: \(ownerID)\n"
43 | }
44 | if let groupID = tarEntry.groupID {
45 | output += "Gid: \(groupID)\n"
46 | }
47 | if let ownerUserName = tarEntry.ownerUserName {
48 | output += "Uname: \(ownerUserName)\n"
49 | }
50 | if let ownerGroupName = tarEntry.ownerGroupName {
51 | output += "Gname: \(ownerGroupName)\n"
52 | }
53 | if let comment = tarEntry.comment {
54 | output += "Comment: \(comment)\n"
55 | }
56 | if let unknownPaxRecords = tarEntry.unknownExtendedHeaderRecords, unknownPaxRecords.count > 0 {
57 | output += "Unknown PAX (extended header) records:\n"
58 | for entry in unknownPaxRecords {
59 | output += " \(entry.key): \(entry.value)\n"
60 | }
61 | }
62 | }
63 |
64 | if let zipEntry = self as? ZipEntryInfo {
65 | if !zipEntry.comment.isEmpty {
66 | output += "Comment: \(zipEntry.comment)\n"
67 | }
68 | output += String(format: "External File Attributes: 0x%08X\n", zipEntry.externalFileAttributes)
69 | output += "Is text file: \(zipEntry.isTextFile)\n"
70 | output += "File system type: \(zipEntry.fileSystemType)\n"
71 | output += "Compression method: \(zipEntry.compressionMethod)\n"
72 | if let ownerID = zipEntry.ownerID {
73 | output += "Uid: \(ownerID)\n"
74 | }
75 | if let groupID = zipEntry.groupID {
76 | output += "Gid: \(groupID)\n"
77 | }
78 | output += String(format: "CRC32: 0x%08X\n", zipEntry.crc)
79 | }
80 |
81 | if let sevenZipEntry = self as? SevenZipEntryInfo {
82 | if let winAttrs = sevenZipEntry.winAttributes {
83 | output += String(format: "Win attributes: 0x%08X\n", winAttrs)
84 | }
85 | if let crc = sevenZipEntry.crc {
86 | output += String(format: "CRC32: 0x%08X\n", crc)
87 | }
88 | output += "Has stream: \(sevenZipEntry.hasStream)\n"
89 | output += "Is empty: \(sevenZipEntry.isEmpty)\n"
90 | output += "Is anti-file: \(sevenZipEntry.isAnti)\n"
91 | }
92 |
93 | if let size = self.size {
94 | output += "Size: \(size) bytes\n"
95 | }
96 |
97 | if let mtime = self.modificationTime {
98 | output += "Mtime: \(mtime)\n"
99 | }
100 |
101 | if let atime = self.accessTime {
102 | output += "Atime: \(atime)\n"
103 | }
104 |
105 | if let ctime = self.creationTime {
106 | output += "Ctime: \(ctime)\n"
107 | }
108 |
109 | if let permissions = self.permissions?.rawValue {
110 | output += String(format: "Permissions: %o", permissions)
111 | }
112 |
113 | return output
114 | }
115 |
116 | }
117 |
118 | extension TarEntryInfo: CustomStringConvertible { }
119 |
120 | extension ZipEntryInfo: CustomStringConvertible { }
121 |
122 | extension SevenZipEntryInfo: CustomStringConvertible { }
123 |
--------------------------------------------------------------------------------
/Sources/swcomp/Extensions/FileSystemType+CustomStringConvertible.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import SWCompression
8 |
9 | extension FileSystemType: CustomStringConvertible {
10 |
11 | public var description: String {
12 | switch self {
13 | case .fat:
14 | return "FAT"
15 | case .macintosh:
16 | return "old Macintosh file system"
17 | case .ntfs:
18 | return "NTFS"
19 | case .unix:
20 | return "UNIX-like"
21 | case .other:
22 | return "other/unknown"
23 | }
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/swcomp/Extensions/GzipHeader+CustomStringConvertible.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import SWCompression
8 |
9 | extension GzipHeader: CustomStringConvertible {
10 |
11 | public var description: String {
12 | var output = """
13 | File name: \(self.fileName ?? "")
14 | File system type: \(self.osType)
15 | Compression method: \(self.compressionMethod)
16 |
17 | """
18 | if let mtime = self.modificationTime {
19 | output += "Modification time: \(mtime)\n"
20 | }
21 | if let comment = self.comment {
22 | output += "Comment: \(comment)\n"
23 | }
24 | output += "Is text file: \(self.isTextFile)"
25 | return output
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/swcomp/Extensions/TarFormat+ConvertibleFromString.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import SwiftCLI
8 | import SWCompression
9 |
10 | // This extension allows to use TarContainer.Format as a Key option.
11 | extension TarContainer.Format: ConvertibleFromString {
12 |
13 | public init?(input: String) {
14 | switch input {
15 | case "prePosix":
16 | self = .prePosix
17 | case "ustar":
18 | self = .ustar
19 | case "gnu":
20 | self = .gnu
21 | case "pax":
22 | self = .pax
23 | default:
24 | return nil
25 | }
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/swcomp/SwcompError.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | enum SwcompError {
9 |
10 | case noOutputPath
11 | case lz4BigDictId
12 | case lz4NoDict
13 | case lz4BigBlockSize
14 | case benchmarkSmallIterCount
15 | case benchmarkUnknownCompResult
16 | case benchmarkCannotSetup(Benchmark.Type, String, Error)
17 | case benchmarkCannotMeasure(Benchmark.Type, Error)
18 | case benchmarkCannotMeasureBadOutSize(Benchmark.Type)
19 | case benchmarkReaderTarNoInputSize(String)
20 | case benchmarkCannotGetSubcommandPathWindows
21 | case benchmarkCannotAppendToDirectory
22 | case containerSymLinkDestPath(String)
23 | case containerHardLinkDestPath(String)
24 | case containerNoEntryData(String)
25 | case containerOutPathExistsNotDir
26 | case fileHandleCannotOpen
27 | case tarCreateXzNotSupported
28 | case tarCreateOutPathExists
29 | case tarCreateInPathDoesNotExist
30 |
31 | var errorCode: Int32 {
32 | switch self {
33 | case .noOutputPath:
34 | return 1
35 | case .lz4BigDictId:
36 | return 101
37 | case .lz4NoDict:
38 | return 102
39 | case .lz4BigBlockSize:
40 | return 103
41 | case .benchmarkSmallIterCount:
42 | return 201
43 | case .benchmarkUnknownCompResult:
44 | return 202
45 | case .benchmarkCannotSetup:
46 | return 203
47 | case .benchmarkCannotMeasure:
48 | return 204
49 | case .benchmarkCannotMeasureBadOutSize:
50 | return 214
51 | case .benchmarkReaderTarNoInputSize:
52 | return 205
53 | case .benchmarkCannotGetSubcommandPathWindows:
54 | return 206
55 | case .benchmarkCannotAppendToDirectory:
56 | return 207
57 | case .containerSymLinkDestPath:
58 | return 301
59 | case .containerHardLinkDestPath:
60 | return 311
61 | case .containerNoEntryData:
62 | return 302
63 | case .containerOutPathExistsNotDir:
64 | return 303
65 | case .fileHandleCannotOpen:
66 | return 401
67 | case .tarCreateXzNotSupported:
68 | return 501
69 | case .tarCreateOutPathExists:
70 | return 502
71 | case .tarCreateInPathDoesNotExist:
72 | return 503
73 | }
74 | }
75 |
76 | var message: String {
77 | switch self {
78 | case .noOutputPath:
79 | return "Unable to get output path and no output parameter was specified."
80 | case .lz4BigDictId:
81 | return "Too large dictionary ID."
82 | case .lz4NoDict:
83 | return "Dictionary ID is specified without specifying the dictionary itself."
84 | case .lz4BigBlockSize:
85 | return "Too big block size."
86 | case .benchmarkSmallIterCount:
87 | return "Iteration count, if set, must be not less than 1."
88 | case .benchmarkUnknownCompResult:
89 | return "Unknown comparison."
90 | case .benchmarkCannotSetup(let benchmark, let input, let error):
91 | return "Unable to set up benchmark \(benchmark): input=\(input), error=\(error)."
92 | case .benchmarkCannotMeasure(let benchmark, let error):
93 | return "Unable to measure benchmark \(benchmark), error=\(error)."
94 | case .benchmarkCannotMeasureBadOutSize(let benchmark):
95 | return "Unable to measure benchmark \(benchmark): outputData.count is not greater than zero."
96 | case .benchmarkReaderTarNoInputSize(let input):
97 | return "ReaderTAR.benchmarkSetUp(): file size is not available for input=\(input)."
98 | case .benchmarkCannotGetSubcommandPathWindows:
99 | return "Cannot get subcommand path on Windows. (This error should never be shown!)"
100 | case .benchmarkCannotAppendToDirectory:
101 | return "Cannot append results to the save path since it is a directory."
102 | case .containerSymLinkDestPath(let entryName):
103 | return "Unable to get destination path for symbolic link \(entryName)."
104 | case .containerHardLinkDestPath(let entryName):
105 | return "Unable to get destination path for hard link \(entryName)."
106 | case .containerNoEntryData(let entryName):
107 | return "Unable to get data for the entry \(entryName)."
108 | case .containerOutPathExistsNotDir:
109 | return "Specified output path already exists and is not a directory."
110 | case .fileHandleCannotOpen:
111 | return "Unable to open input file."
112 | case .tarCreateXzNotSupported:
113 | return "XZ compression is not supported when creating a container."
114 | case .tarCreateOutPathExists:
115 | return "Output path already exists."
116 | case .tarCreateInPathDoesNotExist:
117 | return "Specified input path doesn't exist."
118 | }
119 | }
120 | }
121 |
122 | func swcompExit(_ error: SwcompError) -> Never {
123 | print("\nERROR: \(error.message)")
124 | exit(error.errorCode)
125 | }
126 |
--------------------------------------------------------------------------------
/Sources/swcomp/main.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import SWCompression
8 | import SwiftCLI
9 |
10 | let _SWC_VERSION = "4.8.6"
11 |
12 | let cli = CLI(name: "swcomp", version: _SWC_VERSION,
13 | description: """
14 | swcomp - a small command-line client for SWCompression framework.
15 | Serves as an example of SWCompression usage.
16 | """)
17 | cli.parser.parseOptionsAfterCollectedParameter = true
18 | cli.commands = [XZCommand(),
19 | LZ4Command(),
20 | LZMACommand(),
21 | BZip2Command(),
22 | GZipCommand(),
23 | ZipCommand(),
24 | TarCommand(),
25 | SevenZipCommand(),
26 | BenchmarkGroup()]
27 | cli.goAndExit()
28 |
--------------------------------------------------------------------------------
/Tests/BZip2CompressionTests.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import XCTest
7 | import SWCompression
8 |
9 | class BZip2CompressionTests: XCTestCase {
10 |
11 | func answerTest(_ testName: String) throws {
12 | let answerData = try Constants.data(forAnswer: testName)
13 | let compressedData = BZip2.compress(data: answerData)
14 | let redecompressedData = try BZip2.decompress(data: compressedData)
15 | XCTAssertEqual(redecompressedData, answerData)
16 | if answerData.count > 0 { // Compression ratio is always bad for empty file.
17 | let compressionRatio = Double(answerData.count) / Double(compressedData.count)
18 | print(String(format: "BZip2.\(testName).compressionRatio = %.3f", compressionRatio))
19 | }
20 | }
21 |
22 | func stringTest(_ string: String) throws {
23 | let answerData = Data(string.utf8)
24 |
25 | let compressedData = BZip2.compress(data: answerData)
26 |
27 | let redecompressedData = try BZip2.decompress(data: compressedData)
28 | XCTAssertEqual(redecompressedData, answerData)
29 | }
30 |
31 | func testBZip2CompressStrings() throws {
32 | try stringTest("ban")
33 | try stringTest("banana")
34 | try stringTest("abaaba")
35 | try stringTest("abracadabra")
36 | try stringTest("cabbage")
37 | try stringTest("baabaabac")
38 | try stringTest("AAAAAAABBBBCCCD")
39 | try stringTest("AAAAAAA")
40 | try stringTest("qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890")
41 | }
42 |
43 | func testBZip2CompressBytes() throws {
44 | var bytes = ""
45 | for i: UInt8 in 0...255 {
46 | bytes += String(format: "%c", i)
47 | }
48 | try stringTest(bytes)
49 | }
50 |
51 | func testWithAnswer1BZip2Compress() throws {
52 | try answerTest("test1")
53 | }
54 |
55 | func testWithAnswer2BZip2Compress() throws {
56 | try answerTest("test2")
57 | }
58 |
59 | func testWithAnswer3BZip2Compress() throws {
60 | try answerTest("test3")
61 | }
62 |
63 | func testWithAnswer4BZip2Compress() throws {
64 | try answerTest("test4")
65 | }
66 |
67 | func testWithAnswer5BZip2Compress() throws {
68 | try answerTest("test5")
69 | }
70 |
71 | func testWithAnswer6BZip2Compress() throws {
72 | try answerTest("test6")
73 | }
74 |
75 | // func testWithAnswer7BZip2Compress() throws {
76 | // try answerTest("test7")
77 | // }
78 |
79 | func testWithAnswer8BZip2Compress() throws {
80 | try answerTest("test8")
81 | }
82 |
83 | func testWithAnswer9BZip2Compress() throws {
84 | try answerTest("test9")
85 | }
86 |
87 | func testBurrowsWheelerRoundtrip() throws {
88 | // This test is inspired by the reported issue #38 that uncovered a mistake with a pointer variable in BWT.
89 | // "1"s can be anything (except zero), but it must be the same byte value in all places.
90 | // Two consecutive zeros in the middle seem to be crucial for some reason.
91 | let testData = Data([0, 1, 0, 1, 0, 0, 1, 0, 1])
92 | let compressedData = BZip2.compress(data: testData)
93 | let redecompressedData = try BZip2.decompress(data: compressedData)
94 | XCTAssertEqual(redecompressedData, testData)
95 | }
96 |
97 | }
98 |
--------------------------------------------------------------------------------
/Tests/BZip2Tests.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import XCTest
7 | import SWCompression
8 |
9 | class BZip2Tests: XCTestCase {
10 |
11 | private static let testType: String = "bz2"
12 |
13 | func perform(test testName: String) throws {
14 | let testData = try Constants.data(forTest: testName, withType: BZip2Tests.testType)
15 | let decompressedData = try BZip2.decompress(data: testData)
16 |
17 | let answerData = try Constants.data(forAnswer: testName)
18 | XCTAssertEqual(decompressedData, answerData)
19 | }
20 |
21 | func test1BZip2() throws {
22 | try self.perform(test: "test1")
23 | }
24 |
25 | func test2BZip2() throws {
26 | try self.perform(test: "test2")
27 | }
28 |
29 | func test3BZip2() throws {
30 | try self.perform(test: "test3")
31 | }
32 |
33 | func test4BZip2() throws {
34 | try self.perform(test: "test4")
35 | }
36 |
37 | func test5BZip2() throws {
38 | try self.perform(test: "test5")
39 | }
40 |
41 | func test6BZip2() throws {
42 | try self.perform(test: "test6")
43 | }
44 |
45 | func test7BZip2() throws {
46 | try self.perform(test: "test7")
47 | }
48 |
49 | func test8BZip2() throws {
50 | try self.perform(test: "test8")
51 | }
52 |
53 | func test9BZip2() throws {
54 | try self.perform(test: "test9")
55 | }
56 |
57 | func testNonStandardRunLength() throws {
58 | try self.perform(test: "test_nonstandard_runlength")
59 | }
60 |
61 | func testBadFile_short() {
62 | XCTAssertThrowsError(try BZip2.decompress(data: Data([0])))
63 | }
64 |
65 | func testBadFile_invalid() throws {
66 | let testData = try Constants.data(forAnswer: "test6")
67 | XCTAssertThrowsError(try BZip2.decompress(data: testData))
68 | }
69 |
70 | func testBadFile_truncated() throws {
71 | // This tests that encountering data truncated in the middle of a Huffman symbol correctly throws an error
72 | // (and doesn't crash).
73 | let testData = try Constants.data(forTest: "test1", withType: BZip2Tests.testType)[0...40]
74 | XCTAssertThrowsError(try BZip2.decompress(data: testData))
75 | }
76 |
77 | func testEmptyData() throws {
78 | XCTAssertThrowsError(try BZip2.decompress(data: Data()))
79 | }
80 |
81 | func testChecksumMismatch() throws {
82 | // Here we test that an error for checksum mismatch is thrown correctly and its associated value contains
83 | // expected data. We do this by programmatically adjusting the input: we change one of the bytes for the checkum,
84 | // which makes it incorrect.
85 | var testData = try Constants.data(forTest: "test1", withType: BZip2Tests.testType)
86 | // The checksum is the last 4 bytes.
87 | testData[testData.endIndex - 2] &+= 1
88 | var thrownError: Error? = nil
89 | XCTAssertThrowsError(try BZip2.decompress(data: testData)) { thrownError = $0 }
90 | XCTAssertTrue(thrownError is BZip2Error, "Unexpected error type: \(type(of: thrownError))")
91 | if case let .some(.wrongCRC(decompressedData)) = thrownError as? BZip2Error {
92 | let answerData = try Constants.data(forAnswer: "test1")
93 | XCTAssertEqual(decompressedData, answerData)
94 | } else {
95 | XCTFail("Unexpected error: \(String(describing: thrownError))")
96 | }
97 | }
98 |
99 | }
100 |
--------------------------------------------------------------------------------
/Tests/Constants.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 |
8 | class Constants {
9 |
10 | /* Contents of test files:
11 | - test1: text file with "Hello, World!\n".
12 | - test2: text file with copyright free song lyrics from http://www.freesonglyrics.co.uk/lyrics13.html
13 | - test3: text file with random string from https://www.random.org/strings/
14 | - test4: text file with string "I'm a tester" repeated several times.
15 | - test5: empty file.
16 | - test6: file with size of 1MB containing nulls from /dev/zero.
17 | - test7: file with size of 1MB containing random bytes from /dev/urandom.
18 | - test8: text file from lzma_specification.
19 | - test9: file with size of 10KB containing random bytes from /dev/urandom.
20 | */
21 |
22 | static func data(forTest name: String, withType ext: String) throws -> Data {
23 | let url = Constants.url(forTest: name, withType: ext)
24 | return try Data(contentsOf: url)
25 | }
26 |
27 | static func handle(forTest name: String, withType ext: String) throws -> FileHandle {
28 | let url = Constants.url(forTest: name, withType: ext)
29 | return try FileHandle(forReadingFrom: url)
30 | }
31 |
32 | private static func url(forTest name: String, withType ext: String) -> URL {
33 | return testBundle.url(forResource: name, withExtension: ext)!
34 | }
35 |
36 | static func data(forAnswer name: String) throws -> Data {
37 | let url = Constants.url(forAnswer: name)
38 | return try Data(contentsOf: url)
39 | }
40 |
41 | private static func url(forAnswer name: String) -> URL {
42 | return testBundle.url(forResource: name, withExtension: "answer")!
43 | }
44 |
45 | private static let testBundle: Bundle = Bundle(for: Constants.self)
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/Tests/DeflateCompressionTests.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import XCTest
7 | import SWCompression
8 |
9 | class DeflateCompressionTests: XCTestCase {
10 |
11 | func answerTest(_ testName: String) throws {
12 | let answerData = try Constants.data(forAnswer: testName)
13 | let compressedData = Deflate.compress(data: answerData)
14 | let redecompressedData = try Deflate.decompress(data: compressedData)
15 | XCTAssertEqual(redecompressedData, answerData)
16 | if answerData.count > 0 { // Compression ratio is always bad for empty file.
17 | let compressionRatio = Double(answerData.count) / Double(compressedData.count)
18 | print(String(format: "Deflate.\(testName).compressionRatio = %.3f", compressionRatio))
19 | }
20 | }
21 |
22 | func stringTest(_ string: String) throws {
23 | let answerData = Data(string.utf8)
24 | let compressedData = Deflate.compress(data: answerData)
25 | let redecompressedData = try Deflate.decompress(data: compressedData)
26 | XCTAssertEqual(redecompressedData, answerData)
27 | }
28 |
29 | func testDeflateCompressStrings() throws {
30 | try stringTest("ban")
31 | try stringTest("banana")
32 | try stringTest("abaaba")
33 | try stringTest("abracadabra")
34 | try stringTest("cabbage")
35 | try stringTest("baabaabac")
36 | try stringTest("AAAAAAABBBBCCCD")
37 | try stringTest("AAAAAAA")
38 | try stringTest("qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890")
39 | }
40 |
41 | func testDeflate1() throws {
42 | try self.answerTest("test1")
43 | }
44 |
45 | func testDeflate2() throws {
46 | try self.answerTest("test2")
47 | }
48 |
49 | func testDeflate3() throws {
50 | try self.answerTest("test3")
51 | }
52 |
53 | func testDeflate4() throws {
54 | try self.answerTest("test4")
55 | }
56 |
57 | func testDeflate5() throws {
58 | try self.answerTest("test5")
59 | }
60 |
61 | func testDeflate6() throws {
62 | try self.answerTest("test6")
63 | }
64 |
65 | func testDeflate7() throws {
66 | try self.answerTest("test7")
67 | }
68 |
69 | func testDeflate8() throws {
70 | try self.answerTest("test8")
71 | }
72 |
73 | func testDeflate9() throws {
74 | try self.answerTest("test9")
75 | }
76 |
77 | func testTrickySequence() throws {
78 | // This test helped us find an issue with implementation (match index was wrongly used as cyclical index).
79 | // This test may become useless in the future if the encoder starts preferring creation of an uncompressed block
80 | // for this input due to changes to the compression logic.
81 | let answerData = Data([0x2E, 0x20, 0x2E, 0x20, 0x2E, 0x20, 0x20])
82 | let compressedData = Deflate.compress(data: answerData)
83 | let redecompressedData = try Deflate.decompress(data: compressedData)
84 | XCTAssertEqual(redecompressedData, answerData)
85 | }
86 |
87 | }
88 |
--------------------------------------------------------------------------------
/Tests/LzmaTests.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import XCTest
7 | import SWCompression
8 |
9 | class LzmaTests: XCTestCase {
10 |
11 | private static let testType: String = "lzma"
12 |
13 | func perform(test testName: String) throws {
14 | let testData = try Constants.data(forTest: testName, withType: LzmaTests.testType)
15 | let decompressedData = try LZMA.decompress(data: testData)
16 |
17 | let answerData = try Constants.data(forAnswer: "test8")
18 | XCTAssertEqual(decompressedData, answerData)
19 | }
20 |
21 | func testLzma8() throws {
22 | try self.perform(test: "test8")
23 | }
24 |
25 | func testLzma9() throws {
26 | try self.perform(test: "test9")
27 | }
28 |
29 | func testLzma10() throws {
30 | try self.perform(test: "test10")
31 | }
32 |
33 | func testLzma11() throws {
34 | try self.perform(test: "test11")
35 | }
36 |
37 | func testLzmaEmpty() throws {
38 | let testData = try Constants.data(forTest: "test_empty", withType: LzmaTests.testType)
39 | XCTAssertEqual(try LZMA.decompress(data: testData), Data())
40 | }
41 |
42 | func testBadFile_short() {
43 | // Not enough data for LZMA properties.
44 | XCTAssertThrowsError(try LZMA.decompress(data: Data([0, 1, 2, 3])))
45 | // Not enough data to initialize range decoder.
46 | XCTAssertThrowsError(try LZMA.decompress(data: Data([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])))
47 | }
48 |
49 | func testBadFile_invalid() throws {
50 | let testData = try Constants.data(forAnswer: "test7")
51 | XCTAssertThrowsError(try LZMA.decompress(data: testData))
52 | }
53 |
54 | func testEmptyData() throws {
55 | XCTAssertThrowsError(try LZMA.decompress(data: Data()))
56 | XCTAssertThrowsError(try LZMA2.decompress(data: Data()))
57 | }
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/Tests/Sha256Tests.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import XCTest
8 | @testable import SWCompression
9 |
10 | class Sha256Tests: XCTestCase {
11 |
12 | func test1() {
13 | let message = ""
14 | let answer = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
15 | let hash = Sha256.hash(data: Data(message.utf8))
16 | XCTAssertEqual(hash.map { String(format: "%02x", $0) }.joined(), answer)
17 | }
18 |
19 | func test2() {
20 | let message = "a"
21 | let answer = "ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb"
22 | let data = Data(message.utf8)
23 | let hash = Sha256.hash(data: data)
24 | XCTAssertEqual(hash.map { String(format: "%02x", $0) }.joined(), answer)
25 | }
26 |
27 | func test3() {
28 | let message = "abc"
29 | let answer = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
30 | let data = Data(message.utf8)
31 | let hash = Sha256.hash(data: data)
32 | XCTAssertEqual(hash.map { String(format: "%02x", $0) }.joined(), answer)
33 | }
34 |
35 | func test4() {
36 | let message = "message digest"
37 | let answer = "f7846f55cf23e14eebeab5b4e1550cad5b509e3348fbc4efa3a1413d393cb650"
38 | let data = Data(message.utf8)
39 | let hash = Sha256.hash(data: data)
40 | XCTAssertEqual(hash.map { String(format: "%02x", $0) }.joined(), answer)
41 | }
42 |
43 | func test5() {
44 | let message = "abcdefghijklmnopqrstuvwxyz"
45 | let answer = "71c480df93d6ae2f1efad1447c66c9525e316218cf51fc8d9ed832f2daf18b73"
46 | let data = Data(message.utf8)
47 | let hash = Sha256.hash(data: data)
48 | XCTAssertEqual(hash.map { String(format: "%02x", $0) }.joined(), answer)
49 | }
50 |
51 | func test6() {
52 | let message = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
53 | let answer = "db4bfcbd4da0cd85a60c3c37d3fbd8805c77f15fc6b1fdfe614ee0a7c8fdb4c0"
54 | let data = Data(message.utf8)
55 | let hash = Sha256.hash(data: data)
56 | XCTAssertEqual(hash.map { String(format: "%02x", $0) }.joined(), answer)
57 | }
58 |
59 | func test7() {
60 | let message = "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
61 | let answer = "f371bc4a311f2b009eef952dd83ca80e2b60026c8e935592d0f9c308453c813e"
62 | let data = Data(message.utf8)
63 | let hash = Sha256.hash(data: data)
64 | XCTAssertEqual(hash.map { String(format: "%02x", $0) }.joined(), answer)
65 | }
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/Tests/TestZipExtraField.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import BitByteData
8 | import SWCompression
9 |
10 | struct TestZipExtraField: ZipExtraField {
11 |
12 | static let id: UInt16 = 0x0646
13 |
14 | let size: Int
15 | let location: ZipExtraFieldLocation
16 |
17 | var helloString: String?
18 |
19 | init(_ byteReader: LittleEndianByteReader, _ size: Int, location: ZipExtraFieldLocation) {
20 | self.size = size
21 | self.location = location
22 | self.helloString = String(data: Data(byteReader.bytes(count: size)), encoding: .utf8)
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/Tests/XxHash32Tests.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import Foundation
7 | import XCTest
8 | @testable import SWCompression
9 |
10 | class XxHash32Tests: XCTestCase {
11 |
12 | func test1() {
13 | let message = ""
14 | let answer = 0x02cc5d05 as UInt32
15 | let hash = XxHash32.hash(data: Data(message.utf8))
16 | XCTAssertEqual(hash, answer)
17 | }
18 |
19 | func test2() {
20 | let message = "a"
21 | let answer = 0x550d7456 as UInt32
22 | let hash = XxHash32.hash(data: Data(message.utf8))
23 | XCTAssertEqual(hash, answer)
24 | }
25 |
26 | func test3() {
27 | let message = "abc"
28 | let answer = 0x32d153ff as UInt32
29 | let hash = XxHash32.hash(data: Data(message.utf8))
30 | XCTAssertEqual(hash, answer)
31 | }
32 |
33 | func test4() {
34 | let message = "message digest"
35 | let answer = 0x7c948494 as UInt32
36 | let hash = XxHash32.hash(data: Data(message.utf8))
37 | XCTAssertEqual(hash, answer)
38 | }
39 |
40 | func test5() {
41 | let message = "abcdefghijklmnopqrstuvwxyz"
42 | let answer = 0x63a14d5f as UInt32
43 | let hash = XxHash32.hash(data: Data(message.utf8))
44 | XCTAssertEqual(hash, answer)
45 | }
46 |
47 | func test6() {
48 | let message = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
49 | let answer = 0x9c285e64 as UInt32
50 | let hash = XxHash32.hash(data: Data(message.utf8))
51 | XCTAssertEqual(hash, answer)
52 | }
53 |
54 | func test7() {
55 | let message = "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
56 | let answer = 0x9c05f475 as UInt32
57 | let hash = XxHash32.hash(data: Data(message.utf8))
58 | XCTAssertEqual(hash, answer)
59 | }
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/Tests/ZlibTests.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024 Timofey Solomko
2 | // Licensed under MIT License
3 | //
4 | // See LICENSE for license information
5 |
6 | import XCTest
7 | import SWCompression
8 |
9 | class ZlibTests: XCTestCase {
10 |
11 | private static let testType: String = "zlib"
12 |
13 | func testZlib() throws {
14 | let testName = "test"
15 |
16 | let testData = try Constants.data(forTest: testName, withType: ZlibTests.testType)
17 | let testZlibHeader = try ZlibHeader(archive: testData)
18 |
19 | XCTAssertEqual(testZlibHeader.compressionMethod, .deflate)
20 | XCTAssertEqual(testZlibHeader.compressionLevel, .defaultAlgorithm)
21 | XCTAssertEqual(testZlibHeader.windowSize, 32768)
22 | }
23 |
24 | func testZlibFull() throws {
25 | let testData = try Constants.data(forTest: "random_file", withType: ZlibTests.testType)
26 | let decompressedData = try ZlibArchive.unarchive(archive: testData)
27 |
28 | let answerData = try Constants.data(forAnswer: "test9")
29 | XCTAssertEqual(decompressedData, answerData)
30 | }
31 |
32 | func testCreateZlib() throws {
33 | let testData = try Constants.data(forAnswer: "test9")
34 | let archiveData = ZlibArchive.archive(data: testData)
35 | let reextractedData = try ZlibArchive.unarchive(archive: archiveData)
36 |
37 | XCTAssertEqual(testData, reextractedData)
38 | }
39 |
40 | func testZlibEmpty() throws {
41 | let testData = try Constants.data(forTest: "test_empty", withType: ZlibTests.testType)
42 | XCTAssertEqual(try ZlibArchive.unarchive(archive: testData), Data())
43 | }
44 |
45 | func testBadFile_short() {
46 | XCTAssertThrowsError(try ZlibArchive.unarchive(archive: Data([0x78])))
47 | XCTAssertThrowsError(try ZlibHeader(archive: Data([0x78])))
48 | }
49 |
50 | func testBadFile_invalid() throws {
51 | let testData = try Constants.data(forAnswer: "test6")
52 | XCTAssertThrowsError(try ZlibArchive.unarchive(archive: testData))
53 | }
54 |
55 | func testEmptyData() throws {
56 | XCTAssertThrowsError(try ZlibArchive.unarchive(archive: Data()))
57 | }
58 |
59 | func testChecksumMismatch() throws {
60 | // Here we test that an error for checksum mismatch is thrown correctly and its associated value contains
61 | // expected data. We do this by programmatically adjusting the input: we change one of the bytes for the checkum,
62 | // which makes it incorrect.
63 | var testData = try Constants.data(forTest: "random_file", withType: ZlibTests.testType)
64 | // Here we modify the stored value of adler32.
65 | testData[10249] &+= 1
66 | var thrownError: Error? = nil
67 | XCTAssertThrowsError(try ZlibArchive.unarchive(archive: testData)) { thrownError = $0 }
68 | XCTAssertTrue(thrownError is ZlibError, "Unexpected error type: \(type(of: thrownError))")
69 | if case let .some(.wrongAdler32(decompressedData)) = thrownError as? ZlibError {
70 | let answerData = try Constants.data(forAnswer: "test9")
71 | XCTAssertEqual(decompressedData, answerData)
72 | } else {
73 | XCTFail("Unexpected error: \(String(describing: thrownError))")
74 | }
75 | }
76 |
77 | }
78 |
--------------------------------------------------------------------------------