Merge branch 'release-4.6.0'

This commit is contained in:
Timofey Solomko
2021-08-04 15:51:09 +03:00
64 changed files with 1022 additions and 710 deletions
+2 -2
View File
@@ -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:
+21
View File
@@ -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
View File
@@ -1 +1 @@
github "tsolomko/BitByteData" ~> 1.4.4
github "tsolomko/BitByteData" ~> 2.0.0
+1 -1
View File
@@ -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.
-32
View File
@@ -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]
)
+17 -18
View File
@@ -1,6 +1,5 @@
# SWCompression
[![Swift 4.2](https://img.shields.io/badge/Swift-4.2-blue.svg)](https://developer.apple.com/swift/)
[![Swift 5.X](https://img.shields.io/badge/Swift-5.X-blue.svg)](https://developer.apple.com/swift/)
[![GitHub license](https://img.shields.io/badge/license-MIT-lightgrey.svg)](https://raw.githubusercontent.com/tsolomko/SWCompression/master/LICENSE)
[![Build Status](https://travis-ci.com/tsolomko/SWCompression.svg?branch=develop)](https://travis-ci.com/tsolomko/SWCompression)
@@ -10,7 +9,7 @@ A framework with (de)compression algorithms and functions for working with vario
## What is this?
SWCompression — is a framework with a collection of functions for:
SWCompression is a framework with a collection of functions for:
1. Decompression (and sometimes compression) using different algorithms.
2. Reading (and sometimes writing) archives of different formats.
@@ -34,7 +33,7 @@ Also, SWCompression is _written with Swift only._
## Installation
SWCompression can be integrated into your project using Swift Package Manager, CocoaPods or Carthage.
SWCompression can be integrated into your project using Swift Package Manager, CocoaPods, or Carthage.
### Swift Package Manager
@@ -47,7 +46,7 @@ let package = Package(
name: "PackageName",
dependencies: [
.package(url: "https://github.com/tsolomko/SWCompression.git",
from: "4.5.0")
from: "4.6.0")
],
targets: [
.target(
@@ -58,11 +57,11 @@ let package = Package(
)
```
More details you can find in [Swift Package Manager's Documentation](https://github.com/apple/swift-package-manager/tree/master/Documentation).
More details you can find in [Swift Package Manager's Documentation](https://github.com/apple/swift-package-manager/tree/main/Documentation).
### CocoaPods
Add `pod 'SWCompression', '~> 4.5'` and `use_frameworks!` lines to your Podfile.
Add `pod 'SWCompression', '~> 4.6'` and `use_frameworks!` lines to your Podfile.
To complete installation, run `pod install`.
@@ -105,19 +104,17 @@ BZip2 and LZMA/LZMA2 support).
### Carthage
__Important:__ Only Swift 5.x is supported when installing SWCompression via Carthage.
Add to your Cartfile `github "tsolomko/SWCompression" ~> 4.5`.
Add to your Cartfile `github "tsolomko/SWCompression" ~> 4.6`.
Then:
1. If you use Xcode 12 or later you should run `carthage update --use-xcframeworks --no-use-binaries`. After that drag
and drop both `SWCompression.xcframework` and `BitByteData.xcframework` files from from the `Carthage/Build/` directory
into the "Frameworks, Libraries, and Embedded Content" section of your target's "General" tab in Xcode.
1. If you use Xcode 12 or later you should run `carthage update --use-xcframeworks`. After that drag and drop both
`SWCompression.xcframework` and `BitByteData.xcframework` files from from the `Carthage/Build/` directory into the
"Frameworks, Libraries, and Embedded Content" section of your target's "General" tab in Xcode.
2. If you use Xcode 11 or earlier you should run `carthage update --no-use-binaries`. After that drag and drop both
`SWCompression.framework` and `BitByteData.framework` files from from the `Carthage/Build/<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"
+4 -4
View File
@@ -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"
+2 -2
View File
@@ -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>
+30 -18
View File
@@ -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";
+4 -4
View File
@@ -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
+2 -2
View File
@@ -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 {
+2 -2
View File
@@ -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
+3 -3
View File
@@ -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
+16 -16
View File
@@ -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 }
}
}
+3 -3
View File
@@ -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)
+3 -1
View File
@@ -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
}
+2 -2
View File
@@ -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
}
+1 -1
View File
@@ -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))
}
+4 -4
View File
@@ -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.
+2 -2
View File
@@ -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)
+2 -2
View File
@@ -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)
+61
View File
@@ -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
View File
@@ -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 -2
View File
@@ -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
}
-10
View File
@@ -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
View File
@@ -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)
}
}
-60
View File
@@ -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
}
}
+1 -1
View File
@@ -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
+23 -18
View File
@@ -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"
}
+300
View File
@@ -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
}
}
+77
View File
@@ -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)
}
}
}
+1 -1
View File
@@ -6,7 +6,7 @@
import Foundation
import BitByteData
extension ByteReader {
extension LittleEndianByteReader {
func multiByteDecode() throws -> Int {
var i = 1
+1 -7
View File
@@ -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
+6 -7
View File
@@ -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 }
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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 }
+4 -4
View File
@@ -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
+1 -1
View File
@@ -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 }
+2 -2
View File
@@ -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.
+13 -11
View File
@@ -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'.
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
}
+1 -1
View File
@@ -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 }
+7 -7
View File
@@ -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
}
}
+4 -4
View File
@@ -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'
""")
+5 -5
View File
@@ -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)
+1 -1
View File
@@ -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()
+1 -1
View File
@@ -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()]
+3 -3
View File
@@ -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()
+6 -6
View File
@@ -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))
}
}
+3 -3
View File
@@ -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"
+2 -2
View File
@@ -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(),
+7 -7
View File
@@ -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
View File
@@ -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)
}
}
+22
View File
@@ -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
View File
@@ -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
View File
@@ -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: