diff --git a/.jazzy.yaml b/.jazzy.yaml index 247e1986..9e1d8f58 100644 --- a/.jazzy.yaml +++ b/.jazzy.yaml @@ -3,11 +3,11 @@ sourcekitten_sourcefile: docs.json clean: false author: Timofey Solomko module: SWCompression -module_version: 4.5.11 +module_version: 4.6.0 copyright: '© 2021 Timofey Solomko' readme: README.md github_url: https://github.com/tsolomko/SWCompression -github_file_prefix: https://github.com/tsolomko/SWCompression/tree/4.5.11 +github_file_prefix: https://github.com/tsolomko/SWCompression/tree/4.6.0 theme: fullwidth custom_categories: diff --git a/CHANGELOG.md b/CHANGELOG.md index d47da297..85bb1312 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## 4.6.0 + +- Swift 4.2 is no longer supported. +- Minimum iOS deployment version (when installed via CocoaPods or Carthage) is now 9.0. +- Increased the lowest required version of BitByteData dependency to 2.0. +- Added the `TarContainer.create(from:force:)` function which allows to specify TAR format. + - It is now possible to create TAR containers of GNU, ustar (POSIX), and pre-POSIX formats. +- The `TarContainer.create(from:)` function (alongside with the newly added function) no longer throws. + - The `TarCreateError.utf8NonEncodable` error is now never thrown. +- Handling of truncated TAR containers should now be more consistent. + - Previously introduced check for truncated containers now throws the `TarError.tooSmallFileIsPassed` error instead + of `TarError.wrongField`. +- Documentation has been updated: + - Adjusted documentation of the `TarEntryInfo` properties to account for other formats used in creation of a + container. + - Adjusted documentation of the `TarError.tooSmallFileIsPassed` error to mention its use in all situations when + truncated data is encountered. +- swcomp changes: + - `zip -i` command now prints entry comments only if they are not empty. + - Slight grammatical improvements to the help messages of swcomp. + ## 4.5.11 - Fixed a crash when processing a truncated TAR file. diff --git a/Cartfile b/Cartfile index 2068ee10..656bf838 100644 --- a/Cartfile +++ b/Cartfile @@ -1 +1 @@ -github "tsolomko/BitByteData" ~> 1.4.4 +github "tsolomko/BitByteData" ~> 2.0.0 diff --git a/Package.swift b/Package.swift index 769ea9f1..e63587cd 100644 --- a/Package.swift +++ b/Package.swift @@ -13,7 +13,7 @@ let package = Package( // .package(url: "https://github.com/jakeheis/SwiftCLI", // from: "5.2.0"), .package(url: "https://github.com/tsolomko/BitByteData", - from: "1.4.4"), + from: "2.0.0"), ], targets: [ // SWCOMP: Uncomment the lines below to build swcomp example program. diff --git a/Package@swift-4.2.swift b/Package@swift-4.2.swift deleted file mode 100644 index d81037fa..00000000 --- a/Package@swift-4.2.swift +++ /dev/null @@ -1,32 +0,0 @@ -// swift-tools-version:4.0 -import PackageDescription - -let package = Package( - name: "SWCompression", - products: [ - .library( - name: "SWCompression", - targets: ["SWCompression"]), - ], - dependencies: [ - // SWCOMP: Uncomment the line below to build swcomp example program. - // .package(url: "https://github.com/jakeheis/SwiftCLI", - // from: "5.2.0"), - .package(url: "https://github.com/tsolomko/BitByteData", - from: "1.4.4"), - ], - targets: [ - // SWCOMP: Uncomment the lines below to build swcomp example program. - // .target( - // name: "swcomp", - // dependencies: ["SWCompression", "SwiftCLI"], - // path: "Sources", - // sources: ["swcomp"]), - .target( - name: "SWCompression", - dependencies: ["BitByteData"], - path: "Sources", - sources: ["Common", "7-Zip", "BZip2", "Deflate", "GZip", "LZMA", "LZMA2", "TAR", "XZ", "ZIP", "Zlib"]), - ], - swiftLanguageVersions: [4] -) diff --git a/README.md b/README.md index 92eb628b..d59f3905 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ # SWCompression -[![Swift 4.2](https://img.shields.io/badge/Swift-4.2-blue.svg)](https://developer.apple.com/swift/) [![Swift 5.X](https://img.shields.io/badge/Swift-5.X-blue.svg)](https://developer.apple.com/swift/) [![GitHub license](https://img.shields.io/badge/license-MIT-lightgrey.svg)](https://raw.githubusercontent.com/tsolomko/SWCompression/master/LICENSE) [![Build Status](https://travis-ci.com/tsolomko/SWCompression.svg?branch=develop)](https://travis-ci.com/tsolomko/SWCompression) @@ -10,7 +9,7 @@ A framework with (de)compression algorithms and functions for working with vario ## What is this? -SWCompression — is a framework with a collection of functions for: +SWCompression is a framework with a collection of functions for: 1. Decompression (and sometimes compression) using different algorithms. 2. Reading (and sometimes writing) archives of different formats. @@ -34,7 +33,7 @@ Also, SWCompression is _written with Swift only._ ## Installation -SWCompression can be integrated into your project using Swift Package Manager, CocoaPods or Carthage. +SWCompression can be integrated into your project using Swift Package Manager, CocoaPods, or Carthage. ### Swift Package Manager @@ -47,7 +46,7 @@ let package = Package( name: "PackageName", dependencies: [ .package(url: "https://github.com/tsolomko/SWCompression.git", - from: "4.5.0") + from: "4.6.0") ], targets: [ .target( @@ -58,11 +57,11 @@ let package = Package( ) ``` -More details you can find in [Swift Package Manager's Documentation](https://github.com/apple/swift-package-manager/tree/master/Documentation). +More details you can find in [Swift Package Manager's Documentation](https://github.com/apple/swift-package-manager/tree/main/Documentation). ### CocoaPods -Add `pod 'SWCompression', '~> 4.5'` and `use_frameworks!` lines to your Podfile. +Add `pod 'SWCompression', '~> 4.6'` and `use_frameworks!` lines to your Podfile. To complete installation, run `pod install`. @@ -105,19 +104,17 @@ BZip2 and LZMA/LZMA2 support). ### Carthage -__Important:__ Only Swift 5.x is supported when installing SWCompression via Carthage. - -Add to your Cartfile `github "tsolomko/SWCompression" ~> 4.5`. +Add to your Cartfile `github "tsolomko/SWCompression" ~> 4.6`. Then: -1. If you use Xcode 12 or later you should run `carthage update --use-xcframeworks --no-use-binaries`. After that drag -and drop both `SWCompression.xcframework` and `BitByteData.xcframework` files from from the `Carthage/Build/` directory -into the "Frameworks, Libraries, and Embedded Content" section of your target's "General" tab in Xcode. +1. If you use Xcode 12 or later you should run `carthage update --use-xcframeworks`. After that drag and drop both +`SWCompression.xcframework` and `BitByteData.xcframework` files from from the `Carthage/Build/` directory into the +"Frameworks, Libraries, and Embedded Content" section of your target's "General" tab in Xcode. -2. If you use Xcode 11 or earlier you should run `carthage update --no-use-binaries`. After that drag and drop both -`SWCompression.framework` and `BitByteData.framework` files from from the `Carthage/Build//` directory into the -"Embedded Binaries" section of your target's "General" tab in Xcode. +2. If you use Xcode 11 or earlier you should run `carthage update`. After that drag and drop both +`SWCompression.framework` and `BitByteData.framework` files from from the `Carthage/Build//` directory into +the "Embedded Binaries" section of your target's "General" tab in Xcode. For Xcode 12 or later you can currently also use the [xconfig workaround](https://github.com/Carthage/Carthage/blob/master/Documentation/Xcode12Workaround.md). @@ -165,7 +162,8 @@ do { ### Documentation Every function or type of SWCompression's public API is documented. This documentation can be found at its own -[website](http://tsolomko.github.io/SWCompression). +[website](http://tsolomko.github.io/SWCompression) or via a slightly shorter link: +[swcompression.tsolomko.me](http://swcompression.tsolomko.me) ### Sophisticated example @@ -175,7 +173,8 @@ you need to uncomment several lines in "Package.swift" and run `swift build -c r ## Contributing Whether you find a bug, have a suggestion, idea, feedback or something else, please -[create an issue](https://github.com/tsolomko/SWCompression/issues) on GitHub. +[create an issue](https://github.com/tsolomko/SWCompression/issues) on GitHub. If you have any questions, you can ask +them on the [Discussions](https://github.com/tsolomko/SWCompression/discussions) page. In the case of a bug, it will be especially helpful if you attach a file (archive, etc.) that caused the bug to occur. @@ -205,7 +204,7 @@ There are two reasons for this complicated setup. Firstly, some of these files c unfortunate if the users of SWCompression had to download them every time during the installation. Secondly, Swift Package Manager and contemporary versions of Xcode don't always work well with git-lfs-enabled repositories. To prevent any potential problems test files were moved into another repository. Additionaly, the custom command line tool `utils.py` -is used to work around issues occuring on certain user systems (see, for example, #9). +is used to work around issues occuring on certain user systems (see, for example, [#9](https://github.com/tsolomko/SWCompression/issues/9)). Please note, that if you want to add a new _type_ of test files, in addition to running `git lfs track`, you have to also copy into the "Tests/Test Files/gitattributes-copy" file a line this command adds to the "Tests/Test Files/.gitattributes" diff --git a/SWCompression.podspec b/SWCompression.podspec index a2544376..428d48fe 100644 --- a/SWCompression.podspec +++ b/SWCompression.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "SWCompression" - s.version = "4.5.11" + s.version = "4.6.0" s.summary = "A framework with functions for working with compression, archives and containers." s.description = "A framework with (de)compression algorithms and functions for processing various archives and containers." @@ -15,14 +15,14 @@ Pod::Spec.new do |s| s.source = { :git => "https://github.com/tsolomko/SWCompression.git", :tag => "#{s.version}" } - s.ios.deployment_target = "8.0" + s.ios.deployment_target = "9.0" s.osx.deployment_target = "10.10" s.tvos.deployment_target = "9.0" s.watchos.deployment_target = "2.0" - s.swift_versions = ["4", "5"] + s.swift_versions = ["5"] - s.dependency "BitByteData", "~> 1.4.4" + s.dependency "BitByteData", "~> 2.0" s.subspec "Deflate" do |sp| sp.source_files = "Sources/{Deflate/*,Common/*,Common/CodingTree/*}.swift" diff --git a/SWCompression.xcodeproj/SWCompression.plist b/SWCompression.xcodeproj/SWCompression.plist index 29025807..f6f8a4ef 100644 --- a/SWCompression.xcodeproj/SWCompression.plist +++ b/SWCompression.xcodeproj/SWCompression.plist @@ -15,9 +15,9 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 4.5.11 + 4.6.0 CFBundleVersion - 78 + 80 NSHumanReadableCopyright Copyright © 2021 Timofey Solomko diff --git a/SWCompression.xcodeproj/TestSWCompression.plist b/SWCompression.xcodeproj/TestSWCompression.plist index c6fd15cd..95886263 100644 --- a/SWCompression.xcodeproj/TestSWCompression.plist +++ b/SWCompression.xcodeproj/TestSWCompression.plist @@ -15,8 +15,8 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 4.5.11 + 4.6.0 CFBundleVersion - 78 + 80 diff --git a/SWCompression.xcodeproj/project.pbxproj b/SWCompression.xcodeproj/project.pbxproj index bd60d0fe..bb016374 100644 --- a/SWCompression.xcodeproj/project.pbxproj +++ b/SWCompression.xcodeproj/project.pbxproj @@ -54,7 +54,7 @@ 06516B782139505800C9823B /* Sha256Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06516B772139505800C9823B /* Sha256Tests.swift */; }; 0651FEC21F8E2E9100C49646 /* ContainerEntryType+Tar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0651FEC11F8E2E9100C49646 /* ContainerEntryType+Tar.swift */; }; 0656B8261F8A960700111605 /* TarEntryInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0656B8251F8A960700111605 /* TarEntryInfo.swift */; }; - 0656B82B1F8A979A00111605 /* ByteReader+Tar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0656B82A1F8A979A00111605 /* ByteReader+Tar.swift */; }; + 0656B82B1F8A979A00111605 /* LittleEndianByteReader+Tar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0656B82A1F8A979A00111605 /* LittleEndianByteReader+Tar.swift */; }; 06595E0E1F1E8252006501C2 /* 7zFolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06595E0D1F1E8252006501C2 /* 7zFolder.swift */; }; 06595E131F1E90C2006501C2 /* 7zCoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06595E121F1E90C2006501C2 /* 7zCoder.swift */; }; 065D0ADC1F2B9D9100CE2DA8 /* 7zEntryInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 065D0ADB1F2B9D9100CE2DA8 /* 7zEntryInfo.swift */; }; @@ -102,13 +102,13 @@ 06CDFCA81F111D9700292758 /* DeflateError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06CDFCA71F111D9700292758 /* DeflateError.swift */; }; 06CDFCAD1F111DBE00292758 /* LZMAError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06CDFCAC1F111DBE00292758 /* LZMAError.swift */; }; 06CDFCB21F111DEC00292758 /* LZMA2Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06CDFCB11F111DEC00292758 /* LZMA2Error.swift */; }; - 06CFF6B22078C5A5003B8375 /* TarEntryInfoProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06CFF6B12078C5A5003B8375 /* TarEntryInfoProvider.swift */; }; + 06CFF6B22078C5A5003B8375 /* TarParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06CFF6B12078C5A5003B8375 /* TarParser.swift */; }; 06D3802E21E36190008B50C6 /* Code.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06D3802D21E36190008B50C6 /* Code.swift */; }; 06D42DA52067B46800C1A98B /* test_custom_extra_field.zip in Resources */ = {isa = PBXBuildFile; fileRef = 06D42DA42067B46800C1A98B /* test_custom_extra_field.zip */; }; 06D42DA72067B71000C1A98B /* TestZipExtraField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06D42DA62067B71000C1A98B /* TestZipExtraField.swift */; }; 06D95AC11F4C54E0006B46AC /* BZip2+Compress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06D95AC01F4C54E0006B46AC /* BZip2+Compress.swift */; }; 06DDB1711F8190D400035BEF /* BZip2+BlockSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06DDB1701F8190D400035BEF /* BZip2+BlockSize.swift */; }; - 06DE29B81FB7024C0098A761 /* ByteReader+Zip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06DE29B71FB7024C0098A761 /* ByteReader+Zip.swift */; }; + 06DE29B81FB7024C0098A761 /* LittleEndianByteReader+Zip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06DE29B71FB7024C0098A761 /* LittleEndianByteReader+Zip.swift */; }; 06E2346C1E068E9600F18798 /* XZArchive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E2346B1E068E9600F18798 /* XZArchive.swift */; }; 06E234821E06A6C200F18798 /* CheckSums.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E234811E06A6C200F18798 /* CheckSums.swift */; }; 06E6A6391F5C0FBB00D04856 /* CodeLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E6A6381F5C0FBB00D04856 /* CodeLength.swift */; }; @@ -213,6 +213,9 @@ 06F066771FFB763400312A82 /* test8.bz2 in Resources */ = {isa = PBXBuildFile; fileRef = 06F066111FFB763300312A82 /* test8.bz2 */; }; 06F276DF1F2BAB4A00E67335 /* 7zEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06F276DE1F2BAB4900E67335 /* 7zEntry.swift */; }; 06FEAD921F54B9CD00AD016E /* EncodingTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06FEAD911F54B9CD00AD016E /* EncodingTree.swift */; }; + E60CBA8A26AF379200A20130 /* test_only_dir_header.tar in Resources */ = {isa = PBXBuildFile; fileRef = E60CBA8926AF379200A20130 /* test_only_dir_header.tar */; }; + E648151D26A889B8009261F0 /* TarHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = E648151C26A889B8009261F0 /* TarHeader.swift */; }; + E64F71BC26AAFD6A008BB308 /* Data+Tar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E64F71BB26AAFD6A008BB308 /* Data+Tar.swift */; }; E652D8F3263D670900FC229B /* test_nonstandard_runlength.bz2 in Resources */ = {isa = PBXBuildFile; fileRef = E652D8F2263D670900FC229B /* test_nonstandard_runlength.bz2 */; }; E652D8F5263D678000FC229B /* test_nonstandard_runlength.answer in Resources */ = {isa = PBXBuildFile; fileRef = E652D8F4263D678000FC229B /* test_nonstandard_runlength.answer */; }; E66F36242538726E00076A6E /* test_empty.lzma in Resources */ = {isa = PBXBuildFile; fileRef = E66F36232538726E00076A6E /* test_empty.lzma */; }; @@ -280,7 +283,7 @@ 06516B772139505800C9823B /* Sha256Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sha256Tests.swift; sourceTree = ""; }; 0651FEC11F8E2E9100C49646 /* ContainerEntryType+Tar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContainerEntryType+Tar.swift"; sourceTree = ""; }; 0656B8251F8A960700111605 /* TarEntryInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TarEntryInfo.swift; sourceTree = ""; }; - 0656B82A1F8A979A00111605 /* ByteReader+Tar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ByteReader+Tar.swift"; sourceTree = ""; }; + 0656B82A1F8A979A00111605 /* LittleEndianByteReader+Tar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LittleEndianByteReader+Tar.swift"; sourceTree = ""; }; 06595E0D1F1E8252006501C2 /* 7zFolder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 7zFolder.swift; sourceTree = ""; }; 06595E121F1E90C2006501C2 /* 7zCoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = 7zCoder.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 065D0ADB1F2B9D9100CE2DA8 /* 7zEntryInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 7zEntryInfo.swift; sourceTree = ""; }; @@ -325,13 +328,13 @@ 06CDFCA71F111D9700292758 /* DeflateError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeflateError.swift; sourceTree = ""; }; 06CDFCAC1F111DBE00292758 /* LZMAError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LZMAError.swift; sourceTree = ""; }; 06CDFCB11F111DEC00292758 /* LZMA2Error.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LZMA2Error.swift; sourceTree = ""; }; - 06CFF6B12078C5A5003B8375 /* TarEntryInfoProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TarEntryInfoProvider.swift; sourceTree = ""; }; + 06CFF6B12078C5A5003B8375 /* TarParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TarParser.swift; sourceTree = ""; }; 06D3802D21E36190008B50C6 /* Code.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Code.swift; sourceTree = ""; }; 06D42DA42067B46800C1A98B /* test_custom_extra_field.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = test_custom_extra_field.zip; sourceTree = ""; }; 06D42DA62067B71000C1A98B /* TestZipExtraField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestZipExtraField.swift; sourceTree = ""; }; 06D95AC01F4C54E0006B46AC /* BZip2+Compress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BZip2+Compress.swift"; sourceTree = ""; }; 06DDB1701F8190D400035BEF /* BZip2+BlockSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BZip2+BlockSize.swift"; sourceTree = ""; }; - 06DE29B71FB7024C0098A761 /* ByteReader+Zip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ByteReader+Zip.swift"; sourceTree = ""; }; + 06DE29B71FB7024C0098A761 /* LittleEndianByteReader+Zip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LittleEndianByteReader+Zip.swift"; sourceTree = ""; }; 06E2346B1E068E9600F18798 /* XZArchive.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XZArchive.swift; sourceTree = ""; }; 06E234811E06A6C200F18798 /* CheckSums.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CheckSums.swift; path = Sources/Common/CheckSums.swift; sourceTree = SOURCE_ROOT; }; 06E6A6381F5C0FBB00D04856 /* CodeLength.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CodeLength.swift; path = Sources/Common/CodingTree/CodeLength.swift; sourceTree = SOURCE_ROOT; }; @@ -437,6 +440,9 @@ 06F276DE1F2BAB4900E67335 /* 7zEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 7zEntry.swift; sourceTree = ""; }; 06FEAD911F54B9CD00AD016E /* EncodingTree.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = EncodingTree.swift; path = Sources/Common/CodingTree/EncodingTree.swift; sourceTree = SOURCE_ROOT; }; 06FED40B1DD7717E0013DFB2 /* BZip2.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BZip2.swift; sourceTree = ""; }; + E60CBA8926AF379200A20130 /* test_only_dir_header.tar */ = {isa = PBXFileReference; lastKnownFileType = archive.tar; path = test_only_dir_header.tar; sourceTree = ""; }; + E648151C26A889B8009261F0 /* TarHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TarHeader.swift; sourceTree = ""; }; + E64F71BB26AAFD6A008BB308 /* Data+Tar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Tar.swift"; sourceTree = ""; }; E652D8F2263D670900FC229B /* test_nonstandard_runlength.bz2 */ = {isa = PBXFileReference; lastKnownFileType = file; path = test_nonstandard_runlength.bz2; sourceTree = ""; }; E652D8F4263D678000FC229B /* test_nonstandard_runlength.answer */ = {isa = PBXFileReference; lastKnownFileType = file; path = test_nonstandard_runlength.answer; sourceTree = ""; }; E66F36232538726E00076A6E /* test_empty.lzma */ = {isa = PBXFileReference; lastKnownFileType = file; path = test_empty.lzma; sourceTree = ""; }; @@ -620,7 +626,7 @@ 061C06471F0E8FF500832F0C /* ZipCentralDirectoryEntry.swift */, 061C06511F0E904600832F0C /* ZipEndOfCentralDirectory.swift */, 06092A231FA4CC3D00DE9FD5 /* CompressionMethod+Zip.swift */, - 06DE29B71FB7024C0098A761 /* ByteReader+Zip.swift */, + 06DE29B71FB7024C0098A761 /* LittleEndianByteReader+Zip.swift */, 0686A6441FA4C47300E89C9E /* FileSystemType+Zip.swift */, ); path = ZIP; @@ -632,12 +638,14 @@ 06ADB2D81EBCEFA60053A188 /* TarContainer.swift */, 061C065B1F0E90E600832F0C /* TarEntry.swift */, 0656B8251F8A960700111605 /* TarEntryInfo.swift */, + 06CFF6B12078C5A5003B8375 /* TarParser.swift */, + E648151C26A889B8009261F0 /* TarHeader.swift */, 062C1D7B1F8FCA9300917968 /* TarExtendedHeader.swift */, 0698B10821048FB800A7C551 /* TarCreateError.swift */, 061C06561F0E90C800832F0C /* TarError.swift */, 0651FEC11F8E2E9100C49646 /* ContainerEntryType+Tar.swift */, - 0656B82A1F8A979A00111605 /* ByteReader+Tar.swift */, - 06CFF6B12078C5A5003B8375 /* TarEntryInfoProvider.swift */, + 0656B82A1F8A979A00111605 /* LittleEndianByteReader+Tar.swift */, + E64F71BB26AAFD6A008BB308 /* Data+Tar.swift */, ); path = TAR; sourceTree = ""; @@ -806,6 +814,7 @@ 06F065E01FFB763300312A82 /* test.tar */, 06F065E71FFB763300312A82 /* test_empty_dir.tar */, 06F065EC1FFB763300312A82 /* test_empty_cont.tar */, + E60CBA8926AF379200A20130 /* test_only_dir_header.tar */, 06F065ED1FFB763300312A82 /* SWCompressionSourceCode.tar */, 06F065EE1FFB763300312A82 /* test_empty_file.tar */, 06F065EF1FFB763300312A82 /* full_test.tar */, @@ -1037,6 +1046,7 @@ 06F066481FFB763400312A82 /* SWCompressionSourceCode.zip in Resources */, 06F0665D1FFB763400312A82 /* test8.lzma in Resources */, 06F0664F1FFB763400312A82 /* test_oldgnu.tar in Resources */, + E60CBA8A26AF379200A20130 /* test_only_dir_header.tar in Resources */, 06F066211FFB763300312A82 /* test9.answer in Resources */, 06F066221FFB763300312A82 /* test5.answer in Resources */, 06F0662C1FFB763400312A82 /* test7.gz in Resources */, @@ -1109,6 +1119,7 @@ buildActionMask = 2147483647; files = ( 061C06521F0E904600832F0C /* ZipEndOfCentralDirectory.swift in Sources */, + E64F71BC26AAFD6A008BB308 /* Data+Tar.swift in Sources */, 06BB8F301E0C2C720079FB9E /* LZMA2.swift in Sources */, 0620F4C31F8E24B6001C9B7A /* CompressionAlgorithm.swift in Sources */, 06955E491F65C761004D5D79 /* BZip2+Lengths.swift in Sources */, @@ -1135,7 +1146,7 @@ 06A172831FA5D3770048A89C /* LZMA2Decoder.swift in Sources */, 061C06571F0E90C800832F0C /* TarError.swift in Sources */, 06A960561F1E7D0B0078E6D1 /* 7zPackInfo.swift in Sources */, - 06DE29B81FB7024C0098A761 /* ByteReader+Zip.swift in Sources */, + 06DE29B81FB7024C0098A761 /* LittleEndianByteReader+Zip.swift in Sources */, 06BAC5941F8CFF28002443F4 /* ZipEntryInfo.swift in Sources */, 0625C49F1FB4B92F00A52C43 /* CompressionMethod+7z.swift in Sources */, 0620F4B31F8E2477001C9B7A /* ContainerEntry.swift in Sources */, @@ -1150,6 +1161,7 @@ 061C063D1F0E8AB700832F0C /* ZipError.swift in Sources */, 0606CF8120641209002B6EE9 /* BuiltinExtraFields.swift in Sources */, 0631CB061F81109E00A2E0AB /* Deflate+Constants.swift in Sources */, + E648151D26A889B8009261F0 /* TarHeader.swift in Sources */, 06D95AC11F4C54E0006B46AC /* BZip2+Compress.swift in Sources */, 06912BD21F5C8F3D0070BB60 /* Deflate+Lengths.swift in Sources */, 06A9606A1F1E7E0D0078E6D1 /* 7zSubstreamInfo.swift in Sources */, @@ -1171,7 +1183,7 @@ 0686A6451FA4C47300E89C9E /* FileSystemType+Zip.swift in Sources */, 06CDFCA31F111D6C00292758 /* BZip2Error.swift in Sources */, 06D3802E21E36190008B50C6 /* Code.swift in Sources */, - 06CFF6B22078C5A5003B8375 /* TarEntryInfoProvider.swift in Sources */, + 06CFF6B22078C5A5003B8375 /* TarParser.swift in Sources */, 06CC3FE21F8AAF3100BD576D /* ByteReader+XZ.swift in Sources */, 065D0ADC1F2B9D9100CE2DA8 /* 7zEntryInfo.swift in Sources */, 06F276DF1F2BAB4A00E67335 /* 7zEntry.swift in Sources */, @@ -1193,7 +1205,7 @@ 06CDFCAD1F111DBE00292758 /* LZMAError.swift in Sources */, 06E6A6391F5C0FBB00D04856 /* CodeLength.swift in Sources */, 0642998B206017590044BA0C /* ZipExtraField.swift in Sources */, - 0656B82B1F8A979A00111605 /* ByteReader+Tar.swift in Sources */, + 0656B82B1F8A979A00111605 /* LittleEndianByteReader+Tar.swift in Sources */, 06B689581F278815007C9316 /* 7zProperty.swift in Sources */, 0686A63B1FA4C07D00E89C9E /* FileSystemType.swift in Sources */, 06516B76213938B000C9823B /* Sha256.swift in Sources */, @@ -1258,7 +1270,7 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CURRENT_PROJECT_VERSION = 78; + CURRENT_PROJECT_VERSION = 80; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -1298,7 +1310,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MACOSX_DEPLOYMENT_TARGET = 10.10; ONLY_ACTIVE_ARCH = YES; OTHER_CODE_SIGN_FLAGS = "--deep"; @@ -1339,7 +1351,7 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CURRENT_PROJECT_VERSION = 78; + CURRENT_PROJECT_VERSION = 80; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_STRICT_OBJC_MSGSEND = YES; "FRAMEWORK_SEARCH_PATHS[sdk=appletvos*]" = ( @@ -1377,7 +1389,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MACOSX_DEPLOYMENT_TARGET = 10.10; OTHER_CODE_SIGN_FLAGS = "--deep"; OTHER_LDFLAGS = ( @@ -1400,7 +1412,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 78; + DYLIB_CURRENT_VERSION = 80; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = SWCompression.xcodeproj/SWCompression.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -1427,7 +1439,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 78; + DYLIB_CURRENT_VERSION = 80; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = SWCompression.xcodeproj/SWCompression.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; diff --git a/Sources/7-Zip/7zContainer.swift b/Sources/7-Zip/7zContainer.swift index 3b564250..ff7b1892 100644 --- a/Sources/7-Zip/7zContainer.swift +++ b/Sources/7-Zip/7zContainer.swift @@ -49,10 +49,10 @@ public class SevenZipContainer: Container { /// Combined calculated CRC of entire folder == all files in folder. var folderCRC = CheckSums.crc32(Data()) - /// `ByteReader` object with unpacked stream's data. - var unpackedStreamData = ByteReader(data: Data()) + /// `LittleEndianByteReader` object with unpacked stream's data. + var unpackedStreamData = LittleEndianByteReader(data: Data()) - let byteReader = ByteReader(data: data) + let byteReader = LittleEndianByteReader(data: data) for file in files { if file.isEmptyStream { @@ -108,7 +108,7 @@ public class SevenZipContainer: Container { // One stream can contain data for several files, so we need to decode the stream first, then split // it into files. - unpackedStreamData = ByteReader(data: try folder.unpack(data: streamData)) + unpackedStreamData = LittleEndianByteReader(data: try folder.unpack(data: streamData)) } // `SevenZipSubstreamInfo` object must contain information about file's size and may also contain diff --git a/Sources/7-Zip/7zFolder.swift b/Sources/7-Zip/7zFolder.swift index e3c6140d..a463f3e5 100644 --- a/Sources/7-Zip/7zFolder.swift +++ b/Sources/7-Zip/7zFolder.swift @@ -164,7 +164,7 @@ class SevenZipFolder { properties.count == 1 else { throw LZMA2Error.wrongDictionarySize } - decodedData = try LZMA2.decompress(ByteReader(data: decodedData), properties[0]) + decodedData = try LZMA2.decompress(LittleEndianByteReader(data: decodedData), properties[0]) case .lzma: // Both properties' byte (lp, lc, pb) and dictionary size are stored in coder's properties. guard let properties = coder.properties, @@ -185,7 +185,7 @@ class SevenZipFolder { properties.count == 1 else { throw SevenZipError.internalStructureError } - decodedData = DeltaFilter.decode(ByteReader(data: decodedData), (properties[0] &+ 1).toInt()) + decodedData = DeltaFilter.decode(LittleEndianByteReader(data: decodedData), (properties[0] &+ 1).toInt()) } else if coder.isEncryptionMethod { throw SevenZipError.encryptionNotSupported } else { diff --git a/Sources/Common/DeltaFilter.swift b/Sources/Common/DeltaFilter.swift index 87538663..02198d43 100644 --- a/Sources/Common/DeltaFilter.swift +++ b/Sources/Common/DeltaFilter.swift @@ -6,9 +6,9 @@ import Foundation import BitByteData -final class DeltaFilter { +enum DeltaFilter { - static func decode(_ byteReader: ByteReader, _ distance: Int) -> Data { + static func decode(_ byteReader: LittleEndianByteReader, _ distance: Int) -> Data { var out = [UInt8]() var pos = 0 diff --git a/Sources/Common/Extensions.swift b/Sources/Common/Extensions.swift index 2e6f97f2..700e16d6 100644 --- a/Sources/Common/Extensions.swift +++ b/Sources/Common/Extensions.swift @@ -7,7 +7,7 @@ import Foundation extension UnsignedInteger { - @inline(__always) + @inlinable @inline(__always) func toInt() -> Int { return Int(truncatingIfNeeded: self) } @@ -16,12 +16,12 @@ extension UnsignedInteger { extension Int { - @inline(__always) + @inlinable @inline(__always) func toUInt8() -> UInt8 { return UInt8(truncatingIfNeeded: UInt(self)) } - @inline(__always) + @inlinable @inline(__always) func roundTo512() -> Int { if self >= Int.max - 510 { return Int.max diff --git a/Sources/GZip/GzipHeader.swift b/Sources/GZip/GzipHeader.swift index b90ca47d..b505ead7 100644 --- a/Sources/GZip/GzipHeader.swift +++ b/Sources/GZip/GzipHeader.swift @@ -53,27 +53,27 @@ public struct GzipHeader { it might not be archived with GZip at all. */ public init(archive data: Data) throws { - let byteReader = ByteReader(data: data) - try self.init(byteReader) + let reader = LsbBitReader(data: data) + try self.init(reader) } - init(_ byteReader: ByteReader) throws { - // Valid GZip header must contain at least 2 bytes of data. - guard byteReader.bytesLeft >= 10 + init(_ reader: LsbBitReader) throws { + // Valid GZip header must contain at least 10 bytes of data. + guard reader.bytesLeft >= 10 else { throw GzipError.wrongMagic } // First two bytes should be correct 'magic' bytes - let magic = byteReader.uint16() + let magic = reader.uint16() guard magic == 0x8b1f else { throw GzipError.wrongMagic } var headerBytes: [UInt8] = [0x1f, 0x8b] // Third byte is a method of compression. Only type 8 (DEFLATE) compression is supported for GZip archives. - let method = byteReader.byte() + let method = reader.byte() guard method == 8 else { throw GzipError.wrongCompressionMethod } headerBytes.append(method) self.compressionMethod = .deflate - let rawFlags = byteReader.byte() + let rawFlags = reader.byte() guard rawFlags & 0xE0 == 0 else { throw GzipError.wrongFlags } let flags = Flags(rawValue: rawFlags) @@ -81,16 +81,16 @@ public struct GzipHeader { var mtime = 0 for i in 0..<4 { - let byte = byteReader.byte() + let byte = reader.byte() mtime |= byte.toInt() << (8 * i) headerBytes.append(byte) } self.modificationTime = mtime == 0 ? nil : Date(timeIntervalSince1970: TimeInterval(mtime)) - let extraFlags = byteReader.byte() + let extraFlags = reader.byte() headerBytes.append(extraFlags) - let rawOsType = byteReader.byte() + let rawOsType = reader.byte() self.osType = FileSystemType(rawOsType) headerBytes.append(rawOsType) @@ -100,12 +100,12 @@ public struct GzipHeader { if flags.contains(.fextra) { var xlen = 0 for i in 0..<2 { - let byte = byteReader.byte() + let byte = reader.byte() xlen |= byte.toInt() << (8 * i) headerBytes.append(byte) } for _ in 0..= 13 else { throw LZMAError.wrongProperties } - let byteReader = ByteReader(data: data) + let byteReader = LittleEndianByteReader(data: data) let properties = try LZMAProperties(byteReader) let uncompSize = byteReader.int(fromBytes: 8) return try decompress(byteReader, properties, uncompSize) @@ -56,11 +56,11 @@ public class LZMA: DecompressionAlgorithm { public static func decompress(data: Data, properties: LZMAProperties, uncompressedSize: Int? = nil) throws -> Data { - let byteReader = ByteReader(data: data) + let byteReader = LittleEndianByteReader(data: data) return try decompress(byteReader, properties, uncompressedSize) } - static func decompress(_ byteReader: ByteReader, + static func decompress(_ byteReader: LittleEndianByteReader, _ properties: LZMAProperties, _ uncompSize: Int?) throws -> Data { var decoder = LZMADecoder(byteReader) diff --git a/Sources/LZMA/LZMAConstants.swift b/Sources/LZMA/LZMAConstants.swift index 8b051662..5e2fe5e7 100644 --- a/Sources/LZMA/LZMAConstants.swift +++ b/Sources/LZMA/LZMAConstants.swift @@ -5,7 +5,8 @@ import Foundation -struct LZMAConstants { +enum LZMAConstants { + static let topValue: UInt32 = 1 << 24 static let numBitModelTotalBits = 11 static let numMoveBits = 5 @@ -19,4 +20,5 @@ struct LZMAConstants { static let numFullDistances = 1 << (endPosModelIndex >> 1) static let matchMinLen = 2 // LZMAConstants.numStates << LZMAConstants.numPosBitsMax = 192 + } diff --git a/Sources/LZMA/LZMADecoder.swift b/Sources/LZMA/LZMADecoder.swift index 9fd635c5..6c2ea639 100644 --- a/Sources/LZMA/LZMADecoder.swift +++ b/Sources/LZMA/LZMADecoder.swift @@ -8,7 +8,7 @@ import BitByteData struct LZMADecoder { - private let byteReader: ByteReader + private let byteReader: LittleEndianByteReader var properties = LZMAProperties() @@ -72,7 +72,7 @@ struct LZMADecoder { /// Used to select exact variable from 'IsRep', 'IsRepG0', 'IsRepG1' and 'IsRepG2' arrays. private var state = 0 - init(_ byteReader: ByteReader) { + init(_ byteReader: LittleEndianByteReader) { self.byteReader = byteReader } diff --git a/Sources/LZMA/LZMAProperties.swift b/Sources/LZMA/LZMAProperties.swift index 79051e3e..5656b17f 100644 --- a/Sources/LZMA/LZMAProperties.swift +++ b/Sources/LZMA/LZMAProperties.swift @@ -59,7 +59,7 @@ public struct LZMAProperties { self.dictionarySize = dictSize } - init(_ byteReader: ByteReader) throws { + init(_ byteReader: LittleEndianByteReader) throws { try self.init(lzmaByte: byteReader.byte(), byteReader.int(fromBytes: 4)) } diff --git a/Sources/LZMA/LZMARangeDecoder.swift b/Sources/LZMA/LZMARangeDecoder.swift index cff405fa..07ca582f 100644 --- a/Sources/LZMA/LZMARangeDecoder.swift +++ b/Sources/LZMA/LZMARangeDecoder.swift @@ -8,7 +8,7 @@ import BitByteData struct LZMARangeDecoder { - private let byteReader: ByteReader + private let byteReader: LittleEndianByteReader private var range = 0xFFFFFFFF as UInt32 private var code = 0 as UInt32 @@ -18,8 +18,8 @@ struct LZMARangeDecoder { return self.code == 0 } - init(_ byteReader: ByteReader) throws { - // To initialize rande decoder at least 5 bytes are necessary. + init(_ byteReader: LittleEndianByteReader) throws { + // To initialize range decoder at least 5 bytes are necessary. guard byteReader.bytesLeft >= 5 else { throw LZMAError.rangeDecoderInitError } @@ -34,7 +34,7 @@ struct LZMARangeDecoder { } init() { - self.byteReader = ByteReader(data: Data()) + self.byteReader = LittleEndianByteReader(data: Data()) } /// `range` property cannot be smaller than `(1 << 24)`. This function keeps it bigger. diff --git a/Sources/LZMA2/LZMA2.swift b/Sources/LZMA2/LZMA2.swift index 6bf16dfd..cafa914b 100644 --- a/Sources/LZMA2/LZMA2.swift +++ b/Sources/LZMA2/LZMA2.swift @@ -23,11 +23,11 @@ public class LZMA2: DecompressionAlgorithm { - Returns: Decompressed data. */ public static func decompress(data: Data) throws -> Data { - let byteReader = ByteReader(data: data) + let byteReader = LittleEndianByteReader(data: data) return try decompress(byteReader, byteReader.byte()) } - static func decompress(_ byteReader: ByteReader, _ dictSizeByte: UInt8) throws -> Data { + static func decompress(_ byteReader: LittleEndianByteReader, _ dictSizeByte: UInt8) throws -> Data { var decoder = try LZMA2Decoder(byteReader, dictSizeByte) try decoder.decode() return Data(decoder.out) diff --git a/Sources/LZMA2/LZMA2Decoder.swift b/Sources/LZMA2/LZMA2Decoder.swift index 46f5f81a..897dd19c 100644 --- a/Sources/LZMA2/LZMA2Decoder.swift +++ b/Sources/LZMA2/LZMA2Decoder.swift @@ -8,14 +8,14 @@ import BitByteData struct LZMA2Decoder { - private let byteReader: ByteReader + private let byteReader: LittleEndianByteReader private var decoder: LZMADecoder var out: [UInt8] { return self.decoder.out } - init(_ byteReader: ByteReader, _ dictSizeByte: UInt8) throws { + init(_ byteReader: LittleEndianByteReader, _ dictSizeByte: UInt8) throws { self.byteReader = byteReader self.decoder = LZMADecoder(byteReader) diff --git a/Sources/TAR/Data+Tar.swift b/Sources/TAR/Data+Tar.swift new file mode 100644 index 00000000..9c9ed4fb --- /dev/null +++ b/Sources/TAR/Data+Tar.swift @@ -0,0 +1,61 @@ +// Copyright (c) 2021 Timofey Solomko +// Licensed under MIT License +// +// See LICENSE for license information + +import Foundation + +extension Data { + + @inline(__always) + mutating func appendAsTarBlock(_ data: Data) { + self.append(data) + let paddingSize = data.count.roundTo512() - data.count + self.append(Data(count: paddingSize)) + } + + mutating func append(tarInt value: Int?, maxLength: Int) { + guard var value = value else { + // No value; fill field with NULLs. + self.append(Data(count: maxLength)) + return + } + + let maxOctalValue = (1 << (maxLength * 3)) - 1 + guard value > maxOctalValue || value < 0 else { + // Normal octal encoding. + self.append(Data(String(value, radix: 8).utf8).zeroPad(maxLength)) + return + } + + // Base-256 encoding. + // As long as we have at least 8 bytes for our value, conversion to base-256 will always succeed, since (64-bit) + // Int.max neatly fits into 8 bytes of 256-base encoding. + assert(maxLength >= 8 && Int.bitWidth <= 64) + var buffer = Array(repeating: 0 as UInt8, count: maxLength) + for i in stride(from: maxLength - 1, to: 0, by: -1) { + buffer[i] = UInt8(truncatingIfNeeded: value & 0xFF) + value >>= 8 + } + buffer[0] |= 0x80 // Highest bit indicates base-256 encoding. + self.append(Data(buffer)) + } + + mutating func append(tarString string: String?, maxLength: Int) { + guard let string = string else { + // No value; fill field with NULLs. + self.append(Data(count: maxLength)) + return + } + self.append(Data(string.utf8).zeroPad(maxLength)) + } + + /// This should work in the same way as `String.padding(toLength: length, withPad: "\0", startingAt: 0)`. + @inline(__always) + private func zeroPad(_ length: Int) -> Data { + var out = length < self.count ? self.prefix(upTo: length) : self + out.append(Data(count: length - out.count)) + return out + } + +} diff --git a/Sources/TAR/ByteReader+Tar.swift b/Sources/TAR/LittleEndianByteReader+Tar.swift similarity index 96% rename from Sources/TAR/ByteReader+Tar.swift rename to Sources/TAR/LittleEndianByteReader+Tar.swift index 2cf29c7c..d0a83896 100644 --- a/Sources/TAR/ByteReader+Tar.swift +++ b/Sources/TAR/LittleEndianByteReader+Tar.swift @@ -6,7 +6,7 @@ import Foundation import BitByteData -extension ByteReader { +extension LittleEndianByteReader { /** Reads a `String` field from TAR container. The end of the field is defined by either: @@ -35,8 +35,6 @@ extension ByteReader { Integers are encoded in TAR as ASCII text. We are treating them as UTF-8 encoded strings since UTF-8 is backwards compatible with ASCII. - - Integers can also be encoded as non-decimal based number. This is controlled by `radix` parameter. */ func tarInt(maxLength: Int) -> Int? { guard maxLength > 0 diff --git a/Sources/TAR/TarContainer.swift b/Sources/TAR/TarContainer.swift index a8ede371..5c28adce 100644 --- a/Sources/TAR/TarContainer.swift +++ b/Sources/TAR/TarContainer.swift @@ -35,24 +35,22 @@ public class TarContainer: Container { - SeeAlso: `TarContainer.Format` */ public static func formatOf(container data: Data) throws -> Format { - // TAR container should be at least 512 bytes long (when it contains only one header). - guard data.count >= 512 - else { throw TarError.tooSmallFileIsPassed } - - /// Object with input data which supports convenient work with bit shifts. - var infoProvider = TarEntryInfoProvider(data) - + var parser = TarParser(data) var ustarEncountered = false - while let info = try infoProvider.next() { - if info.specialEntryType == .globalExtendedHeader || info.specialEntryType == .localExtendedHeader { - return .pax - } else if info.specialEntryType == .longName || info.specialEntryType == .longLinkName { - return .gnu - } else { - switch info.format { - case .pax: + parsingLoop: while true { + let result = try parser.next() + switch result { + case .specialEntry(let specialEntryType): + if specialEntryType == .globalExtendedHeader || specialEntryType == .localExtendedHeader { return .pax + } else if specialEntryType == .longName || specialEntryType == .longLinkName { + return .gnu + } + case .entryInfo(_, _, let headerFormat): + switch headerFormat { + case .pax: + fatalError("Unexpected format of basic header: pax") case .gnu: return .gnu case .ustar: @@ -60,6 +58,13 @@ public class TarContainer: Container { case .prePosix: break } + case .truncated: + // We don't have an error with a more suitable name. + throw TarError.tooSmallFileIsPassed + case .finished: + fallthrough + case .eofMarker: + break parsingLoop } } @@ -71,33 +76,98 @@ public class TarContainer: Container { - Parameter entries: TAR entries to store in the container. - - Throws: `TarCreateError.utf8NonEncodable` which indicates that one of the `TarEntryInfo`'s string properties - (such as `name`) cannot be encoded with UTF-8 encoding. + - SeeAlso: `TarEntryInfo` properties documenation to see how their values are connected with the specific TAR + format used during container creation. + */ + public static func create(from entries: [TarEntry]) -> Data { + return create(from: entries, force: .pax) + } + + /** + Creates a new TAR container with `entries` as its content and generates its `Data` using the specified `format`. + + This function forces the usage of the `format`, meaning that certain properties about the `entries` may be missing + from the resulting container data if the chosen format doesn't support certain features. For example, relatively + long names (and linknames) will be truncated if the `.ustar` or `.prePosix` format is specified. + + It is highly recommended to use the `TarContainer.create(from:)` function (or use the `.pax` format) to ensure the + best representation of the `entries` in the output. Other (non-PAX) formats should only be used if you have a + specific need for them and you understand limitations of those formats. + + - Parameter entries: TAR entries to store in the container. + - Parameter force: For the usage of the specified format. - SeeAlso: `TarEntryInfo` properties documenation to see how their values are connected with the specific TAR format used during container creation. */ - public static func create(from entries: [TarEntry]) throws -> Data { + public static func create(from entries: [TarEntry], force format: TarContainer.Format) -> Data { + // The general strategy is as follows. For each entry we: + // 1. Create special entries if required by the entry's info and if supported by the format. + // 2. For each special entry we create a TarHeader. + // 3. For each TarHeader we generate binary data, and then append it with the content of the special entry to + // the output. + // 4. Perform the previous two steps for the entry itself. + // Every time we append something to the output we also make sure that the data is padded to 512 byte-long blocks. + + // In theory if the counters are big enough, the names of the special entries can become long enough to cause + // problems (truncation, etc.). In practice, the largest possible counter (UInt.max) is 20 symbols long, which + // when combined with the longest used special entry name can never cause any problems, since it is still + // shorter then 99 symbols available in the "name" field of the TAR header. + // However, if in the distant future Int.max becomes large enough to cause any issues (e.g. 128-bit and higher + // integers), the following check will catch it. + assert(String(UInt.max).count < 100 - 19) // "SWC_LocalPaxHeader_".count == 19 + // We also use &+ when incrementing counters to prevent integer overflow crashes: we rather deal with the special + // entries having repeating names, then crash the program. + var longNameCounter = 0 as UInt + var longLinkNameCounter = 0 as UInt + var localPaxHeaderCounter = 0 as UInt + var out = Data() - var extHeadersCount = 0 for entry in entries { - let extHeader = TarExtendedHeader(entry.info) - let extHeaderData = try extHeader.generateContainerData() - if !extHeaderData.isEmpty { - var extHeaderInfo = TarEntryInfo(name: "SWC_PaxHeader_\(extHeadersCount)", type: .unknown) - extHeaderInfo.specialEntryType = .localExtendedHeader - extHeaderInfo.permissions = Permissions(rawValue: 420) - extHeaderInfo.ownerID = entry.info.ownerID - extHeaderInfo.groupID = entry.info.groupID - extHeaderInfo.modificationTime = Date() + if format == .gnu { + if entry.info.name.utf8.count > 100 { + let nameData = Data(entry.info.name.utf8) + let longNameHeader = TarHeader(specialName: "SWC_LongName_\(longNameCounter)", + specialType: .longName, size: nameData.count, + uid: entry.info.ownerID, gid: entry.info.groupID) + out.append(longNameHeader.generateContainerData(.gnu)) + assert(out.count % 512 == 0) + out.appendAsTarBlock(nameData) + longNameCounter &+= 1 + } - let extHeaderEntry = TarEntry(info: extHeaderInfo, data: extHeaderData) - try out.append(extHeaderEntry.generateContainerData()) + if entry.info.linkName.utf8.count > 100 { + let linkNameData = Data(entry.info.linkName.utf8) + let longLinkNameHeader = TarHeader(specialName: "SWC_LongLinkName_\(longLinkNameCounter)", + specialType: .longLinkName, size: linkNameData.count, + uid: entry.info.ownerID, gid: entry.info.groupID) + out.append(longLinkNameHeader.generateContainerData(.gnu)) + assert(out.count % 512 == 0) + out.appendAsTarBlock(linkNameData) + longLinkNameCounter &+= 1 + } + } else if format == .pax { + let extHeader = TarExtendedHeader(entry.info) + let extHeaderData = extHeader.generateContainerData() + if !extHeaderData.isEmpty { + let extHeaderHeader = TarHeader(specialName: "SWC_LocalPaxHeader_\(localPaxHeaderCounter)", + specialType: .localExtendedHeader, size: extHeaderData.count, + uid: entry.info.ownerID, gid: entry.info.groupID) + out.append(extHeaderHeader.generateContainerData(.pax)) + assert(out.count % 512 == 0) + out.appendAsTarBlock(extHeaderData) + localPaxHeaderCounter &+= 1 + } + } - extHeadersCount += 1 + let header = TarHeader(entry.info) + out.append(header.generateContainerData(format)) + assert(out.count % 512 == 0) + if let data = entry.data { + out.appendAsTarBlock(data) } - try out.append(entry.generateContainerData()) } + // Two 512-byte blocks consisting of zeros as an EOF marker. out.append(Data(count: 1024)) return out } @@ -116,25 +186,38 @@ public class TarContainer: Container { - Returns: Array of `TarEntry`. */ public static func open(container data: Data) throws -> [TarEntry] { - let infos = try info(container: data) + var parser = TarParser(data) var entries = [TarEntry]() - for entryInfo in infos { - if entryInfo.type == .directory { - var entry = TarEntry(info: entryInfo, data: nil) - entry.info.size = 0 - entries.append(entry) - } else { - let dataStartIndex = entryInfo.blockStartIndex + 512 - let dataEndIndex = dataStartIndex + entryInfo.size! - // Verify that data is not truncated. - // The data.startIndex inequality is strict since by this point at least one header (i.e. 512 bytes) - // has been processed. The data.endIndex inequality is strict since there must be a 1024 bytes-long EOF - // marker block which isn't included into any entry. - guard dataStartIndex > data.startIndex && dataEndIndex < data.endIndex - else { throw TarError.wrongField } - let entryData = data.subdata(in: dataStartIndex.. data.startIndex && dataEndIndex < data.endIndex + else { throw TarError.tooSmallFileIsPassed } + let entryData = data.subdata(in: dataStartIndex.. [TarEntryInfo] { - // TAR container should be at least 512 bytes long (when it contains only one header). - guard data.count >= 512 - else { throw TarError.tooSmallFileIsPassed } - - /// Object with input data which supports convenient work with bit shifts. - var infoProvider = TarEntryInfoProvider(data) + var parser = TarParser(data) var entries = [TarEntryInfo]() - while let info = try infoProvider.next() { - guard info.specialEntryType == nil - else { continue } - entries.append(info) + parsingLoop: while true { + let result = try parser.next() + switch result { + case .specialEntry: + continue parsingLoop + case .entryInfo(let info, _, _): + entries.append(info) + case .truncated: + // We don't have an error with a more suitable name. + throw TarError.tooSmallFileIsPassed + case .finished: + fallthrough + case .eofMarker: + break parsingLoop + } } return entries diff --git a/Sources/TAR/TarCreateError.swift b/Sources/TAR/TarCreateError.swift index 61312585..15b89d8b 100644 --- a/Sources/TAR/TarCreateError.swift +++ b/Sources/TAR/TarCreateError.swift @@ -6,9 +6,13 @@ /** Represents an error which happened during the creation of a new TAR container. - - Note: This error type is going to be merged with `TarError` in the next major update. + - Note: This error type is never used and will be removed in the next major update. */ public enum TarCreateError: Error { - /// One of the `TarEntryInfo`'s string properties (such as `name`) cannot be encoded with UTF-8 encoding. + /** + One of the `TarEntryInfo`'s string properties (such as `name`) cannot be encoded with UTF-8 encoding. + + - Note: This error is never thrown and will be removed in the next major update. + */ case utf8NonEncodable } diff --git a/Sources/TAR/TarEntry.swift b/Sources/TAR/TarEntry.swift index 20918f01..a3da7ee1 100644 --- a/Sources/TAR/TarEntry.swift +++ b/Sources/TAR/TarEntry.swift @@ -35,14 +35,4 @@ public struct TarEntry: ContainerEntry { self.data = data } - func generateContainerData() throws -> Data { - var out = try self.info.generateContainerData() - guard let data = self.data - else { return out } - out.append(data) - let paddingSize = data.count.roundTo512() - data.count - out.append(Data(count: paddingSize)) - return out - } - } diff --git a/Sources/TAR/TarEntryInfo.swift b/Sources/TAR/TarEntryInfo.swift index 6cb7f742..c298391f 100644 --- a/Sources/TAR/TarEntryInfo.swift +++ b/Sources/TAR/TarEntryInfo.swift @@ -4,21 +4,10 @@ // See LICENSE for license information import Foundation -import BitByteData /// Provides access to information about an entry from the TAR container. public struct TarEntryInfo: ContainerEntryInfo { - enum SpecialEntryType: UInt8 { - case longName = 76 - case longLinkName = 75 - case globalExtendedHeader = 103 - case localExtendedHeader = 120 - // Sun were the first to use extended headers. Their headers are mostly compatible with PAX ones, but differ in - // the typeflag used ("X" instead of "x"). - case sunExtendedHeader = 88 - } - // MARK: ContainerEntryInfo /** @@ -31,20 +20,21 @@ public struct TarEntryInfo: ContainerEntryInfo { 3. GNU format type "L" (LongName) entry. 4. Default TAR header. - - Note: When new TAR container is created, if `name` cannot be encoded with ASCII or its ASCII byte-representation - is longer than 100 bytes then a PAX extended header will be created to represent this value correctly. - - Note: When creating new TAR container, `name` is always encoded with UTF-8 in basic TAR header. + - Note: When a new TAR container is created, if `name` cannot be encoded with ASCII or its ASCII byte-representation + is longer than 100 bytes then a PAX extended header will be created to represent this value correctly, unless other + format is forced. + - Note: When creating new TAR container, `name` is always encoded with UTF-8 in the basic TAR header. */ public var name: String /** Entry's data size. - - Note: This property cannot be directly modified. Instead it is updated automatically to be equal to its parent + - Note: This property cannot be directly modified. Instead it is updated automatically to be equal to its parent's `entry.data.count`. - - Note: When new TAR container is created, if `size` is bigger than 8589934591 then a PAX extended header will be - created to represent this value correctly. Also, base-256 encoding will be used to encode this value in basic TAR - header. + - Note: When a new TAR container is created, if `size` is bigger than 8589934591 then a PAX extended header will be + created to represent this value correctly, unless other format is forced. In addition, base-256 encoding will be + used to encode this value in the basic TAR header. */ public internal(set) var size: Int? @@ -53,25 +43,25 @@ public struct TarEntryInfo: ContainerEntryInfo { /** Entry's last access time (only available for PAX format; `nil` otherwise). - - Note: When new TAR container is created, if `accessTime` is not `nil` then a PAX extended header will be created - to store this property. + - Note: When a new TAR container is created, if `accessTime` is not `nil` then a PAX extended header will be created + to store this property, unless other format is forced. */ public var accessTime: Date? /** Entry's creation time (only available for PAX format; `nil` otherwise). - - Note: When new TAR container is created, if `creationTime` is not `nil` then a PAX extended header will be - created to store this property. + - Note: When a new TAR container is created, if `creationTime` is not `nil` then a PAX extended header will be + created to store this property, unless other format is forced. */ public var creationTime: Date? /** Entry's last modification time. - - Note: When new TAR container is created, if `modificationTime` is bigger than 8589934591 then a PAX extended - header will be created to represent this value correctly. Also, base-256 encoding will be used to encode this value - in basic TAR header. + - Note: When a new TAR container is created, if `modificationTime` is bigger than 8589934591 then a PAX extended + header will be created to represent this value correctly, unless other format is forced. In addition, base-256 + encoding will be used to encode this value in the basic TAR header. */ public var modificationTime: Date? @@ -79,34 +69,34 @@ public struct TarEntryInfo: ContainerEntryInfo { // MARK: TAR specific - /// Entry's compression method. Always `.copy` for entries of TAR containers. + /// Entry's compression method. Always `.copy` for the entries of TAR containers. public let compressionMethod = CompressionMethod.copy /** ID of entry's owner. - - Note: When new TAR container is created, if `ownerID` is bigger than 2097151 then a PAX extended header will be - created to represent this value correctly. Also, base-256 encoding will be used to encode this value in basic TAR - header. + - Note: When a new TAR container is created, if `ownerID` is bigger than 2097151 then a PAX extended header will be + created to represent this value correctly, unless other format is forced. In addition, base-256 encoding will be + used to encode this value in the basic TAR header. */ public var ownerID: Int? /** ID of the group of entry's owner. - - Note: When new TAR container is created, if `groupID` is bigger than 2097151 then a PAX extended header will be - created to represent this value correctly. Also, base-256 encoding will be used to encode this value in basic TAR - header. + - Note: When a new TAR container is created, if `groupID` is bigger than 2097151 then a PAX extended header will be + created to represent this value correctly, unless other format is forced. In addition, base-256 encoding will be + used to encode this value in the basic TAR header. */ public var groupID: Int? /** User name of entry's owner. - - Note: When new TAR container is created, if `ownerUserName` cannot be encoded with ASCII or its ASCII + - Note: When a new TAR container is created, if `ownerUserName` cannot be encoded with ASCII or its ASCII byte-representation is longer than 32 bytes then a PAX extended header will be created to represent this value - correctly. - - Note: When creating new TAR container, `ownerUserName` is always encoded with UTF-8 in ustar header. + correctly, unless other format is forced. + - Note: When creating new TAR container, `ownerUserName` is always encoded with UTF-8 in the ustar header. */ public var ownerUserName: String? @@ -115,8 +105,8 @@ public struct TarEntryInfo: ContainerEntryInfo { - Note: When new TAR container is created, if `ownerGroupName` cannot be encoded with ASCII or its ASCII byte-representation is longer than 32 bytes then a PAX extended header will be created to represent this value - correctly. - - Note: When creating new TAR container, `ownerGroupName` is always encoded with UTF-8 in ustar header. + correctly, unless other format is forced. + - Note: When creating new TAR container, `ownerGroupName` is always encoded with UTF-8 in the ustar header. */ public var ownerGroupName: String? @@ -140,7 +130,7 @@ public struct TarEntryInfo: ContainerEntryInfo { Name of the character set used to encode entry's data (only available for PAX format; `nil` otherwise). - Note: When new TAR container is created, if `charset` is not `nil` then a PAX extended header will be created to - store this property. + store this property, unless other format is forced. */ public var charset: String? @@ -148,7 +138,7 @@ public struct TarEntryInfo: ContainerEntryInfo { Entry's comment (only available for PAX format; `nil` otherwise). - Note: When new TAR container is created, if `comment` is not `nil` then a PAX extended header will be created to - store this property. + store this property, unless other format is forced. */ public var comment: String? @@ -164,7 +154,7 @@ public struct TarEntryInfo: ContainerEntryInfo { - Note: When new TAR container is created, if `linkName` cannot be encoded with ASCII or its ASCII byte-representation is longer than 100 bytes then a PAX extended header will be created to represent this value - correctly. + correctly, unless other format is forced. - Note: When creating new TAR container, `linkName` is always encoded with UTF-8 in basic TAR header. */ public var linkName: String @@ -173,15 +163,10 @@ public struct TarEntryInfo: ContainerEntryInfo { All custom (unknown) records from global and local PAX extended headers. `nil`, if there were no headers. - Note: When new TAR container is created, if `unknownExtendedHeaderRecords` is not `nil` then a *local* PAX - extended header will be created to store this property. + extended header will be created to store this property, unless other format is forced. */ public var unknownExtendedHeaderRecords: [String: String]? - var specialEntryType: SpecialEntryType? - let format: TarContainer.Format - - let blockStartIndex: Int - /** Initializes the entry's info with its name and type. @@ -194,18 +179,10 @@ public struct TarEntryInfo: ContainerEntryInfo { self.name = name self.type = type self.linkName = "" - // These properties are only used when entry is loaded from the container. - self.format = .pax - self.blockStartIndex = 0 } - init(_ byteReader: ByteReader, _ global: TarExtendedHeader?, _ local: TarExtendedHeader?, - _ longName: String?, _ longLinkName: String?) throws { - self.blockStartIndex = byteReader.offset - - // File name - var name = byteReader.tarCString(maxLength: 100) - + init(_ header: TarHeader, _ global: TarExtendedHeader?, _ local: TarExtendedHeader?, + _ longName: String?, _ longLinkName: String?) { // General notes for all the properties processing below: // 1. There might be a corresponding field in either global or local extended PAX header. // 2. We still need to read general TAR fields so we can't eliminate auxiliary local let-variables. @@ -214,124 +191,49 @@ public struct TarEntryInfo: ContainerEntryInfo { // Corruption of the container should be detected by checksum comparison, so we decided to ignore them here; // the alternative, which was used in previous versions, is to throw an error. - if let posixAttributes = byteReader.tarInt(maxLength: 8) { - // Sometimes file mode field also contains unix type, so we need to filter it out. - self.permissions = Permissions(rawValue: UInt32(truncatingIfNeeded: posixAttributes) & 0xFFF) - } else { - self.permissions = nil - } - - let ownerAccountID = byteReader.tarInt(maxLength: 8) - self.ownerID = (local?.uid ?? global?.uid) ?? ownerAccountID - - let groupAccountID = byteReader.tarInt(maxLength: 8) - self.groupID = (local?.gid ?? global?.gid) ?? groupAccountID - - guard let fileSize = byteReader.tarInt(maxLength: 12) - else { throw TarError.wrongField } - self.size = (local?.size ?? global?.size) ?? fileSize - - let mtime = byteReader.tarInt(maxLength: 12) + self.permissions = header.permissions + self.ownerID = (local?.uid ?? global?.uid) ?? header.uid + self.groupID = (local?.gid ?? global?.gid) ?? header.gid + self.size = (local?.size ?? global?.size) ?? header.size if let paxMtime = local?.mtime ?? global?.mtime { self.modificationTime = Date(timeIntervalSince1970: paxMtime) - } else if let mtime = mtime { - self.modificationTime = Date(timeIntervalSince1970: TimeInterval(mtime)) + } else { + self.modificationTime = header.mtime } - // Checksum - guard let checksum = byteReader.tarInt(maxLength: 8) - else { throw TarError.wrongHeaderChecksum } - - let currentIndex = byteReader.offset - byteReader.offset = blockStartIndex - var headerBytesForChecksum = byteReader.bytes(count: 512) - headerBytesForChecksum.replaceSubrange(148..<156, with: Array(repeating: 0x20, count: 8)) - byteReader.offset = currentIndex - - // Some implementations treat bytes as signed integers, but some don't. - // So we check both cases, equality in one of them will pass the checksum test. - let unsignedOurChecksum = headerBytesForChecksum.reduce(0 as UInt) { $0 + UInt(truncatingIfNeeded: $1) } - let signedOurChecksum = headerBytesForChecksum.reduce(0 as Int) { $0 + $1.toInt() } - guard unsignedOurChecksum == UInt(truncatingIfNeeded: checksum) || signedOurChecksum == checksum - else { throw TarError.wrongHeaderChecksum } - // File type - let fileTypeIndicator = byteReader.byte() - self.specialEntryType = SpecialEntryType(rawValue: fileTypeIndicator) - self.type = ContainerEntryType(fileTypeIndicator) + guard case .normal(let entryType) = header.type + else { fatalError("TarEntryInfo.init: unexpected TarHeader.type, \(header.type)") } + self.type = entryType - // Linked file name - let linkName = byteReader.tarCString(maxLength: 100) - - // There are two different formats utilizing this section of TAR header: GNU format and POSIX (aka "ustar"; - // PAX containers can also be considered as POSIX). They differ in the value of magic field as well as what - // comes after deviceMinorNumber field. While "ustar" format may contain prefix for file name, GNU format - // uses this place for storing atime/ctime and fields related to sparse-files. In practice, these fields are - // rarely used by GNU tar and only present if "incremental backups" options were used. Thus, GNU format TAR - // container can often be incorrectly considered as having prefix field containing only NULLs. - let magic = byteReader.uint64() - - var gnuAtime: Int? - var gnuCtime: Int? - - if magic == 0x0020207261747375 || magic == 0x3030007261747375 || magic == 0x3030207261747375 { - let uname = byteReader.tarCString(maxLength: 32) - self.ownerUserName = (local?.uname ?? global?.uname) ?? uname - - let gname = byteReader.tarCString(maxLength: 32) - self.ownerGroupName = (local?.gname ?? global?.gname) ?? gname - - self.deviceMajorNumber = byteReader.tarInt(maxLength: 8) - self.deviceMinorNumber = byteReader.tarInt(maxLength: 8) - - if magic == 0x00_20_20_72_61_74_73_75 { // GNU format. - // GNU format mostly is identical to POSIX format and in the common situations can be considered as - // having prefix containing only NULLs. However, in the case of incremental backups produced by GNU tar - // this part of the TAR header is used for storing a lot of different properties. For now, we are only - // reading atime and ctime. - - gnuAtime = byteReader.tarInt(maxLength: 12) - gnuCtime = byteReader.tarInt(maxLength: 12) - } else { - let prefix = byteReader.tarCString(maxLength: 155) - if prefix != "" { - if prefix.last == "/" { - name = prefix + name - } else { - name = prefix + "/" + name - } - } - } - } else { - self.ownerUserName = local?.uname ?? global?.uname - self.ownerGroupName = local?.gname ?? global?.gname - } - - if local != nil || global != nil { - self.format = .pax - } else if magic == 0x0020207261747375 || longName != nil || longLinkName != nil { - self.format = .gnu - } else if magic == 0x3030007261747375 || magic == 0x3030207261747375 { - self.format = .ustar - } else { - self.format = .prePosix - } + self.ownerUserName = (local?.uname ?? global?.uname) ?? header.uname + self.ownerGroupName = (local?.gname ?? global?.gname) ?? header.gname + self.deviceMajorNumber = header.deviceMajorNumber + self.deviceMinorNumber = header.deviceMinorNumber // Set `name` and `linkName` to values from PAX or GNU format if possible. + var name = header.name + if let prefix = header.prefix, prefix != "" { + if prefix.last == "/" { + name = prefix + name + } else { + name = prefix + "/" + name + } + } self.name = ((local?.path ?? global?.path) ?? longName) ?? name - self.linkName = ((local?.linkpath ?? global?.linkpath) ?? longLinkName) ?? linkName + self.linkName = ((local?.linkpath ?? global?.linkpath) ?? longLinkName) ?? header.linkName // Set additional properties from PAX extended headers. if let atime = local?.atime ?? global?.atime { self.accessTime = Date(timeIntervalSince1970: atime) - } else if let gnuAtime = gnuAtime { - self.accessTime = Date(timeIntervalSince1970: TimeInterval(gnuAtime)) + } else { + self.accessTime = header.atime } if let ctime = local?.ctime ?? global?.ctime { self.creationTime = Date(timeIntervalSince1970: ctime) - } else if let gnuCtime = gnuCtime { - self.creationTime = Date(timeIntervalSince1970: TimeInterval(gnuCtime)) + } else { + self.creationTime = header.ctime } self.charset = local?.charset ?? global?.charset @@ -347,152 +249,4 @@ public struct TarEntryInfo: ContainerEntryInfo { } } - func generateContainerData() throws -> Data { - // It is not possible to encode non-english characters with ASCII (expectedly), so we are using UTF-8. - // While this contradicts format specification, in case of ustar and basic TAR format our other options in - // situation when it is not possible to encode with ASCII are: - // - crash with fatalError, etc. - // - throw an error. - // - ignore the problem, and just write NULLs. - // The last option is, obviously, is not ideal. Overall, it seems like using UTF-8 instead of ASCII is the most - // viable option. - - var out = Data() - - try out.append(tarString: self.name, maxLength: 100) - - out.append(tarInt: self.permissions?.rawValue.toInt(), maxLength: 8) - out.append(tarInt: self.ownerID, maxLength: 8) - out.append(tarInt: self.groupID, maxLength: 8) - out.append(tarInt: self.size, maxLength: 12) - - if let mtime = self.modificationTime?.timeIntervalSince1970 { - out.append(tarInt: Int(mtime), maxLength: 12) - } else { - out.append(tarInt: nil, maxLength: 12) - } - - // Checksum is calculated based on the complete header with spaces instead of checksum. - out.append(contentsOf: Array(repeating: 0x20, count: 8)) - - let fileTypeIndicator = self.specialEntryType?.rawValue ?? self.type.fileTypeIndicator - out.append(fileTypeIndicator) - - try out.append(tarString: self.linkName, maxLength: 100) - - out.append(contentsOf: [0x75, 0x73, 0x74, 0x61, 0x72, 0x00, 0x30, 0x30]) // "ustar\000" - // In theory, user/group name is not guaranteed to have only ASCII characters, so the same disclaimer as for - // file name field applies here. - try out.append(tarString: self.ownerUserName, maxLength: 32) - try out.append(tarString: self.ownerGroupName, maxLength: 32) - out.append(tarInt: self.deviceMajorNumber, maxLength: 8) - out.append(tarInt: self.deviceMinorNumber, maxLength: 8) - - guard let nameData = self.name.data(using: .utf8) - else { throw TarCreateError.utf8NonEncodable } - - if nameData.count > 100 { - var maxPrefixLength = nameData.count - if maxPrefixLength > 156 { - // We can set actual maximum possible length of prefix equal to 156 and not 155, because it may - // include trailing slash which will be removed during splitting. - maxPrefixLength = 156 - } else if nameData[maxPrefixLength - 1] == 0x2F { - // Skip trailing slash. - maxPrefixLength -= 1 - } - - // Looking for the last slash in the potential prefix. -1 if not found. - // It determines the end of the actual prefix and the beginning of the updated name field. - #if (swift(>=4.1.50) || (swift(>=3.4) && !swift(>=4.0))) || !os(Linux) - let lastPrefixSlashIndex = nameData.prefix(upTo: maxPrefixLength) - .range(of: Data([0x2f]), options: .backwards)?.lowerBound ?? -1 - #else - // TODO: This is a workaround for runtime crash in `Data.prefix(upTo:).range(of:options:)` on Linux with - // Swift 4.1. It seems like it is fixed in 4.2 and master snapshots, so it will be removed when Swift - // 5.0 is released. - var lastPrefixSlashIndex = -1 - for i in stride(from: maxPrefixLength - 1, through: 0, by: -1) { - if nameData[i] == 0x2f { - lastPrefixSlashIndex = i - break - } - } - #endif - let updatedNameLength = nameData.count - lastPrefixSlashIndex - 1 - let prefixLength = lastPrefixSlashIndex - - if lastPrefixSlashIndex <= 0 || updatedNameLength > 100 || updatedNameLength == 0 || prefixLength > 155 { - // Unsplittable name. - out.append(Data(count: 155)) - } else { - // Add prefix data to output. - out.append(nameData.prefix(upTo: lastPrefixSlashIndex)) - // Update name field data in output. - var newNameData = nameData.suffix(from: lastPrefixSlashIndex + 1) - newNameData.append(Data(count: 100 - newNameData.count)) - out.replaceSubrange(0..<100, with: newNameData) - } - } else { - out.append(Data(count: 155)) // Empty prefix - } - - // Checksum calculation. - // First, we pad header data to 512 bytes. - out.append(Data(count: 512 - out.count)) - let checksum = out.reduce(0 as Int) { $0 + $1.toInt() } - let checksumString = String(format: "%06o", checksum).appending("\0 ") - out.replaceSubrange(148..<156, with: checksumString.data(using: .ascii)!) - - return out - } - -} - -fileprivate extension Data { - - /// This should work in the same way as `String.padding(toLength: length, withPad: "\0", startingAt: 0)`. - @inline(__always) - private func zeroPad(_ length: Int) -> Data { - var out = length < self.count ? self.prefix(upTo: length) : self - out.append(Data(count: length - out.count)) - return out - } - - mutating func append(tarInt value: Int?, maxLength: Int) { - guard var value = value else { - // No value; fill field with NULLs. - self.append(Data(count: maxLength)) - return - } - - let maxOctalValue = (1 << (maxLength * 3)) - 1 - guard value > maxOctalValue || value < 0 else { - // Normal octal encoding. - self.append(String(value, radix: 8).data(using: .utf8)!.zeroPad(maxLength)) - return - } - - // Base-256 encoding. - var buffer = Array(repeating: 0 as UInt8, count: maxLength) - for i in stride(from: maxLength - 1, to: 0, by: -1) { - buffer[i] = UInt8(truncatingIfNeeded: value & 0xFF) - value >>= 8 - } - buffer[0] |= 0x80 // Highest bit indicates base-256 encoding. - self.append(Data(buffer)) - } - - mutating func append(tarString string: String?, maxLength: Int) throws { - guard let string = string else { - // No value; fill field with NULLs. - self.append(Data(count: maxLength)) - return - } - - guard let stringData = string.data(using: .utf8)?.zeroPad(maxLength) - else { throw TarCreateError.utf8NonEncodable } - self.append(stringData) - } - } diff --git a/Sources/TAR/TarEntryInfoProvider.swift b/Sources/TAR/TarEntryInfoProvider.swift deleted file mode 100644 index cac4bffd..00000000 --- a/Sources/TAR/TarEntryInfoProvider.swift +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) 2021 Timofey Solomko -// Licensed under MIT License -// -// See LICENSE for license information - -import Foundation -import BitByteData - -// While it is tempting to make Provider conform to `IteratorProtocol` and `Sequence` protocols, it is in fact -// impossible to do so, since `TarEntryInfo.init(...)` is throwing and `IteratorProtocol.next()` cannot be throwing. -struct TarEntryInfoProvider { - - private let byteReader: ByteReader - private var lastGlobalExtendedHeader: TarExtendedHeader? - private var lastLocalExtendedHeader: TarExtendedHeader? - private var longLinkName: String? - private var longName: String? - - init(_ data: Data) { - self.byteReader = ByteReader(data: data) - } - - mutating func next() throws -> TarEntryInfo? { - guard byteReader.bytesLeft >= 1024, - byteReader.data[byteReader.offset.. Double(maxOctalLengthTwelve)) { self.mtime = mtime } + // The non-asciiness of the (link)name is still a reason to use PAX headers, even though we encode using UTF-8 + // in basic TAR headers anyway, because one can imagine a third-party implementation, that can read PAX headers + // properly, but still expects all string fields in the basic header to be ASCII-only. By using PAX headers we + // can "support" those implementations, though this will work only if they skip (non-ASCII/invalid) string + // fields after encountering a PAX header. let asciiNameData = info.name.data(using: .ascii) if asciiNameData == nil || asciiNameData!.count > 100 { self.path = info.name @@ -128,66 +134,65 @@ struct TarExtendedHeader { self.unknownRecords = info.unknownExtendedHeaderRecords ?? [:] } - func generateContainerData() throws -> Data { + func generateContainerData() -> Data { var headerString = "" if let atime = self.atime { - headerString += try TarExtendedHeader.generateHeaderString("atime", String(atime)) + headerString += TarExtendedHeader.generateHeaderString("atime", String(atime)) } if let ctime = self.ctime { - headerString += try TarExtendedHeader.generateHeaderString("ctime", String(ctime)) + headerString += TarExtendedHeader.generateHeaderString("ctime", String(ctime)) } if let mtime = self.mtime { - headerString += try TarExtendedHeader.generateHeaderString("mtime", String(mtime)) + headerString += TarExtendedHeader.generateHeaderString("mtime", String(mtime)) } if let size = self.size { - headerString += try TarExtendedHeader.generateHeaderString("size", String(size)) + headerString += TarExtendedHeader.generateHeaderString("size", String(size)) } if let uid = self.uid { - headerString += try TarExtendedHeader.generateHeaderString("uid", String(uid)) + headerString += TarExtendedHeader.generateHeaderString("uid", String(uid)) } if let gid = self.gid { - headerString += try TarExtendedHeader.generateHeaderString("gid", String(gid)) + headerString += TarExtendedHeader.generateHeaderString("gid", String(gid)) } if let uname = self.uname { - headerString += try TarExtendedHeader.generateHeaderString("uname", uname) + headerString += TarExtendedHeader.generateHeaderString("uname", uname) } if let gname = self.gname { - headerString += try TarExtendedHeader.generateHeaderString("gname", gname) + headerString += TarExtendedHeader.generateHeaderString("gname", gname) } if let path = self.path { - headerString += try TarExtendedHeader.generateHeaderString("path", path) + headerString += TarExtendedHeader.generateHeaderString("path", path) } if let linkpath = self.linkpath { - headerString += try TarExtendedHeader.generateHeaderString("linkpath", linkpath) + headerString += TarExtendedHeader.generateHeaderString("linkpath", linkpath) } if let charset = self.charset { - headerString += try TarExtendedHeader.generateHeaderString("charset", charset) + headerString += TarExtendedHeader.generateHeaderString("charset", charset) } if let comment = self.comment { - headerString += try TarExtendedHeader.generateHeaderString("comment", comment) + headerString += TarExtendedHeader.generateHeaderString("comment", comment) } for (key, value) in self.unknownRecords { - headerString += try TarExtendedHeader.generateHeaderString(key, value) + headerString += TarExtendedHeader.generateHeaderString(key, value) } - return headerString.data(using: .utf8)! + return Data(headerString.utf8) } - private static func generateHeaderString(_ fieldName: String, _ valueString: String) throws -> String { - guard let valueCount = valueString.data(using: .utf8)?.count - else { throw TarCreateError.utf8NonEncodable } + private static func generateHeaderString(_ fieldName: String, _ valueString: String) -> String { + let valueCount = Data(valueString.utf8).count return TarExtendedHeader.calculateCountString(fieldName, valueCount) + " \(fieldName)=\(valueString)\n" } diff --git a/Sources/TAR/TarHeader.swift b/Sources/TAR/TarHeader.swift new file mode 100644 index 00000000..b0c9ea2f --- /dev/null +++ b/Sources/TAR/TarHeader.swift @@ -0,0 +1,300 @@ +// Copyright (c) 2021 Timofey Solomko +// Licensed under MIT License +// +// See LICENSE for license information + +import Foundation +import BitByteData + +/// This type represents the low-level header structure of the TAR format. +struct TarHeader { + + enum HeaderEntryType { + case normal(ContainerEntryType) + case special(SpecialEntryType) + } + + enum SpecialEntryType: UInt8 { + case longName = 76 + case longLinkName = 75 + case globalExtendedHeader = 103 + case localExtendedHeader = 120 + // Sun were the first to use extended headers. Their headers are mostly compatible with PAX ones, but differ in + // the typeflag used ("X" instead of "x"). + case sunExtendedHeader = 88 + } + + // These fields are present in all formats. + let name: String + let permissions: Permissions? + let uid: Int? + let gid: Int? + let size: Int + private(set) var mtime: Date? + // - checksum + let type: HeaderEntryType + let linkName: String + + // Ustar only + // - magic "ustar\000" + private(set) var uname: String? + private(set) var gname: String? + private(set) var deviceMajorNumber: Int? + private(set) var deviceMinorNumber: Int? + private(set) var prefix: String? + + // These fields are present in gnu and star formats. + // - magic ("ustar \0" for [old] gnu) + private(set) var atime: Date? + private(set) var ctime: Date? + + let format: TarContainer.Format + let blockStartIndex: Int + + init(_ reader: LittleEndianByteReader) throws { + self.blockStartIndex = reader.offset + self.name = reader.tarCString(maxLength: 100) + + if let posixAttributes = reader.tarInt(maxLength: 8) { + // Sometimes file mode field also contains unix type, so we need to filter it out. + self.permissions = Permissions(rawValue: UInt32(truncatingIfNeeded: posixAttributes) & 0xFFF) + } else { + self.permissions = nil + } + + self.uid = reader.tarInt(maxLength: 8) + self.gid = reader.tarInt(maxLength: 8) + + guard let size = reader.tarInt(maxLength: 12) + else { throw TarError.wrongField } + self.size = size + + if let mtime = reader.tarInt(maxLength: 12) { + self.mtime = Date(timeIntervalSince1970: TimeInterval(mtime)) + } + + // Checksum + guard let checksum = reader.tarInt(maxLength: 8) + else { throw TarError.wrongHeaderChecksum } + + let currentIndex = reader.offset + reader.offset = blockStartIndex + var headerBytesForChecksum = reader.bytes(count: 512) + headerBytesForChecksum.replaceSubrange(148..<156, with: Array(repeating: 0x20, count: 8)) + reader.offset = currentIndex + + // Some implementations treat bytes as signed integers, but some don't. + // So we check both cases, equality in one of them will pass the checksum test. + let unsignedOurChecksum = headerBytesForChecksum.reduce(0 as UInt) { $0 + UInt(truncatingIfNeeded: $1) } + let signedOurChecksum = headerBytesForChecksum.reduce(0 as Int) { $0 + $1.toInt() } + guard unsignedOurChecksum == UInt(truncatingIfNeeded: checksum) || signedOurChecksum == checksum + else { throw TarError.wrongHeaderChecksum } + + let fileTypeIndicator = reader.byte() + if let specialEntryType = SpecialEntryType(rawValue: fileTypeIndicator) { + self.type = .special(specialEntryType) + } else { + self.type = .normal(ContainerEntryType(fileTypeIndicator)) + } + + self.linkName = reader.tarCString(maxLength: 100) + + // There are two different formats utilizing this section of TAR header: GNU format and POSIX (aka "ustar"; + // PAX containers can also be considered as POSIX). They differ in the value of magic field as well as what + // comes after deviceMinorNumber field. While "ustar" format may contain prefix for file name, GNU format + // uses this place for storing atime/ctime and fields related to sparse-files. In practice, these fields are + // rarely used by GNU tar and only present if "incremental backups" options were used. Thus, GNU format TAR + // container can often be incorrectly considered as having prefix field containing only NULLs. + let magic = reader.uint64() + + if magic == 0x0020207261747375 || magic == 0x3030007261747375 || magic == 0x3030207261747375 { + self.uname = reader.tarCString(maxLength: 32) + self.gname = reader.tarCString(maxLength: 32) + self.deviceMajorNumber = reader.tarInt(maxLength: 8) + self.deviceMinorNumber = reader.tarInt(maxLength: 8) + + if magic == 0x00_20_20_72_61_74_73_75 { // GNU format. + // GNU format is mostly identical to POSIX format and in the common situations can be considered as + // having prefix containing only NULLs. However, in the case of incremental backups produced by GNU tar + // this part of the TAR header is used for storing a lot of different properties. For now, we are only + // reading atime and ctime. + if let atime = reader.tarInt(maxLength: 12) { + self.atime = Date(timeIntervalSince1970: TimeInterval(atime)) + } + if let ctime = reader.tarInt(maxLength: 12) { + self.ctime = Date(timeIntervalSince1970: TimeInterval(ctime)) + } + self.format = .gnu + } else { + self.prefix = reader.tarCString(maxLength: 155) + self.format = .ustar + } + } else { + self.format = .prePosix + } + } + + init(specialName: String, specialType: SpecialEntryType, size: Int, uid: Int?, gid: Int?) { + self.name = specialName + self.type = .special(specialType) + self.size = size + self.permissions = Permissions(rawValue: 420) + self.uid = uid + self.gid = gid + self.mtime = Date() + self.linkName = "" + if specialType == .longName || specialType == .longLinkName { + self.format = .gnu + } else if specialType == .globalExtendedHeader || specialType == .localExtendedHeader { + self.format = .pax + } else { + self.format = .prePosix + } + // Unused if header was created using this initializer. + self.blockStartIndex = -1 + } + + init(_ info: TarEntryInfo) { + self.name = info.name + self.type = .normal(info.type) + self.size = info.size ?? 0 + self.atime = info.accessTime + self.ctime = info.creationTime + self.mtime = info.modificationTime + self.permissions = info.permissions + self.uid = info.ownerID + self.gid = info.groupID + self.uname = info.ownerUserName + self.gname = info.ownerGroupName + self.deviceMajorNumber = info.deviceMajorNumber + self.deviceMinorNumber = info.deviceMinorNumber + self.linkName = info.linkName + self.format = .pax + // Unused if header was created using this initializer. + self.blockStartIndex = -1 + } + + /// This function overrides the value stored in the `format` property. + func generateContainerData(_ format: TarContainer.Format) -> Data { + // It is not possible to encode non-english characters with ASCII (expectedly), so we are using UTF-8. + // While this contradicts format specification, in case of ustar and basic TAR format our other options in + // situation when it is not possible to encode with ASCII are: + // - crash with fatalError, etc. + // - throw an error. + // - ignore the problem, and just write NULLs. + // The last option is, obviously, not ideal. Overall, it seems like using UTF-8 instead of ASCII is the most + // viable option. + + var out = Data() + + out.append(tarString: self.name, maxLength: 100) + + out.append(tarInt: self.permissions?.rawValue.toInt(), maxLength: 8) + out.append(tarInt: self.uid, maxLength: 8) + out.append(tarInt: self.gid, maxLength: 8) + out.append(tarInt: self.size, maxLength: 12) + + if let mtime = self.mtime?.timeIntervalSince1970 { + out.append(tarInt: Int(mtime), maxLength: 12) + } else { + out.append(tarInt: nil, maxLength: 12) + } + + // Checksum is calculated based on the complete header with spaces instead of checksum. + out.append(contentsOf: Array(repeating: 0x20, count: 8)) + + let fileTypeIndicator: UInt8 + switch self.type { + case .normal(let entryType): + fileTypeIndicator = entryType.fileTypeIndicator + case .special(let specialType): + fileTypeIndicator = specialType.rawValue + } + out.append(fileTypeIndicator) + + out.append(tarString: self.linkName, maxLength: 100) + + // For prePosix format there is no additional fields. + + // Magic + if format == .ustar || format == .pax { + out.append(contentsOf: [0x75, 0x73, 0x74, 0x61, 0x72, 0x00, 0x30, 0x30]) // "ustar\000" + } else if format == .gnu { + out.append(contentsOf: [0x75, 0x73, 0x74, 0x61, 0x72, 0x20, 0x20, 0x00]) // "ustar \0" + } + + if format != .prePosix { + // Check in case other formats are added in the future. + assert(format == .ustar || format == .gnu || format == .pax) + // Both ustar, pax, and gnu formats contain the following four fields. + // In theory, user/group name is not guaranteed to have only ASCII characters, so the same disclaimer as for + // file name field applies here. + out.append(tarString: self.uname, maxLength: 32) + out.append(tarString: self.gname, maxLength: 32) + out.append(tarInt: self.deviceMajorNumber, maxLength: 8) + out.append(tarInt: self.deviceMinorNumber, maxLength: 8) + + // ustar and pax formats contain prefix field. + if format == .ustar || format == .pax { + // Splitting the name property into the name and prefix fields. + let nameData = Data(self.name.utf8) + if nameData.count > 100 { + var maxPrefixLength = nameData.count + if maxPrefixLength > 156 { + // We can set actual maximum possible length of prefix equal to 156 and not 155, because it may + // include trailing slash which will be removed during splitting. + maxPrefixLength = 156 + } else if nameData[maxPrefixLength - 1] == 0x2F { + // Skip trailing slash. + maxPrefixLength -= 1 + } + + // Looking for the last slash in the potential prefix. -1 if not found. + // It determines the end of the actual prefix and the beginning of the updated name field. + // This way of finding the last slash works, since there is no other Unicode character that contains + // the 0x2F byte when encoded in UTF-8. + let lastPrefixSlashIndex = nameData.prefix(upTo: maxPrefixLength) + .range(of: Data([0x2f]), options: .backwards)?.lowerBound ?? -1 + let updatedNameLength = nameData.count - lastPrefixSlashIndex - 1 + let prefixLength = lastPrefixSlashIndex + + if lastPrefixSlashIndex <= 0 || updatedNameLength > 100 || updatedNameLength == 0 || prefixLength > 155 { + // Unsplittable name. + out.append(Data(count: 155)) + } else { + // Add prefix data to output. + out.append(nameData.prefix(upTo: lastPrefixSlashIndex)) + // Update name field data in output. + var newNameData = nameData.suffix(from: lastPrefixSlashIndex + 1) + newNameData.append(Data(count: 100 - newNameData.count)) + out.replaceSubrange(0..<100, with: newNameData) + } + } + } else if format == .gnu { + // Gnu format contains atime and ctime instead of a prefix field. + if let atime = self.atime?.timeIntervalSince1970 { + out.append(tarInt: Int(atime), maxLength: 12) + } else { + out.append(tarInt: nil, maxLength: 12) + } + if let ctime = self.ctime?.timeIntervalSince1970 { + out.append(tarInt: Int(ctime), maxLength: 12) + } else { + out.append(tarInt: nil, maxLength: 12) + } + } + } + + // Checksum calculation. + // First, we pad header data to 512 bytes. + out.append(Data(count: 512 - out.count)) + let checksum = out.reduce(0 as Int) { $0 + $1.toInt() } + let checksumString = String(format: "%06o", checksum).appending("\0 ") + out.replaceSubrange(148..<156, with: checksumString.data(using: .ascii)!) + + assert(out.count == 512) + + return out + } + +} diff --git a/Sources/TAR/TarParser.swift b/Sources/TAR/TarParser.swift new file mode 100644 index 00000000..bd4277d7 --- /dev/null +++ b/Sources/TAR/TarParser.swift @@ -0,0 +1,77 @@ +// Copyright (c) 2021 Timofey Solomko +// Licensed under MIT License +// +// See LICENSE for license information + +import Foundation +import BitByteData + +// While it is tempting to make Provider conform to `IteratorProtocol` and `Sequence` protocols, it is in fact +// impossible to do so, since `TarHeader.init(...)` is throwing and `IteratorProtocol.next()` cannot be throwing. +struct TarParser { + + enum ParsingResult { + case truncated + case eofMarker + case finished + case specialEntry(TarHeader.SpecialEntryType) + case entryInfo(TarEntryInfo, Int, TarContainer.Format) + } + + private let reader: LittleEndianByteReader + private var lastGlobalExtendedHeader: TarExtendedHeader? + private var lastLocalExtendedHeader: TarExtendedHeader? + private var longLinkName: String? + private var longName: String? + + init(_ data: Data) { + self.reader = LittleEndianByteReader(data: data) + } + + mutating func next() throws -> ParsingResult { + if reader.isFinished { + return .finished + } else if reader.bytesLeft >= 1024 && reader.data[reader.offset..= 0) + let dataStartIndex = header.blockStartIndex + 512 + + if case .special(let specialEntryType) = header.type { + switch specialEntryType { + case .globalExtendedHeader: + let dataEndIndex = dataStartIndex + header.size + lastGlobalExtendedHeader = try TarExtendedHeader(reader.data[dataStartIndex.. Int { var i = 1 diff --git a/Sources/XZ/Sha256.swift b/Sources/XZ/Sha256.swift index 7ea7b481..cc7d1769 100644 --- a/Sources/XZ/Sha256.swift +++ b/Sources/XZ/Sha256.swift @@ -36,13 +36,7 @@ struct Sha256 { var h7 = 0x5be0cd19 as UInt32 // Padding - #if compiler(>=5.0) - var bytes = data.withUnsafeBytes { $0.map { $0 } } - #else - var bytes = data.withUnsafeBytes { - [UInt8](UnsafeBufferPointer(start: $0, count: data.count / MemoryLayout.size)) - } - #endif + var bytes = data.withUnsafeBytes { $0.map { $0 } } let originalLength = bytes.count var newLength = originalLength * 8 + 1 diff --git a/Sources/XZ/XZArchive.swift b/Sources/XZ/XZArchive.swift index 2bb54ac3..77ce765b 100644 --- a/Sources/XZ/XZArchive.swift +++ b/Sources/XZ/XZArchive.swift @@ -26,7 +26,7 @@ public class XZArchive: Archive { */ public static func unarchive(archive data: Data) throws -> Data { /// Object with input data which supports convenient work with bytes. - let byteReader = ByteReader(data: data) + let byteReader = LittleEndianByteReader(data: data) // Note: We don't check footer's magic bytes at the beginning, // because it is impossible to determine the end of each stream in multi-stream archives @@ -68,7 +68,7 @@ public class XZArchive: Archive { */ public static func splitUnarchive(archive data: Data) throws -> [Data] { // Same code as in `unarchive(archive:)` but with different type of `result`. - let byteReader = ByteReader(data: data) + let byteReader = LittleEndianByteReader(data: data) var result = [Data]() while !byteReader.isFinished { @@ -87,7 +87,7 @@ public class XZArchive: Archive { return result } - private static func processStream(_ byteReader: ByteReader) throws -> (data: Data, checkError: Bool) { + private static func processStream(_ byteReader: LittleEndianByteReader) throws -> (data: Data, checkError: Bool) { var out = Data() let streamHeader = try XZStreamHeader(byteReader) @@ -130,7 +130,7 @@ public class XZArchive: Archive { } private static func processIndex(_ blockInfos: [(unpaddedSize: Int, uncompSize: Int)], - _ byteReader: ByteReader) throws -> Int { + _ byteReader: LittleEndianByteReader) throws -> Int { let indexStartIndex = byteReader.offset - 1 let recordsCount = try byteReader.multiByteDecode() guard recordsCount == blockInfos.count @@ -167,7 +167,7 @@ public class XZArchive: Archive { } private static func processFooter(_ streamHeader: XZStreamHeader, _ indexSize: Int, - _ byteReader: ByteReader) throws { + _ byteReader: LittleEndianByteReader) throws { let footerCRC = byteReader.uint32() /// Indicates the size of Index field. Should match its real size. let backwardSize = (byteReader.int(fromBytes: 4) + 1) * 4 @@ -191,8 +191,7 @@ public class XZArchive: Archive { else { throw XZError.wrongMagic } } - /// Returns `true` if end of archive is reached, `false` otherwise. - private static func processPadding(_ byteReader: ByteReader) throws { + private static func processPadding(_ byteReader: LittleEndianByteReader) throws { guard !byteReader.isFinished else { return } diff --git a/Sources/XZ/XZBlock.swift b/Sources/XZ/XZBlock.swift index b0e56229..fcc25417 100644 --- a/Sources/XZ/XZBlock.swift +++ b/Sources/XZ/XZBlock.swift @@ -15,7 +15,7 @@ struct XZBlock { return data.count } - init(_ blockHeaderSize: UInt8, _ byteReader: ByteReader, _ checkSize: Int) throws { + init(_ blockHeaderSize: UInt8, _ byteReader: LittleEndianByteReader, _ checkSize: Int) throws { let blockHeaderStartIndex = byteReader.offset - 1 let realBlockHeaderSize = (blockHeaderSize.toInt() + 1) * 4 @@ -34,7 +34,7 @@ struct XZBlock { /// Should match the size of data after decompression. let uncompressedSize = blockFlags & 0x80 != 0 ? try byteReader.multiByteDecode() : -1 - var filters: [(ByteReader) throws -> Data] = [] + var filters: [(LittleEndianByteReader) throws -> Data] = [] for _ in 0.. String? { guard length > 0 diff --git a/Sources/ZIP/ZipCentralDirectoryEntry.swift b/Sources/ZIP/ZipCentralDirectoryEntry.swift index 9f92ddfb..fa353765 100644 --- a/Sources/ZIP/ZipCentralDirectoryEntry.swift +++ b/Sources/ZIP/ZipCentralDirectoryEntry.swift @@ -43,7 +43,7 @@ struct ZipCentralDirectoryEntry { let nextEntryOffset: Int - init(_ byteReader: ByteReader) throws { + init(_ byteReader: LittleEndianByteReader) throws { // Check signature. guard byteReader.uint32() == 0x02014b50 else { throw ZipError.wrongSignature } diff --git a/Sources/ZIP/ZipContainer.swift b/Sources/ZIP/ZipContainer.swift index 9e5d35cf..f683adeb 100644 --- a/Sources/ZIP/ZipContainer.swift +++ b/Sources/ZIP/ZipContainer.swift @@ -62,7 +62,7 @@ public class ZipContainer: Container { var crc32 = helper.entryInfo.crc let fileData: Data - let byteReader = ByteReader(data: data) + let byteReader = LittleEndianByteReader(data: data) byteReader.offset = helper.dataOffset switch helper.entryInfo.compressionMethod { case .copy: @@ -145,7 +145,7 @@ public class ZipContainer: Container { guard data.count >= 22 else { throw ZipError.notFoundCentralDirectoryEnd } - let byteReader = ByteReader(data: data) + let byteReader = LittleEndianByteReader(data: data) var entries = [ZipEntryInfoHelper]() // First, we are looking for End of Central Directory record, specifically, for its signature. diff --git a/Sources/ZIP/ZipEndOfCentralDirectory.swift b/Sources/ZIP/ZipEndOfCentralDirectory.swift index 9bd6c61c..5268b26f 100644 --- a/Sources/ZIP/ZipEndOfCentralDirectory.swift +++ b/Sources/ZIP/ZipEndOfCentralDirectory.swift @@ -11,19 +11,21 @@ struct ZipEndOfCentralDirectory { /// Number of the current disk. private(set) var currentDiskNumber: UInt32 - /// Number of the disk with the start of CD. - private(set) var cdDiskNumber: UInt32 private(set) var cdEntries: UInt64 - private(set) var cdSize: UInt64 private(set) var cdOffset: UInt64 - init(_ byteReader: ByteReader) throws { + // There are two fields in the EndOfCD that aren't currently used anywhere except in the initalizer. + // /// Number of the disk with the start of CD. + // private var cdDiskNumber: UInt32 + // private var cdSize: UInt64 + + init(_ byteReader: LittleEndianByteReader) throws { /// Indicates if Zip64 records should be present. var zip64RecordExists = false self.currentDiskNumber = byteReader.uint32(fromBytes: 2) - self.cdDiskNumber = byteReader.uint32(fromBytes: 2) - guard self.currentDiskNumber == self.cdDiskNumber + var cdDiskNumber = byteReader.uint32(fromBytes: 2) + guard self.currentDiskNumber == cdDiskNumber else { throw ZipError.multiVolumesNotSupported } /// Number of CD entries on the current disk. @@ -34,7 +36,7 @@ struct ZipEndOfCentralDirectory { else { throw ZipError.multiVolumesNotSupported } /// Size of Central Directory. - self.cdSize = byteReader.uint64(fromBytes: 4) + var cdSize = byteReader.uint64(fromBytes: 4) /// Offset to the start of Central Directory. self.cdOffset = byteReader.uint64(fromBytes: 4) @@ -45,9 +47,9 @@ struct ZipEndOfCentralDirectory { // encoding: .utf8) // Check if zip64 records are present. - if self.currentDiskNumber == 0xFFFF || self.cdDiskNumber == 0xFFFF || + if self.currentDiskNumber == 0xFFFF || cdDiskNumber == 0xFFFF || cdEntriesCurrentDisk == 0xFFFF || self.cdEntries == 0xFFFF || - self.cdSize == 0xFFFFFFFF || self.cdOffset == 0xFFFFFFFF { + cdSize == 0xFFFFFFFF || self.cdOffset == 0xFFFFFFFF { zip64RecordExists = true } @@ -88,7 +90,7 @@ struct ZipEndOfCentralDirectory { // Update values read from basic End of CD with the ones from Zip64 End of CD. self.currentDiskNumber = byteReader.uint32() - self.cdDiskNumber = byteReader.uint32() + cdDiskNumber = byteReader.uint32() guard currentDiskNumber == cdDiskNumber else { throw ZipError.multiVolumesNotSupported } @@ -97,7 +99,7 @@ struct ZipEndOfCentralDirectory { guard cdEntries == cdEntriesCurrentDisk else { throw ZipError.multiVolumesNotSupported } - self.cdSize = byteReader.uint64() + cdSize = byteReader.uint64() self.cdOffset = byteReader.uint64() // Then, there might be 'zip64 extensible data sector' with 'special purpose data'. diff --git a/Sources/ZIP/ZipEntryInfo.swift b/Sources/ZIP/ZipEntryInfo.swift index e6fa493c..a833b006 100644 --- a/Sources/ZIP/ZipEntryInfo.swift +++ b/Sources/ZIP/ZipEntryInfo.swift @@ -102,7 +102,7 @@ public struct ZipEntryInfo: ContainerEntryInfo { /// CRC32 of entry's data. public let crc: UInt32 - init(_ byteReader: ByteReader, _ cdEntry: ZipCentralDirectoryEntry, _ localHeader: ZipLocalHeader, + init(_ byteReader: LittleEndianByteReader, _ cdEntry: ZipCentralDirectoryEntry, _ localHeader: ZipLocalHeader, _ hasDataDescriptor: Bool) { self.name = cdEntry.fileName diff --git a/Sources/ZIP/ZipEntryInfoHelper.swift b/Sources/ZIP/ZipEntryInfoHelper.swift index a7b02e2a..d815b45f 100644 --- a/Sources/ZIP/ZipEntryInfoHelper.swift +++ b/Sources/ZIP/ZipEntryInfoHelper.swift @@ -21,7 +21,7 @@ struct ZipEntryInfoHelper { let compSize: UInt64 let uncompSize: UInt64 - init(_ byteReader: ByteReader, _ currentDiskNumber: UInt32) throws { + init(_ byteReader: LittleEndianByteReader, _ currentDiskNumber: UInt32) throws { // Read Central Directory entry. let cdEntry = try ZipCentralDirectoryEntry(byteReader) diff --git a/Sources/ZIP/ZipExtraField.swift b/Sources/ZIP/ZipExtraField.swift index cdad8808..6bc69703 100644 --- a/Sources/ZIP/ZipExtraField.swift +++ b/Sources/ZIP/ZipExtraField.swift @@ -33,7 +33,7 @@ public protocol ZipExtraField { - Postcondition: `size` property of a created instance must be equal to the second argument. - Postcondition: exactly `size` amount of bytes must be read by initializer from `byteReader`. */ - init?(_ byteReader: ByteReader, _ size: Int, location: ZipExtraFieldLocation) + init?(_ byteReader: LittleEndianByteReader, _ size: Int, location: ZipExtraFieldLocation) } diff --git a/Sources/ZIP/ZipLocalHeader.swift b/Sources/ZIP/ZipLocalHeader.swift index e7be7086..e3cd7bae 100644 --- a/Sources/ZIP/ZipLocalHeader.swift +++ b/Sources/ZIP/ZipLocalHeader.swift @@ -38,7 +38,7 @@ struct ZipLocalHeader { let dataOffset: Int - init(_ byteReader: ByteReader) throws { + init(_ byteReader: LittleEndianByteReader) throws { // Check signature. guard byteReader.uint32() == 0x04034b50 else { throw ZipError.wrongSignature } diff --git a/Sources/Zlib/ZlibHeader.swift b/Sources/Zlib/ZlibHeader.swift index 931d13c2..a1fc4bcf 100644 --- a/Sources/Zlib/ZlibHeader.swift +++ b/Sources/Zlib/ZlibHeader.swift @@ -40,17 +40,17 @@ public struct ZlibHeader { - Throws: `ZlibError`. It may indicate that either archive is damaged or it might not be archived with Zlib at all. */ public init(archive data: Data) throws { - let byteReader = ByteReader(data: data) - try self.init(byteReader) + let reader = LsbBitReader(data: data) + try self.init(reader) } - init(_ byteReader: ByteReader) throws { + init(_ reader: LsbBitReader) throws { // Valid Zlib header must contain at least 2 bytes of data. - guard byteReader.bytesLeft >= 2 + guard reader.bytesLeft >= 2 else { throw ZlibError.wrongCompressionMethod } // compressionMethod and compressionInfo combined are needed later for integrity check. - let cmf = byteReader.byte() + let cmf = reader.byte() // First four bits are compression method. // Only compression method = 8 (DEFLATE) is supported. let compressionMethod = cmf & 0xF @@ -67,7 +67,7 @@ public struct ZlibHeader { self.windowSize = windowSize // fcheck, fdict and compresionLevel together make flags byte which is used in integrity check. - let flags = byteReader.byte() + let flags = reader.byte() // First five bits are fcheck bits which are used for integrity check: // let fcheck = flags & 0x1F @@ -85,7 +85,7 @@ public struct ZlibHeader { // If preset dictionary is present 4 bytes will be skipped. if fdict == 1 { - byteReader.offset += 4 + reader.offset += 4 } } diff --git a/Sources/swcomp/Archives/BZip2Command.swift b/Sources/swcomp/Archives/BZip2Command.swift index fb6b627c..04d2f659 100644 --- a/Sources/swcomp/Archives/BZip2Command.swift +++ b/Sources/swcomp/Archives/BZip2Command.swift @@ -10,14 +10,14 @@ import SwiftCLI class BZip2Command: Command { let name = "bz2" - let shortDescription = "Creates or extracts BZip2 archive" + let shortDescription = "Creates or extracts a BZip2 archive" - let compress = Flag("-c", "--compress", description: "Compress input file into BZip2 archive") - let decompress = Flag("-d", "--decompress", description: "Decompress BZip2 archive") + let compress = Flag("-c", "--compress", description: "Compress an input file into a BZip2 archive") + let decompress = Flag("-d", "--decompress", description: "Decompress a BZip2 archive") let blockSize = Key("-b", "--block-size", description: """ - Set block size for compression to a multiple of 100k bytes; \ + Set the block size for compression to a multiple of 100k bytes; \ possible values are from '1' (default) to '9' """) diff --git a/Sources/swcomp/Archives/GZipCommand.swift b/Sources/swcomp/Archives/GZipCommand.swift index 73dee549..fa37590d 100644 --- a/Sources/swcomp/Archives/GZipCommand.swift +++ b/Sources/swcomp/Archives/GZipCommand.swift @@ -10,14 +10,14 @@ import SwiftCLI class GZipCommand: Command { let name = "gz" - let shortDescription = "Creates or extracts GZip archive" + let shortDescription = "Creates or extracts a GZip archive" - let compress = Flag("-c", "--compress", description: "Compress input file into GZip archive") - let decompress = Flag("-d", "--decompress", description: "Decompress GZip archive") - let info = Flag("-i", "--info", description: "Print information from GZip header") + let compress = Flag("-c", "--compress", description: "Compress an input file into a GZip archive") + let decompress = Flag("-d", "--decompress", description: "Decompress a GZip archive") + let info = Flag("-i", "--info", description: "Print information from a GZip header") let useGZipName = Flag("-n", "--use-gzip-name", - description: "Use name saved inside GZip archive as output path, if possible") + description: "Use the name saved inside a GZip archive as an output path, if possible") var optionGroups: [OptionGroup] { let actions = OptionGroup(options: [compress, decompress, info], restriction: .exactlyOne) diff --git a/Sources/swcomp/Archives/LZMACommand.swift b/Sources/swcomp/Archives/LZMACommand.swift index 835c536b..16ea9f06 100644 --- a/Sources/swcomp/Archives/LZMACommand.swift +++ b/Sources/swcomp/Archives/LZMACommand.swift @@ -10,7 +10,7 @@ import SwiftCLI class LZMACommand: Command { let name = "lzma" - let shortDescription = "Extracts LZMA archive" + let shortDescription = "Extracts a LZMA archive" let archive = Parameter() let outputPath = OptionalParameter() diff --git a/Sources/swcomp/Archives/XZCommand.swift b/Sources/swcomp/Archives/XZCommand.swift index 97075da5..3ea58577 100644 --- a/Sources/swcomp/Archives/XZCommand.swift +++ b/Sources/swcomp/Archives/XZCommand.swift @@ -10,7 +10,7 @@ import SwiftCLI class XZCommand: Command { let name = "xz" - let shortDescription = "Extracts XZ archive" + let shortDescription = "Extracts a XZ archive" let archive = Parameter() let outputPath = OptionalParameter() diff --git a/Sources/swcomp/Benchmarks/BenchmarkGroup.swift b/Sources/swcomp/Benchmarks/BenchmarkGroup.swift index 9ab7a25b..984acca1 100644 --- a/Sources/swcomp/Benchmarks/BenchmarkGroup.swift +++ b/Sources/swcomp/Benchmarks/BenchmarkGroup.swift @@ -9,7 +9,7 @@ import SwiftCLI class BenchmarkGroup: CommandGroup { let name = "benchmark" - let shortDescription = "Run specified benchmark using external files" + let shortDescription = "Run the specified benchmark using external files" let children: [Routable] = [UnGzip(), UnXz(), UnBz2(), InfoTar(), InfoZip(), Info7z(), CompDeflate(), CompBz2()] diff --git a/Sources/swcomp/Containers/7ZipCommand.swift b/Sources/swcomp/Containers/7ZipCommand.swift index e321c48b..b96fbea9 100644 --- a/Sources/swcomp/Containers/7ZipCommand.swift +++ b/Sources/swcomp/Containers/7ZipCommand.swift @@ -10,10 +10,10 @@ import SwiftCLI class SevenZipCommand: ContainerCommand { let name = "7z" - let shortDescription = "Extracts 7-Zip container" + let shortDescription = "Extracts a 7-Zip container" - let info = Flag("-i", "--info", description: "Print list of entries in container and their attributes") - let extract = Key("-e", "--extract", description: "Extract container into specified directory") + let info = Flag("-i", "--info", description: "Print the list of entries in a container and their attributes") + let extract = Key("-e", "--extract", description: "Extract a container into the specified directory") let verbose = Flag("-v", "--verbose", description: "Print the list of extracted files and directories.") let archive = Parameter() diff --git a/Sources/swcomp/Containers/TarCommand.swift b/Sources/swcomp/Containers/TarCommand.swift index 24c0579c..54ec091d 100644 --- a/Sources/swcomp/Containers/TarCommand.swift +++ b/Sources/swcomp/Containers/TarCommand.swift @@ -10,17 +10,17 @@ import SwiftCLI class TarCommand: Command { let name = "tar" - let shortDescription = "Extracts TAR container" + let shortDescription = "Extracts a TAR container" let gz = Flag("-z", "--gz", description: "Decompress with GZip first") let bz2 = Flag("-j", "--bz2", description: "Decompress with BZip2 first") let xz = Flag("-x", "--xz", description: "Decompress with XZ first") - let info = Flag("-i", "--info", description: "Print list of entries in container and their attributes") - let extract = Key("-e", "--extract", description: "Extract container into specified directory") - let format = Flag("-f", "--format", description: "Print \"format\" of the container") + let info = Flag("-i", "--info", description: "Print the list of entries in a container and their attributes") + let extract = Key("-e", "--extract", description: "Extract a container into specified directory") + let format = Flag("-f", "--format", description: "Print the \"format\" of a container") let create = Key("-c", "--create", - description: "Create a new container containing specified file/directory (recursively)") + description: "Create a new container containing the specified file/directory (recursively)") let verbose = Flag("-v", "--verbose", description: "Print the list of extracted files and directories.") @@ -93,7 +93,7 @@ class TarCommand: Command { print("d = directory, f = file, l = symbolic link") } let entries = try self.createEntries(inputPath, verbose.value) - let containerData = try TarContainer.create(from: entries) + let containerData = TarContainer.create(from: entries) try containerData.write(to: URL(fileURLWithPath: self.archive.value)) } } diff --git a/Sources/swcomp/Containers/ZipCommand.swift b/Sources/swcomp/Containers/ZipCommand.swift index 164aaa89..f104b072 100644 --- a/Sources/swcomp/Containers/ZipCommand.swift +++ b/Sources/swcomp/Containers/ZipCommand.swift @@ -10,10 +10,10 @@ import SwiftCLI class ZipCommand: ContainerCommand { let name = "zip" - let shortDescription = "Extracts ZIP container" + let shortDescription = "Extracts a ZIP container" - let info = Flag("-i", "--info", description: "Print list of entries in container and their attributes") - let extract = Key("-e", "--extract", description: "Extract container into specified directory") + let info = Flag("-i", "--info", description: "Print the list of entries in a container and their attributes") + let extract = Key("-e", "--extract", description: "Extract a container into the specified directory") let verbose = Flag("-v", "--verbose", description: "Print the list of extracted files and directories.") let archive = Parameter() diff --git a/Sources/swcomp/Extensions/ContainerEntryInfo+CustomStringConvertible.swift b/Sources/swcomp/Extensions/ContainerEntryInfo+CustomStringConvertible.swift index cd6db6a6..783fc7b2 100644 --- a/Sources/swcomp/Extensions/ContainerEntryInfo+CustomStringConvertible.swift +++ b/Sources/swcomp/Extensions/ContainerEntryInfo+CustomStringConvertible.swift @@ -62,7 +62,9 @@ extension ContainerEntryInfo where Self: CustomStringConvertible { } if let zipEntry = self as? ZipEntryInfo { - output += "Comment: \(zipEntry.comment)\n" + if !zipEntry.comment.isEmpty { + output += "Comment: \(zipEntry.comment)\n" + } output += String(format: "External File Attributes: 0x%08X\n", zipEntry.externalFileAttributes) output += "Is text file: \(zipEntry.isTextFile)\n" output += "File system type: \(zipEntry.fileSystemType)\n" diff --git a/Sources/swcomp/main.swift b/Sources/swcomp/main.swift index 860d5169..4b329857 100644 --- a/Sources/swcomp/main.swift +++ b/Sources/swcomp/main.swift @@ -7,9 +7,9 @@ import Foundation import SWCompression import SwiftCLI -let cli = CLI(name: "swcomp", version: "4.5.11", +let cli = CLI(name: "swcomp", version: "4.6.0", description: """ - swcomp - small command-line client for SWCompression framework. + swcomp - a small command-line client for SWCompression framework. Serves as an example of SWCompression usage. """) cli.commands = [XZCommand(), diff --git a/Tests/Sha256Tests.swift b/Tests/Sha256Tests.swift index 166552a4..82cd55f2 100644 --- a/Tests/Sha256Tests.swift +++ b/Tests/Sha256Tests.swift @@ -12,14 +12,14 @@ class Sha256Tests: XCTestCase { func test1() { let message = "" let answer = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" - let hash = Sha256.hash(data: message.data(using: .utf8)!) + let hash = Sha256.hash(data: Data(message.utf8)) XCTAssertEqual(hash.map { String(format: "%02x", $0) }.joined(), answer) } func test2() { let message = "a" let answer = "ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb" - let data = message.data(using: .utf8)! + let data = Data(message.utf8) let hash = Sha256.hash(data: data) XCTAssertEqual(hash.map { String(format: "%02x", $0) }.joined(), answer) } @@ -27,7 +27,7 @@ class Sha256Tests: XCTestCase { func test3() { let message = "abc" let answer = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" - let data = message.data(using: .utf8)! + let data = Data(message.utf8) let hash = Sha256.hash(data: data) XCTAssertEqual(hash.map { String(format: "%02x", $0) }.joined(), answer) } @@ -35,7 +35,7 @@ class Sha256Tests: XCTestCase { func test4() { let message = "message digest" let answer = "f7846f55cf23e14eebeab5b4e1550cad5b509e3348fbc4efa3a1413d393cb650" - let data = message.data(using: .utf8)! + let data = Data(message.utf8) let hash = Sha256.hash(data: data) XCTAssertEqual(hash.map { String(format: "%02x", $0) }.joined(), answer) } @@ -43,7 +43,7 @@ class Sha256Tests: XCTestCase { func test5() { let message = "abcdefghijklmnopqrstuvwxyz" let answer = "71c480df93d6ae2f1efad1447c66c9525e316218cf51fc8d9ed832f2daf18b73" - let data = message.data(using: .utf8)! + let data = Data(message.utf8) let hash = Sha256.hash(data: data) XCTAssertEqual(hash.map { String(format: "%02x", $0) }.joined(), answer) } @@ -51,7 +51,7 @@ class Sha256Tests: XCTestCase { func test6() { let message = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" let answer = "db4bfcbd4da0cd85a60c3c37d3fbd8805c77f15fc6b1fdfe614ee0a7c8fdb4c0" - let data = message.data(using: .utf8)! + let data = Data(message.utf8) let hash = Sha256.hash(data: data) XCTAssertEqual(hash.map { String(format: "%02x", $0) }.joined(), answer) } @@ -59,7 +59,7 @@ class Sha256Tests: XCTestCase { func test7() { let message = "12345678901234567890123456789012345678901234567890123456789012345678901234567890" let answer = "f371bc4a311f2b009eef952dd83ca80e2b60026c8e935592d0f9c308453c813e" - let data = message.data(using: .utf8)! + let data = Data(message.utf8) let hash = Sha256.hash(data: data) XCTAssertEqual(hash.map { String(format: "%02x", $0) }.joined(), answer) } diff --git a/Tests/TarCreateTests.swift b/Tests/TarCreateTests.swift index 4c85bab9..7cb3114e 100644 --- a/Tests/TarCreateTests.swift +++ b/Tests/TarCreateTests.swift @@ -15,15 +15,18 @@ class TarCreateTests: XCTestCase { info.ownerID = 501 info.groupID = 20 info.permissions = Permissions(rawValue: 420) - let date = Date() + // We have to convert time interval to int, since tar can't store fractional timestamps, so we lose in accuracy. + let intTimeInterval = Int(Date().timeIntervalSince1970) + let date = Date(timeIntervalSince1970: Double(intTimeInterval)) info.modificationTime = date info.creationTime = date info.accessTime = date info.comment = "comment" - let data = "Hello, World!\n".data(using: .utf8)! + let data = Data("Hello, World!\n".utf8) let entry = TarEntry(info: info, data: data) - let containerData = try TarContainer.create(from: [entry]) + let containerData = TarContainer.create(from: [entry]) + XCTAssertEqual(try TarContainer.formatOf(container: containerData), .pax) let newEntries = try TarContainer.open(container: containerData) XCTAssertEqual(newEntries.count, 1) @@ -35,6 +38,9 @@ class TarCreateTests: XCTestCase { XCTAssertEqual(newEntries[0].info.ownerID, 501) XCTAssertEqual(newEntries[0].info.groupID, 20) XCTAssertEqual(newEntries[0].info.permissions, Permissions(rawValue: 420)) + XCTAssertEqual(newEntries[0].info.modificationTime, date) + XCTAssertEqual(newEntries[0].info.creationTime, date) + XCTAssertEqual(newEntries[0].info.accessTime, date) XCTAssertEqual(newEntries[0].info.comment, "comment") XCTAssertEqual(newEntries[0].data, data) } @@ -62,7 +68,8 @@ class TarCreateTests: XCTestCase { info.linkName = "file" info.unknownExtendedHeaderRecords = dict - let containerData = try TarContainer.create(from: [TarEntry(info: info, data: Data())]) + let containerData = TarContainer.create(from: [TarEntry(info: info, data: Data())]) + XCTAssertEqual(try TarContainer.formatOf(container: containerData), .pax) let newInfo = try TarContainer.open(container: containerData)[0].info XCTAssertEqual(newInfo.name, "symbolic-link") @@ -90,7 +97,8 @@ class TarCreateTests: XCTestCase { info.name.append(String(repeating: "readme/", count: 15)) info.name.append("readme.txt") - let containerData = try TarContainer.create(from: [TarEntry(info: info, data: Data())]) + let containerData = TarContainer.create(from: [TarEntry(info: info, data: Data())]) + XCTAssertEqual(try TarContainer.formatOf(container: containerData), .pax) let newInfo = try TarContainer.open(container: containerData)[0].info // This name should fit into ustar format using "prefix" field @@ -103,7 +111,8 @@ class TarCreateTests: XCTestCase { info.name.append(String(repeating: "readme/", count: 25)) info.name.append("readme.txt") - let containerData = try TarContainer.create(from: [TarEntry(info: info, data: Data())]) + let containerData = TarContainer.create(from: [TarEntry(info: info, data: Data())]) + XCTAssertEqual(try TarContainer.formatOf(container: containerData), .pax) let newInfo = try TarContainer.open(container: containerData)[0].info XCTAssertEqual(newInfo.name, info.name) @@ -115,7 +124,8 @@ class TarCreateTests: XCTestCase { info.name = "path/to/" info.name.append(String(repeating: "readme/", count: 15)) - let containerData = try TarContainer.create(from: [TarEntry(info: info, data: Data())]) + let containerData = TarContainer.create(from: [TarEntry(info: info, data: Data())]) + XCTAssertEqual(try TarContainer.formatOf(container: containerData), .pax) let newInfo = try TarContainer.open(container: containerData)[0].info XCTAssertEqual(newInfo.name, info.name) @@ -137,7 +147,7 @@ class TarCreateTests: XCTestCase { info.comment = "комментарий" info.linkName = "путь/к/файлу" - let containerData = try TarContainer.create(from: [TarEntry(info: info, data: Data())]) + let containerData = TarContainer.create(from: [TarEntry(info: info, data: Data())]) XCTAssertEqual(try TarContainer.formatOf(container: containerData), .pax) let newInfo = try TarContainer.open(container: containerData)[0].info @@ -165,7 +175,7 @@ class TarCreateTests: XCTestCase { info.groupID = 20 info.modificationTime = date - let containerData = try TarContainer.create(from: [TarEntry(info: info, data: Data())]) + let containerData = TarContainer.create(from: [TarEntry(info: info, data: Data())], force: .ustar) XCTAssertEqual(try TarContainer.formatOf(container: containerData), .ustar) let newInfo = try TarContainer.open(container: containerData)[0].info @@ -189,7 +199,7 @@ class TarCreateTests: XCTestCase { var info = TarEntryInfo(name: "file.txt", type: .regular) info.modificationTime = date - let containerData = try TarContainer.create(from: [TarEntry(info: info, data: Data())]) + let containerData = TarContainer.create(from: [TarEntry(info: info, data: Data())]) XCTAssertEqual(try TarContainer.formatOf(container: containerData), .pax) let newInfo = try TarContainer.open(container: containerData)[0].info @@ -209,27 +219,105 @@ class TarCreateTests: XCTestCase { } func testBigUid() throws { - let uid = (1 << 32) - 1 - var info = TarEntryInfo(name: "file.txt", type: .regular) - info.ownerID = uid + // Int.max tests that base-256 encoding of integer fields works in the edge case. + for uid in [(1 << 32) - 1, Int.max] { + var info = TarEntryInfo(name: "file.txt", type: .regular) + info.ownerID = uid - let containerData = try TarContainer.create(from: [TarEntry(info: info, data: Data())]) - XCTAssertEqual(try TarContainer.formatOf(container: containerData), .pax) + let containerData = TarContainer.create(from: [TarEntry(info: info, data: Data())]) + XCTAssertEqual(try TarContainer.formatOf(container: containerData), .pax) + let newInfo = try TarContainer.open(container: containerData)[0].info + + XCTAssertEqual(newInfo.name, "file.txt") + XCTAssertEqual(newInfo.type, .regular) + XCTAssertEqual(newInfo.size, 0) + XCTAssertEqual(newInfo.ownerID, uid) + XCTAssertEqual(newInfo.linkName, "") + XCTAssertEqual(newInfo.ownerUserName, "") + XCTAssertEqual(newInfo.ownerGroupName, "") + XCTAssertNil(newInfo.permissions) + XCTAssertNil(newInfo.groupID) + XCTAssertNil(newInfo.accessTime) + XCTAssertNil(newInfo.creationTime) + XCTAssertNil(newInfo.modificationTime) + XCTAssertNil(newInfo.comment) + } + } + + func testGnuLongName() throws { + var info = TarEntryInfo(name: "", type: .regular) + info.name = "path/to/" + info.name.append(String(repeating: "name/", count: 25)) + info.name.append("name.txt") + + let containerData = TarContainer.create(from: [TarEntry(info: info, data: Data())], force: .gnu) + XCTAssertEqual(try TarContainer.formatOf(container: containerData), .gnu) let newInfo = try TarContainer.open(container: containerData)[0].info - XCTAssertEqual(newInfo.name, "file.txt") - XCTAssertEqual(newInfo.type, .regular) - XCTAssertEqual(newInfo.size, 0) - XCTAssertEqual(newInfo.ownerID, uid) - XCTAssertEqual(newInfo.linkName, "") - XCTAssertEqual(newInfo.ownerUserName, "") - XCTAssertEqual(newInfo.ownerGroupName, "") - XCTAssertNil(newInfo.permissions) - XCTAssertNil(newInfo.groupID) - XCTAssertNil(newInfo.accessTime) - XCTAssertNil(newInfo.creationTime) - XCTAssertNil(newInfo.modificationTime) - XCTAssertNil(newInfo.comment) + XCTAssertEqual(newInfo.name, info.name) + } + + func testGnuLongLinkName() throws { + var info = TarEntryInfo(name: "", type: .symbolicLink) + info.name = "link" + info.linkName = "path/to/" + info.linkName.append(String(repeating: "name/", count: 25)) + info.linkName.append("name.txt") + + let containerData = TarContainer.create(from: [TarEntry(info: info, data: Data())], force: .gnu) + XCTAssertEqual(try TarContainer.formatOf(container: containerData), .gnu) + let newInfo = try TarContainer.open(container: containerData)[0].info + + XCTAssertEqual(newInfo.name, info.name) + } + + func testGnuBothLongNames() throws { + var info = TarEntryInfo(name: "", type: .symbolicLink) + info.name = "path/to/" + info.name.append(String(repeating: "name/", count: 25)) + info.name.append("name.txt") + info.linkName = "path/to/" + info.linkName.append(String(repeating: "link/", count: 25)) + info.linkName.append("link.txt") + + let containerData = TarContainer.create(from: [TarEntry(info: info, data: Data())], force: .gnu) + XCTAssertEqual(try TarContainer.formatOf(container: containerData), .gnu) + let newInfo = try TarContainer.open(container: containerData)[0].info + + XCTAssertEqual(newInfo.name, info.name) + } + + func testGnuTimes() throws { + var info = TarEntryInfo(name: "dir", type: .directory) + info.ownerUserName = "tsolomko" + info.ownerGroupName = "staff" + info.ownerID = 501 + info.groupID = 20 + info.permissions = Permissions(rawValue: 420) + // We have to convert time interval to int, since tar can't store fractional timestamps, so we lose in accuracy. + let intTimeInterval = Int(Date().timeIntervalSince1970) + let date = Date(timeIntervalSince1970: Double(intTimeInterval)) + info.modificationTime = date + info.creationTime = date + info.accessTime = date + + let containerData = TarContainer.create(from: [TarEntry(info: info, data: Data())], force: .gnu) + XCTAssertEqual(try TarContainer.formatOf(container: containerData), .gnu) + let newEntries = try TarContainer.open(container: containerData) + + XCTAssertEqual(newEntries.count, 1) + XCTAssertEqual(newEntries[0].info.name, "dir") + XCTAssertEqual(newEntries[0].info.type, .directory) + XCTAssertEqual(newEntries[0].info.size, 0) + XCTAssertEqual(newEntries[0].info.ownerUserName, "tsolomko") + XCTAssertEqual(newEntries[0].info.ownerGroupName, "staff") + XCTAssertEqual(newEntries[0].info.ownerID, 501) + XCTAssertEqual(newEntries[0].info.groupID, 20) + XCTAssertEqual(newEntries[0].info.permissions, Permissions(rawValue: 420)) + XCTAssertEqual(newEntries[0].info.modificationTime, date) + XCTAssertEqual(newEntries[0].info.creationTime, date) + XCTAssertEqual(newEntries[0].info.accessTime, date) + XCTAssertNil(newEntries[0].info.comment) } } diff --git a/Tests/TarTests.swift b/Tests/TarTests.swift index a5db96e2..b764390e 100644 --- a/Tests/TarTests.swift +++ b/Tests/TarTests.swift @@ -191,6 +191,28 @@ class TarTests: XCTestCase { XCTAssertEqual(entries[0].data, nil) } + func testOnlyDirectoryHeader() throws { + // This tests the correct handling of the situation when there is nothing in the container but one basic header, + // even no EOF marker (two blocks of zeros). + let testData = try Constants.data(forTest: "test_only_dir_header", withType: TarTests.testType) + + XCTAssertEqual(try TarContainer.formatOf(container: testData), .ustar) + + let entries = try TarContainer.open(container: testData) + + XCTAssertEqual(entries.count, 1) + XCTAssertEqual(entries[0].info.name, "empty_dir/") + XCTAssertEqual(entries[0].info.type, .directory) + XCTAssertEqual(entries[0].info.size, 0) + XCTAssertEqual(entries[0].info.ownerID, 501) + XCTAssertEqual(entries[0].info.groupID, 20) + XCTAssertEqual(entries[0].info.ownerUserName, "timofeysolomko") + XCTAssertEqual(entries[0].info.ownerGroupName, "staff") + XCTAssertEqual(entries[0].info.permissions, Permissions(rawValue: 493)) + XCTAssertNil(entries[0].info.comment) + XCTAssertEqual(entries[0].data, nil) + } + func testEmptyContainer() throws { let testData = try Constants.data(forTest: "test_empty_cont", withType: TarTests.testType) diff --git a/Tests/Test Files b/Tests/Test Files index b11a3090..fe35c32f 160000 --- a/Tests/Test Files +++ b/Tests/Test Files @@ -1 +1 @@ -Subproject commit b11a3090304037141155b6aab01ae49c94ee12bf +Subproject commit fe35c32fdab1aab6becabc1ade18408a42e32113 diff --git a/Tests/TestZipExtraField.swift b/Tests/TestZipExtraField.swift index 38db51df..5f99ba69 100644 --- a/Tests/TestZipExtraField.swift +++ b/Tests/TestZipExtraField.swift @@ -16,7 +16,7 @@ struct TestZipExtraField: ZipExtraField { var helloString: String? - init(_ byteReader: ByteReader, _ size: Int, location: ZipExtraFieldLocation) { + init(_ byteReader: LittleEndianByteReader, _ size: Int, location: ZipExtraFieldLocation) { self.size = size self.location = location self.helloString = String(data: Data(byteReader.bytes(count: size)), encoding: .utf8) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index ec72f9e8..24a89f58 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -12,18 +12,6 @@ stages: - stage: test displayName: 'Build & Test' jobs: - - job: macosSwift42 - pool: - vmImage: 'macOS-10.14' - variables: - DEVELOPER_DIR: '/Applications/Xcode_10.1.app' - steps: - - script: | - set -e -o xtrace - swift --version - swift build - swift build -c release - displayName: 'Build SPM Debug & Release' - job: macos # TODO: Add Xcode 12.5/Swift 5.4. It requires macOS 11 which is currently unavailable on AZP. For Xcode 12.5 we can also enable testing on watchos. strategy: @@ -72,9 +60,6 @@ stages: - job: linux strategy: matrix: - linuxSwift42: - imageName: 'ubuntu-18.04' - containerImage: 'swift:4.2.4' linuxSwift50: imageName: 'ubuntu-18.04' containerImage: 'swift:5.0.3-bionic' @@ -89,7 +74,7 @@ stages: containerImage: 'swift:5.3.3-bionic' linuxSwift54: imageName: 'ubuntu-18.04' - containerImage: 'swift:5.4.1-bionic' + containerImage: 'swift:5.4.2-bionic' pool: vmImage: $(imageName) container: $[ variables['containerImage'] ] @@ -105,7 +90,7 @@ stages: matrix: windowsSwift54: imageName: 'windows-2019' - SWIFT_VERSION: '5.4.1' + SWIFT_VERSION: '5.4.2' pool: vmImage: $(imageName) variables: