diff --git a/.jazzy.yaml b/.jazzy.yaml index 247e1986..9e1d8f58 100644 --- a/.jazzy.yaml +++ b/.jazzy.yaml @@ -3,11 +3,11 @@ sourcekitten_sourcefile: docs.json clean: false author: Timofey Solomko module: SWCompression -module_version: 4.5.11 +module_version: 4.6.0 copyright: '© 2021 Timofey Solomko' readme: README.md github_url: https://github.com/tsolomko/SWCompression -github_file_prefix: https://github.com/tsolomko/SWCompression/tree/4.5.11 +github_file_prefix: https://github.com/tsolomko/SWCompression/tree/4.6.0 theme: fullwidth custom_categories: diff --git a/CHANGELOG.md b/CHANGELOG.md index d47da297..85bb1312 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## 4.6.0 + +- Swift 4.2 is no longer supported. +- Minimum iOS deployment version (when installed via CocoaPods or Carthage) is now 9.0. +- Increased the lowest required version of BitByteData dependency to 2.0. +- Added the `TarContainer.create(from:force:)` function which allows to specify TAR format. + - It is now possible to create TAR containers of GNU, ustar (POSIX), and pre-POSIX formats. +- The `TarContainer.create(from:)` function (alongside with the newly added function) no longer throws. + - The `TarCreateError.utf8NonEncodable` error is now never thrown. +- Handling of truncated TAR containers should now be more consistent. + - Previously introduced check for truncated containers now throws the `TarError.tooSmallFileIsPassed` error instead + of `TarError.wrongField`. +- Documentation has been updated: + - Adjusted documentation of the `TarEntryInfo` properties to account for other formats used in creation of a + container. + - Adjusted documentation of the `TarError.tooSmallFileIsPassed` error to mention its use in all situations when + truncated data is encountered. +- swcomp changes: + - `zip -i` command now prints entry comments only if they are not empty. + - Slight grammatical improvements to the help messages of swcomp. + ## 4.5.11 - Fixed a crash when processing a truncated TAR file. diff --git a/Cartfile b/Cartfile index 00e63398..656bf838 100644 --- a/Cartfile +++ b/Cartfile @@ -1 +1 @@ -github "tsolomko/BitByteData" "2.0.0-test.2" +github "tsolomko/BitByteData" ~> 2.0.0 diff --git a/Package.swift b/Package.swift index 0586a39d..e63587cd 100644 --- a/Package.swift +++ b/Package.swift @@ -13,7 +13,7 @@ let package = Package( // .package(url: "https://github.com/jakeheis/SwiftCLI", // from: "5.2.0"), .package(url: "https://github.com/tsolomko/BitByteData", - from: "2.0.0-test.2"), + from: "2.0.0"), ], targets: [ // SWCOMP: Uncomment the lines below to build swcomp example program. diff --git a/README.md b/README.md index 8a1da6b4..d59f3905 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ # SWCompression -[![Swift 4.2](https://img.shields.io/badge/Swift-4.2-blue.svg)](https://developer.apple.com/swift/) [![Swift 5.X](https://img.shields.io/badge/Swift-5.X-blue.svg)](https://developer.apple.com/swift/) [![GitHub license](https://img.shields.io/badge/license-MIT-lightgrey.svg)](https://raw.githubusercontent.com/tsolomko/SWCompression/master/LICENSE) [![Build Status](https://travis-ci.com/tsolomko/SWCompression.svg?branch=develop)](https://travis-ci.com/tsolomko/SWCompression) @@ -10,7 +9,7 @@ A framework with (de)compression algorithms and functions for working with vario ## What is this? -SWCompression — is a framework with a collection of functions for: +SWCompression is a framework with a collection of functions for: 1. Decompression (and sometimes compression) using different algorithms. 2. Reading (and sometimes writing) archives of different formats. @@ -34,7 +33,7 @@ Also, SWCompression is _written with Swift only._ ## Installation -SWCompression can be integrated into your project using Swift Package Manager, CocoaPods or Carthage. +SWCompression can be integrated into your project using Swift Package Manager, CocoaPods, or Carthage. ### Swift Package Manager @@ -47,7 +46,7 @@ let package = Package( name: "PackageName", dependencies: [ .package(url: "https://github.com/tsolomko/SWCompression.git", - from: "4.5.0") + from: "4.6.0") ], targets: [ .target( @@ -62,7 +61,7 @@ More details you can find in [Swift Package Manager's Documentation](https://git ### CocoaPods -Add `pod 'SWCompression', '~> 4.5'` and `use_frameworks!` lines to your Podfile. +Add `pod 'SWCompression', '~> 4.6'` and `use_frameworks!` lines to your Podfile. To complete installation, run `pod install`. @@ -105,19 +104,17 @@ BZip2 and LZMA/LZMA2 support). ### Carthage -__Important:__ Only Swift 5.x is supported when installing SWCompression via Carthage. - -Add to your Cartfile `github "tsolomko/SWCompression" ~> 4.5`. +Add to your Cartfile `github "tsolomko/SWCompression" ~> 4.6`. Then: -1. If you use Xcode 12 or later you should run `carthage update --use-xcframeworks --no-use-binaries`. After that drag -and drop both `SWCompression.xcframework` and `BitByteData.xcframework` files from from the `Carthage/Build/` directory -into the "Frameworks, Libraries, and Embedded Content" section of your target's "General" tab in Xcode. +1. If you use Xcode 12 or later you should run `carthage update --use-xcframeworks`. After that drag and drop both +`SWCompression.xcframework` and `BitByteData.xcframework` files from from the `Carthage/Build/` directory into the +"Frameworks, Libraries, and Embedded Content" section of your target's "General" tab in Xcode. -2. If you use Xcode 11 or earlier you should run `carthage update --no-use-binaries`. After that drag and drop both -`SWCompression.framework` and `BitByteData.framework` files from from the `Carthage/Build//` directory into the -"Embedded Binaries" section of your target's "General" tab in Xcode. +2. If you use Xcode 11 or earlier you should run `carthage update`. After that drag and drop both +`SWCompression.framework` and `BitByteData.framework` files from from the `Carthage/Build//` directory into +the "Embedded Binaries" section of your target's "General" tab in Xcode. For Xcode 12 or later you can currently also use the [xconfig workaround](https://github.com/Carthage/Carthage/blob/master/Documentation/Xcode12Workaround.md). @@ -165,7 +162,8 @@ do { ### Documentation Every function or type of SWCompression's public API is documented. This documentation can be found at its own -[website](http://tsolomko.github.io/SWCompression). +[website](http://tsolomko.github.io/SWCompression) or via a slightly shorter link: +[swcompression.tsolomko.me](http://swcompression.tsolomko.me) ### Sophisticated example @@ -175,7 +173,8 @@ you need to uncomment several lines in "Package.swift" and run `swift build -c r ## Contributing Whether you find a bug, have a suggestion, idea, feedback or something else, please -[create an issue](https://github.com/tsolomko/SWCompression/issues) on GitHub. +[create an issue](https://github.com/tsolomko/SWCompression/issues) on GitHub. If you have any questions, you can ask +them on the [Discussions](https://github.com/tsolomko/SWCompression/discussions) page. In the case of a bug, it will be especially helpful if you attach a file (archive, etc.) that caused the bug to occur. diff --git a/SWCompression.podspec b/SWCompression.podspec index a31fcf18..428d48fe 100644 --- a/SWCompression.podspec +++ b/SWCompression.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "SWCompression" - s.version = "4.5.11" + s.version = "4.6.0" s.summary = "A framework with functions for working with compression, archives and containers." s.description = "A framework with (de)compression algorithms and functions for processing various archives and containers." @@ -22,7 +22,7 @@ Pod::Spec.new do |s| s.swift_versions = ["5"] - s.dependency "BitByteData", "~> 1.4.4" + s.dependency "BitByteData", "~> 2.0" s.subspec "Deflate" do |sp| sp.source_files = "Sources/{Deflate/*,Common/*,Common/CodingTree/*}.swift" diff --git a/SWCompression.xcodeproj/SWCompression.plist b/SWCompression.xcodeproj/SWCompression.plist index 29025807..f6f8a4ef 100644 --- a/SWCompression.xcodeproj/SWCompression.plist +++ b/SWCompression.xcodeproj/SWCompression.plist @@ -15,9 +15,9 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 4.5.11 + 4.6.0 CFBundleVersion - 78 + 80 NSHumanReadableCopyright Copyright © 2021 Timofey Solomko diff --git a/SWCompression.xcodeproj/TestSWCompression.plist b/SWCompression.xcodeproj/TestSWCompression.plist index c6fd15cd..95886263 100644 --- a/SWCompression.xcodeproj/TestSWCompression.plist +++ b/SWCompression.xcodeproj/TestSWCompression.plist @@ -15,8 +15,8 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 4.5.11 + 4.6.0 CFBundleVersion - 78 + 80 diff --git a/SWCompression.xcodeproj/project.pbxproj b/SWCompression.xcodeproj/project.pbxproj index ac5a4d19..bb016374 100644 --- a/SWCompression.xcodeproj/project.pbxproj +++ b/SWCompression.xcodeproj/project.pbxproj @@ -1270,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; @@ -1351,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*]" = ( @@ -1412,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"; @@ -1439,7 +1439,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 78; + DYLIB_CURRENT_VERSION = 80; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = SWCompression.xcodeproj/SWCompression.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; diff --git a/Sources/TAR/Data+Tar.swift b/Sources/TAR/Data+Tar.swift index 489c713d..9c9ed4fb 100644 --- a/Sources/TAR/Data+Tar.swift +++ b/Sources/TAR/Data+Tar.swift @@ -29,6 +29,9 @@ extension Data { } // 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) @@ -50,7 +53,6 @@ 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 { - // TODO: Maybe this should modify self var out = length < self.count ? self.prefix(upTo: length) : self out.append(Data(count: length - out.count)) return out diff --git a/Sources/TAR/TarContainer.swift b/Sources/TAR/TarContainer.swift index 44155655..5c28adce 100644 --- a/Sources/TAR/TarContainer.swift +++ b/Sources/TAR/TarContainer.swift @@ -47,11 +47,10 @@ public class TarContainer: Container { } else if specialEntryType == .longName || specialEntryType == .longLinkName { return .gnu } - case .entryInfo(let info, _): - // TODO: Probably this case (depending on how info.format is set) is already covered by the above. - switch info.format { + case .entryInfo(_, _, let headerFormat): + switch headerFormat { case .pax: - return .pax + fatalError("Unexpected format of basic header: pax") case .gnu: return .gnu case .ustar: @@ -84,7 +83,24 @@ public class TarContainer: Container { return create(from: entries, force: .pax) } - public static func create(from entries: [TarEntry], force format: TarContainer.Format) -> Data { + /** + 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], 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. @@ -94,17 +110,17 @@ public class TarContainer: Container { // 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 (Int.max) is 19 symbols long, which + // 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(Int.max).count < 100 - 19) // "SWC_LocalPaxHeader_".count == 19 + 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 the same name, then crash the program. - var longNameCounter = 0 - var longLinkNameCounter = 0 - var localPaxHeaderCounter = 0 + // 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() for entry in entries { @@ -178,7 +194,7 @@ public class TarContainer: Container { switch result { case .specialEntry: continue parsingLoop - case .entryInfo(let info, let blockStartIndex): + case .entryInfo(let info, let blockStartIndex, _): if info.type == .directory { var entry = TarEntry(info: info, data: nil) entry.info.size = 0 @@ -230,7 +246,7 @@ public class TarContainer: Container { switch result { case .specialEntry: continue parsingLoop - case .entryInfo(let info, _): + case .entryInfo(let info, _, _): entries.append(info) case .truncated: // We don't have an error with a more suitable name. diff --git a/Sources/TAR/TarEntryInfo.swift b/Sources/TAR/TarEntryInfo.swift index 963bb8c3..c298391f 100644 --- a/Sources/TAR/TarEntryInfo.swift +++ b/Sources/TAR/TarEntryInfo.swift @@ -167,8 +167,6 @@ public struct TarEntryInfo: ContainerEntryInfo { */ public var unknownExtendedHeaderRecords: [String: String]? - let format: TarContainer.Format - /** Initializes the entry's info with its name and type. @@ -181,8 +179,6 @@ 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 } init(_ header: TarHeader, _ global: TarExtendedHeader?, _ local: TarExtendedHeader?, @@ -215,16 +211,6 @@ public struct TarEntryInfo: ContainerEntryInfo { self.deviceMajorNumber = header.deviceMajorNumber self.deviceMinorNumber = header.deviceMinorNumber - if local != nil || global != nil { - self.format = .pax - } else if header.format == .gnu || longName != nil || longLinkName != nil { - self.format = .gnu - } else if header.format == .ustar { - self.format = .ustar - } else { - self.format = .prePosix - } - // Set `name` and `linkName` to values from PAX or GNU format if possible. var name = header.name if let prefix = header.prefix, prefix != "" { diff --git a/Sources/TAR/TarHeader.swift b/Sources/TAR/TarHeader.swift index 9d151092..b0c9ea2f 100644 --- a/Sources/TAR/TarHeader.swift +++ b/Sources/TAR/TarHeader.swift @@ -157,7 +157,7 @@ struct TarHeader { init(_ info: TarEntryInfo) { self.name = info.name self.type = .normal(info.type) - self.size = info.size ?? 0 // TODO: tarInt(...) may not work as expected for 0 instead of nil. + self.size = info.size ?? 0 self.atime = info.accessTime self.ctime = info.creationTime self.mtime = info.modificationTime @@ -169,7 +169,7 @@ struct TarHeader { self.deviceMajorNumber = info.deviceMajorNumber self.deviceMinorNumber = info.deviceMinorNumber self.linkName = info.linkName - self.format = .pax // TODO: If TarEntryInfo.format is not removed than this should be `info.format`. + self.format = .pax // Unused if header was created using this initializer. self.blockStartIndex = -1 } @@ -237,7 +237,6 @@ struct TarHeader { // ustar and pax formats contain prefix field. if format == .ustar || format == .pax { // Splitting the name property into the name and prefix fields. - // TODO: Review let nameData = Data(self.name.utf8) if nameData.count > 100 { var maxPrefixLength = nameData.count @@ -252,6 +251,8 @@ struct TarHeader { // 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 diff --git a/Sources/TAR/TarParser.swift b/Sources/TAR/TarParser.swift index 5879152b..bd4277d7 100644 --- a/Sources/TAR/TarParser.swift +++ b/Sources/TAR/TarParser.swift @@ -15,7 +15,7 @@ struct TarParser { case eofMarker case finished case specialEntry(TarHeader.SpecialEntryType) - case entryInfo(TarEntryInfo, Int) + case entryInfo(TarEntryInfo, Int, TarContainer.Format) } private let reader: LittleEndianByteReader @@ -70,7 +70,7 @@ struct TarParser { lastLocalExtendedHeader = nil longName = nil longLinkName = nil - return .entryInfo(info, header.blockStartIndex) + return .entryInfo(info, header.blockStartIndex, header.format) } } diff --git a/Sources/swcomp/main.swift b/Sources/swcomp/main.swift index 1eed34a8..4b329857 100644 --- a/Sources/swcomp/main.swift +++ b/Sources/swcomp/main.swift @@ -7,7 +7,7 @@ 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 - a small command-line client for SWCompression framework. Serves as an example of SWCompression usage. diff --git a/Tests/TarCreateTests.swift b/Tests/TarCreateTests.swift index 84dc301c..7cb3114e 100644 --- a/Tests/TarCreateTests.swift +++ b/Tests/TarCreateTests.swift @@ -219,27 +219,29 @@ 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 = TarContainer.create(from: [TarEntry(info: info, data: Data())]) - XCTAssertEqual(try TarContainer.formatOf(container: containerData), .pax) - let newInfo = try TarContainer.open(container: containerData)[0].info + 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) + 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 {