mirror of
https://github.com/tsolomko/SWCompression.git
synced 2026-06-23 14:56:41 +00:00
Merge branch 'release-4.6.0'
This commit is contained in:
+2
-2
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1 +1 @@
|
||||
github "tsolomko/BitByteData" ~> 1.4.4
|
||||
github "tsolomko/BitByteData" ~> 2.0.0
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
@@ -1,6 +1,5 @@
|
||||
# SWCompression
|
||||
|
||||
[](https://developer.apple.com/swift/)
|
||||
[](https://developer.apple.com/swift/)
|
||||
[](https://raw.githubusercontent.com/tsolomko/SWCompression/master/LICENSE)
|
||||
[](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/<platform>/` 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/<platform>/` 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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>4.5.11</string>
|
||||
<string>4.6.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>78</string>
|
||||
<string>80</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2021 Timofey Solomko</string>
|
||||
</dict>
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>4.5.11</string>
|
||||
<string>4.6.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>78</string>
|
||||
<string>80</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -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 = "<group>"; };
|
||||
0651FEC11F8E2E9100C49646 /* ContainerEntryType+Tar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContainerEntryType+Tar.swift"; sourceTree = "<group>"; };
|
||||
0656B8251F8A960700111605 /* TarEntryInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TarEntryInfo.swift; sourceTree = "<group>"; };
|
||||
0656B82A1F8A979A00111605 /* ByteReader+Tar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ByteReader+Tar.swift"; sourceTree = "<group>"; };
|
||||
0656B82A1F8A979A00111605 /* LittleEndianByteReader+Tar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LittleEndianByteReader+Tar.swift"; sourceTree = "<group>"; };
|
||||
06595E0D1F1E8252006501C2 /* 7zFolder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 7zFolder.swift; sourceTree = "<group>"; };
|
||||
06595E121F1E90C2006501C2 /* 7zCoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = 7zCoder.swift; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
|
||||
065D0ADB1F2B9D9100CE2DA8 /* 7zEntryInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 7zEntryInfo.swift; sourceTree = "<group>"; };
|
||||
@@ -325,13 +328,13 @@
|
||||
06CDFCA71F111D9700292758 /* DeflateError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeflateError.swift; sourceTree = "<group>"; };
|
||||
06CDFCAC1F111DBE00292758 /* LZMAError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LZMAError.swift; sourceTree = "<group>"; };
|
||||
06CDFCB11F111DEC00292758 /* LZMA2Error.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LZMA2Error.swift; sourceTree = "<group>"; };
|
||||
06CFF6B12078C5A5003B8375 /* TarEntryInfoProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TarEntryInfoProvider.swift; sourceTree = "<group>"; };
|
||||
06CFF6B12078C5A5003B8375 /* TarParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TarParser.swift; sourceTree = "<group>"; };
|
||||
06D3802D21E36190008B50C6 /* Code.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Code.swift; sourceTree = "<group>"; };
|
||||
06D42DA42067B46800C1A98B /* test_custom_extra_field.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = test_custom_extra_field.zip; sourceTree = "<group>"; };
|
||||
06D42DA62067B71000C1A98B /* TestZipExtraField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestZipExtraField.swift; sourceTree = "<group>"; };
|
||||
06D95AC01F4C54E0006B46AC /* BZip2+Compress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BZip2+Compress.swift"; sourceTree = "<group>"; };
|
||||
06DDB1701F8190D400035BEF /* BZip2+BlockSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BZip2+BlockSize.swift"; sourceTree = "<group>"; };
|
||||
06DE29B71FB7024C0098A761 /* ByteReader+Zip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ByteReader+Zip.swift"; sourceTree = "<group>"; };
|
||||
06DE29B71FB7024C0098A761 /* LittleEndianByteReader+Zip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LittleEndianByteReader+Zip.swift"; sourceTree = "<group>"; };
|
||||
06E2346B1E068E9600F18798 /* XZArchive.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XZArchive.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
E60CBA8926AF379200A20130 /* test_only_dir_header.tar */ = {isa = PBXFileReference; lastKnownFileType = archive.tar; path = test_only_dir_header.tar; sourceTree = "<group>"; };
|
||||
E648151C26A889B8009261F0 /* TarHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TarHeader.swift; sourceTree = "<group>"; };
|
||||
E64F71BB26AAFD6A008BB308 /* Data+Tar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Tar.swift"; sourceTree = "<group>"; };
|
||||
E652D8F2263D670900FC229B /* test_nonstandard_runlength.bz2 */ = {isa = PBXFileReference; lastKnownFileType = file; path = test_nonstandard_runlength.bz2; sourceTree = "<group>"; };
|
||||
E652D8F4263D678000FC229B /* test_nonstandard_runlength.answer */ = {isa = PBXFileReference; lastKnownFileType = file; path = test_nonstandard_runlength.answer; sourceTree = "<group>"; };
|
||||
E66F36232538726E00076A6E /* test_empty.lzma */ = {isa = PBXFileReference; lastKnownFileType = file; path = test_empty.lzma; sourceTree = "<group>"; };
|
||||
@@ -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 = "<group>";
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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..<xlen {
|
||||
headerBytes.append(byteReader.byte())
|
||||
headerBytes.append(reader.byte())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ public struct GzipHeader {
|
||||
if flags.contains(.fname) {
|
||||
var fnameBytes: [UInt8] = []
|
||||
while true {
|
||||
let byte = byteReader.byte()
|
||||
let byte = reader.byte()
|
||||
headerBytes.append(byte)
|
||||
guard byte != 0 else { break }
|
||||
fnameBytes.append(byte)
|
||||
@@ -127,7 +127,7 @@ public struct GzipHeader {
|
||||
if flags.contains(.fcomment) {
|
||||
var fcommentBytes: [UInt8] = []
|
||||
while true {
|
||||
let byte = byteReader.byte()
|
||||
let byte = reader.byte()
|
||||
headerBytes.append(byte)
|
||||
guard byte != 0 else { break }
|
||||
fcommentBytes.append(byte)
|
||||
@@ -140,7 +140,7 @@ public struct GzipHeader {
|
||||
// Some archives may contain 2-bytes checksum
|
||||
if flags.contains(.fhcrc) {
|
||||
// Note: it is not actual CRC-16, it is just two least significant bytes of CRC-32.
|
||||
let crc16 = byteReader.uint16()
|
||||
let crc16 = reader.uint16()
|
||||
guard CheckSums.crc32(headerBytes) & 0xFFFF == crc16 else { throw GzipError.wrongHeaderCRC }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ public class LZMA: DecompressionAlgorithm {
|
||||
guard data.count >= 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)
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
+148
-59
@@ -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..<dataEndIndex)
|
||||
entries.append(TarEntry(info: entryInfo, data: entryData))
|
||||
parsingLoop: while true {
|
||||
let result = try parser.next()
|
||||
switch result {
|
||||
case .specialEntry:
|
||||
continue parsingLoop
|
||||
case .entryInfo(let info, let blockStartIndex, _):
|
||||
if info.type == .directory {
|
||||
var entry = TarEntry(info: info, data: nil)
|
||||
entry.info.size = 0
|
||||
entries.append(entry)
|
||||
} else {
|
||||
let dataStartIndex = blockStartIndex + 512
|
||||
let dataEndIndex = dataStartIndex + info.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 can 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.tooSmallFileIsPassed }
|
||||
let entryData = data.subdata(in: dataStartIndex..<dataEndIndex)
|
||||
entries.append(TarEntry(info: info, data: entryData))
|
||||
}
|
||||
case .truncated:
|
||||
// We don't have an error with a more suitable name.
|
||||
throw TarError.tooSmallFileIsPassed
|
||||
case .finished:
|
||||
fallthrough
|
||||
case .eofMarker:
|
||||
break parsingLoop
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,18 +238,24 @@ public class TarContainer: Container {
|
||||
- Returns: Array of `TarEntryInfo`.
|
||||
*/
|
||||
public static func info(container data: Data) throws -> [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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+59
-305
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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..<byteReader.offset + 1024] != Data(count: 1024)
|
||||
else { return nil }
|
||||
|
||||
let info = try TarEntryInfo(byteReader, lastGlobalExtendedHeader, lastLocalExtendedHeader,
|
||||
longName, longLinkName)
|
||||
let dataStartIndex = info.blockStartIndex + 512
|
||||
|
||||
if let specialEntryType = info.specialEntryType {
|
||||
switch specialEntryType {
|
||||
case .globalExtendedHeader:
|
||||
let dataEndIndex = dataStartIndex + info.size!
|
||||
lastGlobalExtendedHeader = try TarExtendedHeader(byteReader.data[dataStartIndex..<dataEndIndex])
|
||||
case .sunExtendedHeader:
|
||||
fallthrough
|
||||
case .localExtendedHeader:
|
||||
let dataEndIndex = dataStartIndex + info.size!
|
||||
lastLocalExtendedHeader = try TarExtendedHeader(byteReader.data[dataStartIndex..<dataEndIndex])
|
||||
case .longLinkName:
|
||||
byteReader.offset = dataStartIndex
|
||||
longLinkName = byteReader.tarCString(maxLength: info.size!)
|
||||
case .longName:
|
||||
byteReader.offset = dataStartIndex
|
||||
longName = byteReader.tarCString(maxLength: info.size!)
|
||||
}
|
||||
byteReader.offset = dataStartIndex + info.size!.roundTo512()
|
||||
} else {
|
||||
// Skip file data.
|
||||
byteReader.offset = dataStartIndex + info.size!.roundTo512()
|
||||
lastLocalExtendedHeader = nil
|
||||
longName = nil
|
||||
longLinkName = nil
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import Foundation
|
||||
It may indicate that either container is damaged or it might not be TAR container at all.
|
||||
*/
|
||||
public enum TarError: Error {
|
||||
/// Size of data is too small, even to contain only one header.
|
||||
/// Data is unexpectedly truncated.
|
||||
case tooSmallFileIsPassed
|
||||
/// Failed to process a *required* TAR header's field.
|
||||
case wrongField
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Also known as PAX header.
|
||||
struct TarExtendedHeader {
|
||||
|
||||
var unknownRecords = [String: String]()
|
||||
@@ -112,6 +113,11 @@ struct TarExtendedHeader {
|
||||
(mtime < 0 || mtime > 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"
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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..<reader.offset + 1024] == Data(count: 1024) {
|
||||
return .eofMarker
|
||||
} else if reader.bytesLeft < 512 {
|
||||
return .truncated
|
||||
}
|
||||
|
||||
let header = try TarHeader(reader)
|
||||
// For header we read at most 512 bytes.
|
||||
assert(reader.offset - header.blockStartIndex <= 512)
|
||||
// Check, just in case, since we use blockStartIndex = -1 when creating TAR containers.
|
||||
assert(header.blockStartIndex >= 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..<dataEndIndex])
|
||||
case .sunExtendedHeader:
|
||||
fallthrough
|
||||
case .localExtendedHeader:
|
||||
let dataEndIndex = dataStartIndex + header.size
|
||||
lastLocalExtendedHeader = try TarExtendedHeader(reader.data[dataStartIndex..<dataEndIndex])
|
||||
case .longLinkName:
|
||||
reader.offset = dataStartIndex
|
||||
longLinkName = reader.tarCString(maxLength: header.size)
|
||||
case .longName:
|
||||
reader.offset = dataStartIndex
|
||||
longName = reader.tarCString(maxLength: header.size)
|
||||
}
|
||||
reader.offset = dataStartIndex + header.size.roundTo512()
|
||||
return .specialEntry(specialEntryType)
|
||||
} else {
|
||||
let info = TarEntryInfo(header, lastGlobalExtendedHeader, lastLocalExtendedHeader, longName, longLinkName)
|
||||
// Skip file data.
|
||||
reader.offset = dataStartIndex + header.size.roundTo512()
|
||||
lastLocalExtendedHeader = nil
|
||||
longName = nil
|
||||
longLinkName = nil
|
||||
return .entryInfo(info, header.blockStartIndex, header.format)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
import Foundation
|
||||
import BitByteData
|
||||
|
||||
extension ByteReader {
|
||||
extension LittleEndianByteReader {
|
||||
|
||||
func multiByteDecode() throws -> Int {
|
||||
var i = 1
|
||||
|
||||
@@ -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<UInt8>.size))
|
||||
}
|
||||
#endif
|
||||
var bytes = data.withUnsafeBytes { $0.map { $0 } }
|
||||
|
||||
let originalLength = bytes.count
|
||||
var newLength = originalLength * 8 + 1
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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..<filtersCount {
|
||||
let filterID = try byteReader.multiByteDecode()
|
||||
guard UInt64(filterID) < 0x4000000000000000
|
||||
@@ -75,7 +75,7 @@ struct XZBlock {
|
||||
byteReader.offset += 4
|
||||
|
||||
let compressedDataStart = byteReader.offset
|
||||
let out = try filters.reversed().reduce(byteReader) { try ByteReader(data: $1($0)) }
|
||||
let out = try filters.reversed().reduce(byteReader) { try LittleEndianByteReader(data: $1($0)) }
|
||||
|
||||
guard compressedSize < 0 || compressedSize == byteReader.offset - compressedDataStart,
|
||||
uncompressedSize < 0 || uncompressedSize == out.data.count
|
||||
|
||||
@@ -31,7 +31,7 @@ struct XZStreamHeader {
|
||||
let checkType: CheckType
|
||||
let flagsCRC: UInt32
|
||||
|
||||
init(_ byteReader: ByteReader) throws {
|
||||
init(_ byteReader: LittleEndianByteReader) throws {
|
||||
// Check magic number.
|
||||
guard byteReader.bytes(count: 6) == [0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00]
|
||||
else { throw XZError.wrongMagic }
|
||||
|
||||
@@ -16,7 +16,7 @@ struct ExtendedTimestampExtraField: ZipExtraField {
|
||||
var ctime: UInt32?
|
||||
var mtime: UInt32?
|
||||
|
||||
init(_ byteReader: ByteReader, _ size: Int, location: ZipExtraFieldLocation) {
|
||||
init(_ byteReader: LittleEndianByteReader, _ size: Int, location: ZipExtraFieldLocation) {
|
||||
let endOffset = byteReader.offset + size
|
||||
self.size = size
|
||||
self.location = location
|
||||
@@ -56,7 +56,7 @@ struct NtfsExtraField: ZipExtraField {
|
||||
let ctime: UInt64
|
||||
let mtime: UInt64
|
||||
|
||||
init?(_ byteReader: ByteReader, _ size: Int, location: ZipExtraFieldLocation) {
|
||||
init?(_ byteReader: LittleEndianByteReader, _ size: Int, location: ZipExtraFieldLocation) {
|
||||
self.size = size
|
||||
self.location = location
|
||||
byteReader.offset += 4 // Skip reserved bytes
|
||||
@@ -81,7 +81,7 @@ struct InfoZipUnixExtraField: ZipExtraField {
|
||||
let uid: Int
|
||||
let gid: Int
|
||||
|
||||
init?(_ byteReader: ByteReader, _ size: Int, location: ZipExtraFieldLocation) {
|
||||
init?(_ byteReader: LittleEndianByteReader, _ size: Int, location: ZipExtraFieldLocation) {
|
||||
self.size = size
|
||||
self.location = location
|
||||
switch location {
|
||||
@@ -105,7 +105,7 @@ struct InfoZipNewUnixExtraField: ZipExtraField {
|
||||
var uid: Int?
|
||||
var gid: Int?
|
||||
|
||||
init?(_ byteReader: ByteReader, _ size: Int, location: ZipExtraFieldLocation) {
|
||||
init?(_ byteReader: LittleEndianByteReader, _ size: Int, location: ZipExtraFieldLocation) {
|
||||
self.size = size
|
||||
self.location = location
|
||||
guard byteReader.byte() == 1 // Version must be 1
|
||||
|
||||
@@ -10,7 +10,7 @@ import BitByteData
|
||||
import CoreFoundation
|
||||
#endif
|
||||
|
||||
extension ByteReader {
|
||||
extension LittleEndianByteReader {
|
||||
|
||||
func zipString(_ length: Int, _ useUtf8: Bool) -> String? {
|
||||
guard length > 0
|
||||
@@ -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 }
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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'.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<BZip2.BlockSize>("-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'
|
||||
""")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()]
|
||||
|
||||
|
||||
@@ -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<String>("-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<String>("-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()
|
||||
|
||||
@@ -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<String>("-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<String>("-e", "--extract", description: "Extract a container into specified directory")
|
||||
let format = Flag("-f", "--format", description: "Print the \"format\" of a container")
|
||||
let create = Key<String>("-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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String>("-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<String>("-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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+116
-28
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
+1
-1
Submodule Tests/Test Files updated: b11a309030...fe35c32fda
@@ -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)
|
||||
|
||||
+2
-17
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user