├── .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 | --------------------------------------------------------------------------------