diff --git a/.gitignore b/.gitignore index db6de344..edd94d5c 100644 --- a/.gitignore +++ b/.gitignore @@ -78,3 +78,6 @@ docs/ # Vscode launch.json generated by Swift extension .vscode/launch.json + +# API baselines generate by swift package diagnose-api-breaking-changes +api_baseline/ diff --git a/.jazzy.yaml b/.jazzy.yaml index ead4a9bd..f556b310 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.8.2 +module_version: 4.8.3 copyright: '© 2022 Timofey Solomko' readme: README.md github_url: https://github.com/tsolomko/SWCompression -github_file_prefix: https://github.com/tsolomko/SWCompression/tree/4.8.2 +github_file_prefix: https://github.com/tsolomko/SWCompression/tree/4.8.3 theme: fullwidth custom_categories: diff --git a/CHANGELOG.md b/CHANGELOG.md index 015b1914..1245227a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +## 4.8.3 + +- There are now minimum deployment targets specified in Swift Package Manager manifest. +- The properties of `GzipHeader` are now `var`-properties (instead of `let`). +- GZip extra fields are now supported. + - Added `GzipHeader.ExtraField` struct. + - Added `GzipHeader.extraFields` property. + - Added a new `extraFields` argument to `GzipArchive.archive` function (with a default array empty value). +- Fixed potential crashes that could occur when processing GZip archives truncated in a header or a "footer". +- Some non-well-formed values of PAX extended header records no longer cause `TarError.wrongPaxHeaderEntry` to be thrown. + - The record values with newline characters are now fully processed. + - The record values that do not contain UTF-8 strings are now ignored. +- swcomp changes: + - The symbolic links are now extracted with the values recorded in the containers. + - The hard links are now extracted from TAR containers instead of being ignored. + - Fixed build issues on Linux and Windows. + - `benchmark` is now a command group with two commands, `run` and `show`. + - Added `-a`, `--append` option to the `benchmark run` command. + - Added `-d`, `--description` option to the `benchmark run` command. + - Added `-t`, `--preserve-timestamp` option to the `benchmark run` command. + - The file format of saved results is now more flexible and allows multi-way comparisons. + - Improved precision of time measurements in benchmarks. + ## 4.8.2 - Swift 5.1 is no longer supported. diff --git a/Package.swift b/Package.swift index a01cf3b1..c34e170e 100644 --- a/Package.swift +++ b/Package.swift @@ -3,6 +3,12 @@ import PackageDescription let package = Package( name: "SWCompression", + platforms: [ + .macOS(.v10_13), + .iOS(.v11), + .tvOS(.v11), + .watchOS(.v4) + ], products: [ .library( name: "SWCompression", diff --git a/README.md b/README.md index dd8debeb..fe37d627 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,8 @@ Every function or type of SWCompression's public API is documented. This documen There is a small command-line program, "swcomp", which is included in this repository in "Sources/swcomp". It can be built using Swift Package Manager. +__IMPORTANT:__ The "swcomp" command-line tool is NOT intended for general use. + ## Contributing Whether you find a bug, have a suggestion, idea, feedback or something else, please diff --git a/SWCompression.podspec b/SWCompression.podspec index e99e1f00..351a2daf 100644 --- a/SWCompression.podspec +++ b/SWCompression.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "SWCompression" - s.version = "4.8.2" + s.version = "4.8.3" 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." diff --git a/SWCompression.xcodeproj/SWCompression.plist b/SWCompression.xcodeproj/SWCompression.plist index f0328189..327e7b10 100644 --- a/SWCompression.xcodeproj/SWCompression.plist +++ b/SWCompression.xcodeproj/SWCompression.plist @@ -15,9 +15,9 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 4.8.2 + 4.8.3 CFBundleVersion - 87 + 88 NSHumanReadableCopyright Copyright © 2022 Timofey Solomko diff --git a/SWCompression.xcodeproj/TestSWCompression.plist b/SWCompression.xcodeproj/TestSWCompression.plist index 921e4d3e..f40b8805 100644 --- a/SWCompression.xcodeproj/TestSWCompression.plist +++ b/SWCompression.xcodeproj/TestSWCompression.plist @@ -15,8 +15,8 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 4.8.2 + 4.8.3 CFBundleVersion - 87 + 88 diff --git a/SWCompression.xcodeproj/project.pbxproj b/SWCompression.xcodeproj/project.pbxproj index befe7713..b83aea5c 100644 --- a/SWCompression.xcodeproj/project.pbxproj +++ b/SWCompression.xcodeproj/project.pbxproj @@ -213,6 +213,8 @@ 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 */; }; + E6023B1B28F9B60000D6F3DC /* test4_extra_field.gz in Resources */ = {isa = PBXBuildFile; fileRef = E6023B1A28F9B60000D6F3DC /* test4_extra_field.gz */; }; + E6023B1D28F9C92200D6F3DC /* GzipHeader+ExtraField.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6023B1C28F9C92200D6F3DC /* GzipHeader+ExtraField.swift */; }; E604F3C22700C75F004BD38A /* test_dict_B5.lz4 in Resources */ = {isa = PBXBuildFile; fileRef = E604F3BF2700C75F004BD38A /* test_dict_B5.lz4 */; }; E604F3C32700C75F004BD38A /* test_dict_B5_BD.lz4 in Resources */ = {isa = PBXBuildFile; fileRef = E604F3C02700C75F004BD38A /* test_dict_B5_BD.lz4 */; }; E604F3C42700C75F004BD38A /* lz4_dict in Resources */ = {isa = PBXBuildFile; fileRef = E604F3C12700C75F004BD38A /* lz4_dict */; }; @@ -266,6 +268,7 @@ E6791E1826FD001A003852A9 /* DataError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6791E1726FD001A003852A9 /* DataError.swift */; }; E6791E1B26FD0094003852A9 /* LZ4.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6791E1A26FD0094003852A9 /* LZ4.swift */; }; E6791E3326FD05EC003852A9 /* LZ4Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6791E3226FD05EC003852A9 /* LZ4Tests.swift */; }; + E68DA766290420BC00259CB4 /* test_pax_record_newline.tar in Resources */ = {isa = PBXBuildFile; fileRef = E68DA765290420BC00259CB4 /* test_pax_record_newline.tar */; }; E694694327480EA6009C897A /* TarReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = E694694227480EA6009C897A /* TarReader.swift */; }; E6974C5B2701AC2600E06C60 /* test_dict_B5_dictID.lz4 in Resources */ = {isa = PBXBuildFile; fileRef = E6974C5A2701AC2600E06C60 /* test_dict_B5_dictID.lz4 */; }; E69FAC922729ACD900D3C406 /* test_dos_latin_us.zip in Resources */ = {isa = PBXBuildFile; fileRef = E69FAC912729ACD900D3C406 /* test_dos_latin_us.zip */; }; @@ -497,6 +500,8 @@ 06F276DE1F2BAB4900E67335 /* 7zEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 7zEntry.swift; sourceTree = ""; }; 06FEAD911F54B9CD00AD016E /* EncodingTree.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = EncodingTree.swift; path = Sources/Common/CodingTree/EncodingTree.swift; sourceTree = SOURCE_ROOT; }; 06FED40B1DD7717E0013DFB2 /* BZip2.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BZip2.swift; sourceTree = ""; }; + E6023B1A28F9B60000D6F3DC /* test4_extra_field.gz */ = {isa = PBXFileReference; lastKnownFileType = archive.gzip; path = test4_extra_field.gz; sourceTree = ""; }; + E6023B1C28F9C92200D6F3DC /* GzipHeader+ExtraField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GzipHeader+ExtraField.swift"; sourceTree = ""; }; E604F3BF2700C75F004BD38A /* test_dict_B5.lz4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = test_dict_B5.lz4; sourceTree = ""; }; E604F3C02700C75F004BD38A /* test_dict_B5_BD.lz4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = test_dict_B5_BD.lz4; sourceTree = ""; }; E604F3C12700C75F004BD38A /* lz4_dict */ = {isa = PBXFileReference; lastKnownFileType = file; path = lz4_dict; sourceTree = ""; }; @@ -551,6 +556,7 @@ E6791E1726FD001A003852A9 /* DataError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataError.swift; sourceTree = ""; }; E6791E1A26FD0094003852A9 /* LZ4.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LZ4.swift; sourceTree = ""; }; E6791E3226FD05EC003852A9 /* LZ4Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LZ4Tests.swift; sourceTree = ""; }; + E68DA765290420BC00259CB4 /* test_pax_record_newline.tar */ = {isa = PBXFileReference; lastKnownFileType = archive.tar; path = test_pax_record_newline.tar; sourceTree = ""; }; E694694227480EA6009C897A /* TarReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TarReader.swift; sourceTree = ""; }; E6974C5A2701AC2600E06C60 /* test_dict_B5_dictID.lz4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = test_dict_B5_dictID.lz4; sourceTree = ""; }; E69FAC912729ACD900D3C406 /* test_dos_latin_us.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = test_dos_latin_us.zip; sourceTree = ""; }; @@ -714,6 +720,7 @@ children = ( 063364E21DC52979007E313F /* GzipArchive.swift */, 061C062A1F0E8A1D00832F0C /* GzipHeader.swift */, + E6023B1C28F9C92200D6F3DC /* GzipHeader+ExtraField.swift */, 061C06251F0E8A0300832F0C /* GzipError.swift */, 0686A63F1FA4C18800E89C9E /* FileSystemType+Gzip.swift */, ); @@ -875,6 +882,7 @@ 06F065BF1FFB763300312A82 /* test2.gz */, 06F065BC1FFB763300312A82 /* test3.gz */, 06F065BB1FFB763300312A82 /* test4.gz */, + E6023B1A28F9B60000D6F3DC /* test4_extra_field.gz */, 06F065B91FFB763300312A82 /* test5.gz */, 06F065BE1FFB763300312A82 /* test6.gz */, 06F065BD1FFB763300312A82 /* test7.gz */, @@ -953,6 +961,7 @@ 0698B10A2104E11200A7C551 /* test_gnu_inc_format.tar */, 0698B10C2106136500A7C551 /* test_big_num_field.tar */, 0698B10E2106344200A7C551 /* test_negative_mtime.tar */, + E68DA765290420BC00259CB4 /* test_pax_record_newline.tar */, ); path = TAR; sourceTree = ""; @@ -1213,6 +1222,7 @@ E652FEE027009BDD006BC312 /* test5_legacy.lz4 in Resources */, 06F066721FFB763400312A82 /* test7.bz2 in Resources */, 06F0665B1FFB763400312A82 /* test10.lzma in Resources */, + E68DA766290420BC00259CB4 /* test_pax_record_newline.tar in Resources */, E652FEFE2700A028006BC312 /* test_B7_BD.lz4 in Resources */, 06F0663C1FFB763400312A82 /* test3.xz in Resources */, 06F066591FFB763400312A82 /* test_win.tar in Resources */, @@ -1278,6 +1288,7 @@ E652FED6270091B6006BC312 /* test_skippable_frame.lz4 in Resources */, 06F0665A1FFB763400312A82 /* test11.lzma in Resources */, E652FEE427009BDD006BC312 /* test3_legacy.lz4 in Resources */, + E6023B1B28F9B60000D6F3DC /* test4_extra_field.gz in Resources */, 06F066401FFB763400312A82 /* test_zip_bzip2.zip in Resources */, 06F066461FFB763400312A82 /* test_empty_file.zip in Resources */, 06F066331FFB763400312A82 /* random_file.zlib in Resources */, @@ -1406,6 +1417,7 @@ 06A9606A1F1E7E0D0078E6D1 /* 7zSubstreamInfo.swift in Sources */, E6C4150726FE230A00F9D36F /* XxHash32.swift in Sources */, 0694A74D1F7C0DF00023BC99 /* BurrowsWheeler.swift in Sources */, + E6023B1D28F9C92200D6F3DC /* GzipHeader+ExtraField.swift in Sources */, 06CC3FDD1F8AAE8B00BD576D /* MsbBitReader+7z.swift in Sources */, 06A3933B1DE0709300182E12 /* Deflate.swift in Sources */, 06092A1A1FA4CB0300DE9FD5 /* CompressionMethod.swift in Sources */, @@ -1516,7 +1528,7 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CURRENT_PROJECT_VERSION = 87; + CURRENT_PROJECT_VERSION = 88; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; EAGER_LINKING = YES; @@ -1601,7 +1613,7 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CURRENT_PROJECT_VERSION = 87; + CURRENT_PROJECT_VERSION = 88; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; EAGER_LINKING = YES; @@ -1666,7 +1678,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 87; + DYLIB_CURRENT_VERSION = 88; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = SWCompression.xcodeproj/SWCompression.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -1693,7 +1705,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 87; + DYLIB_CURRENT_VERSION = 88; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = SWCompression.xcodeproj/SWCompression.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; diff --git a/Sources/GZip/GzipArchive.swift b/Sources/GZip/GzipArchive.swift index 0cafe099..af6c2403 100644 --- a/Sources/GZip/GzipArchive.swift +++ b/Sources/GZip/GzipArchive.swift @@ -78,8 +78,9 @@ public class GzipArchive: Archive { private static func processMember(_ bitReader: LsbBitReader) throws -> Member { // Valid GZip archive must contain at least 20 bytes of data (10 for the header, 2 for an empty Deflate block, - // and 8 for checksums). - guard bitReader.bitsLeft >= 20 * 8 + // and 8 for checksums). In addition, since GZip format is "byte-oriented" we should ensure that members are + // byte-aligned. + guard bitReader.isAligned && bitReader.bytesLeft >= 20 else { throw GzipError.wrongMagic } let header = try GzipHeader(bitReader) @@ -87,6 +88,8 @@ public class GzipArchive: Archive { let memberData = try Deflate.decompress(bitReader) bitReader.align() + guard bitReader.bytesLeft >= 8 + else { throw GzipError.wrongMagic } let crc32 = bitReader.uint32() let isize = bitReader.uint64(fromBytes: 4) guard UInt64(truncatingIfNeeded: memberData.count) % (UInt64(truncatingIfNeeded: 1) << 32) == isize @@ -110,14 +113,20 @@ public class GzipArchive: Archive { - Parameter isTextFile: Set to true, if the file which will be archived is text file or ASCII-file. - Parameter osType: Type of the system on which this archive will be created. - Parameter modificationTime: Last time the file was modified. + - Parameter extraFields: Any extra fields. Note that no extra field is allowed to have second byte of the extra + field (subfield) ID equal to zero. In addition, the length of a field's binary content must be less than + `UInt16.max`, while the total sum of the binary content length of all extra fields plus 4 for each field must also + not exceed `UInt16.max`. See GZip format specification for more details. - - Throws: `GzipError.cannotEncodeISOLatin1` if file name of comment cannot be encoded with ISO-Latin-1 encoding. + - Throws: `GzipError.cannotEncodeISOLatin1` if a file name or a comment cannot be encoded with ISO-Latin-1 encoding + or if the total sum of the binary content length of all extra fields plus 4 for each field exceeds `UInt16.max`. - Returns: Resulting archive's data. */ public static func archive(data: Data, comment: String? = nil, fileName: String? = nil, writeHeaderCRC: Bool = false, isTextFile: Bool = false, - osType: FileSystemType? = nil, modificationTime: Date? = nil) throws -> Data { + osType: FileSystemType? = nil, modificationTime: Date? = nil, + extraFields: [GzipHeader.ExtraField] = []) throws -> Data { var flags: UInt8 = 0 var commentData = Data() @@ -146,6 +155,10 @@ public class GzipArchive: Archive { } } + if !extraFields.isEmpty { + flags |= 1 << 2 + } + if writeHeaderCRC { flags |= 1 << 1 } @@ -167,7 +180,7 @@ public class GzipArchive: Archive { var headerBytes: [UInt8] = [ 0x1f, 0x8b, // 'magic' bytes. 8, // Compression method (DEFLATE). - flags // Flags; currently no flags are set. + flags ] for i in 0..<4 { headerBytes.append(mtimeBytes[i]) @@ -175,6 +188,27 @@ public class GzipArchive: Archive { headerBytes.append(2) // Extra flags; 2 means that DEFLATE used slowest algorithm. headerBytes.append(os) + if !extraFields.isEmpty { + let xlen = extraFields.reduce(0) { $0 + 4 + $1.bytes.count } + guard xlen <= UInt16.max + else { throw GzipError.cannotEncodeISOLatin1 } + headerBytes.append((xlen & 0xFF).toUInt8()) + headerBytes.append(((xlen >> 8) & 0xFF).toUInt8()) + + for extraField in extraFields { + headerBytes.append(extraField.si1) + headerBytes.append(extraField.si2) + + let len = extraField.bytes.count + headerBytes.append((len & 0xFF).toUInt8()) + headerBytes.append(((len >> 8) & 0xFF).toUInt8()) + + for byte in extraField.bytes { + headerBytes.append(byte) + } + } + } + var outData = Data(headerBytes) outData.append(fileNameData) diff --git a/Sources/GZip/GzipHeader+ExtraField.swift b/Sources/GZip/GzipHeader+ExtraField.swift new file mode 100644 index 00000000..f8ba847f --- /dev/null +++ b/Sources/GZip/GzipHeader+ExtraField.swift @@ -0,0 +1,29 @@ +// Copyright (c) 2022 Timofey Solomko +// Licensed under MIT License +// +// See LICENSE for license information + +extension GzipHeader { + + /// Represents an extra field in the header of a GZip archive. + public struct ExtraField { + + /// First byte of the extra field (subfield) ID. + public let si1: UInt8 + + /// Second byte of the extra field (subfield) ID. + public let si2: UInt8 + + /// Binary content of the extra field. + public var bytes: [UInt8] + + /// Initializes and extra field with the specified extra field (subfield) ID bytes and its binary content. + public init(_ si1: UInt8, _ si2: UInt8, _ bytes: [UInt8]) { + self.si1 = si1 + self.si2 = si2 + self.bytes = bytes + } + + } + +} diff --git a/Sources/GZip/GzipHeader.swift b/Sources/GZip/GzipHeader.swift index c5869288..b9a7c386 100644 --- a/Sources/GZip/GzipHeader.swift +++ b/Sources/GZip/GzipHeader.swift @@ -24,25 +24,33 @@ public struct GzipHeader { } /// Compression method of archive. Always `.deflate` for GZip archives. - public let compressionMethod: CompressionMethod + public var compressionMethod: CompressionMethod /** The most recent modification time of the original file. If corresponding archive's field is set to 0, which means that no time was specified, then this property is `nil`. */ - public let modificationTime: Date? + public var modificationTime: Date? /// Type of file system on which archivation took place. - public let osType: FileSystemType + public var osType: FileSystemType /// Name of the original file. If archive doesn't contain file's name, then `nil`. - public let fileName: String? + public var fileName: String? /// Comment stored in archive. If archive doesn't contain any comment, then `nil`. - public let comment: String? + public var comment: String? /// True, if file is likely to be text file or ASCII-file. - public let isTextFile: Bool + public var isTextFile: Bool + + /** + Extra fields present in the header. + + - Note: This feature of the GZip format is extremely rarely used, so in vast majority of cases this property + contains an empty array. + */ + public var extraFields: [ExtraField] /** Initializes the structure with the values from the first 'member' of GZip `archive`. @@ -64,12 +72,14 @@ public struct GzipHeader { // First two bytes should be correct 'magic' bytes let magic = reader.uint16() - guard magic == 0x8b1f else { throw GzipError.wrongMagic } + 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 = reader.byte() - guard method == 8 else { throw GzipError.wrongCompressionMethod } + guard method == 8 + else { throw GzipError.wrongCompressionMethod } headerBytes.append(method) self.compressionMethod = .deflate @@ -96,23 +106,62 @@ public struct GzipHeader { self.isTextFile = flags.contains(.ftext) - // Some archives may contain extra fields + // Some archives may contain extra fields. + self.extraFields = [ExtraField]() if flags.contains(.fextra) { + guard reader.bytesLeft >= 2 + else { throw GzipError.wrongMagic } var xlen = 0 for i in 0..<2 { let byte = reader.byte() xlen |= byte.toInt() << (8 * i) headerBytes.append(byte) } - for _ in 0.. 0 { + // There must be least four bytes of extra fields for SI1, SI2, and 2 bytes of the length parameter + // filled with zeros (minimal variant). + guard reader.bytesLeft >= xlen && xlen >= 4 + else { throw GzipError.wrongMagic } + + let si1 = reader.byte() + headerBytes.append(si1) + + let si2 = reader.byte() + // IDs with zero in the second byte are reserved. + guard si2 != 0 + else { throw GzipError.wrongFlags } + headerBytes.append(si2) + + var len = 0 + for i in 0..<2 { + let byte = reader.byte() + len |= byte.toInt() << (8 * i) + headerBytes.append(byte) + } + xlen -= 4 + + // Total remaining extra fields length must be larger than the length of the binary content of the + // current extra field. + guard xlen >= len + else { throw GzipError.wrongMagic } + var extraFieldBytes = [UInt8]() + for _ in 0..= 2 + else { throw GzipError.wrongMagic } let crc16 = reader.uint16() - guard CheckSums.crc32(headerBytes) & 0xFFFF == crc16 else { throw GzipError.wrongHeaderCRC } + guard CheckSums.crc32(headerBytes) & 0xFFFF == crc16 + else { throw GzipError.wrongHeaderCRC } } } diff --git a/Sources/TAR/TarEntryInfo.swift b/Sources/TAR/TarEntryInfo.swift index 9f8de0a5..82923d81 100644 --- a/Sources/TAR/TarEntryInfo.swift +++ b/Sources/TAR/TarEntryInfo.swift @@ -183,14 +183,6 @@ public struct TarEntryInfo: ContainerEntryInfo { 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. - // 3. `tarInt` returning `nil` corresponds to either field being unused and filled with NULLs or non-UTF-8 - // string describing number which means that either this field or container in general is corrupted. - // 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. - self.permissions = header.permissions self.ownerID = (local?.uid ?? global?.uid) ?? header.uid self.groupID = (local?.gid ?? global?.gid) ?? header.gid diff --git a/Sources/TAR/TarExtendedHeader.swift b/Sources/TAR/TarExtendedHeader.swift index b11da288..52ab7f54 100644 --- a/Sources/TAR/TarExtendedHeader.swift +++ b/Sources/TAR/TarExtendedHeader.swift @@ -29,27 +29,40 @@ struct TarExtendedHeader { var comment: String? init(_ data: Data) throws { - // Split header data into entries with "\n" (0x0A) character as a separator. - let entriesData = data.split(separator: 0x0A) - var unknownRecords = [String: String]() + var i = data.startIndex + while i < data.endIndex { + let lengthStartIndex = i + while data[i] != 0x20 { + i += 1 + } + guard let lengthString = String(data: data[lengthStartIndex..= 1 - else { swcompExit(.benchmarkSmallIterCount) } - - let title = "\(self.selectedBenchmark.titleName) Benchmark\n" - print(String(repeating: "=", count: title.count)) - print(title) - - var results = [BenchmarkResult]() - var otherResults: [BenchmarkResult]? = nil - if let comparePath = comparePath { - let data = try Data(contentsOf: URL(fileURLWithPath: comparePath)) - let decoder = JSONDecoder() - otherResults = try decoder.decode(Array.self, from: data) - } - - for input in self.inputs { - print("Input: \(input)") - let benchmark = self.selectedBenchmark.initialized(input) - let iterationCount = self.iterationCount ?? benchmark.defaultIterationCount - - if !self.noWarmup { - print("Warmup iteration...") - // Zeroth (excluded) iteration. - benchmark.warmupIteration() - } - - var sum = 0.0 - var squareSum = 0.0 - - print("Iterations: ", terminator: "") - #if !os(Linux) - fflush(__stdoutp) - #endif - for i in 1...iterationCount { - if i > 1 { - print(", ", terminator: "") - } - let speed = benchmark.measure() - print(benchmark.format(speed), terminator: "") - #if !os(Linux) - fflush(__stdoutp) - #endif - sum += speed - squareSum += speed * speed - } - - let avgSpeed = sum / Double(iterationCount) - print("\nAverage: " + benchmark.format(avgSpeed)) - let std = sqrt(squareSum / Double(iterationCount) - sum * sum / Double(iterationCount * iterationCount)) - print("Standard deviation: " + benchmark.format(std)) - - let result = BenchmarkResult(name: self.selectedBenchmark.rawValue, input: input, iterCount: iterationCount, - avg: avgSpeed, std: std) - if let other = otherResults?.first(where: { $0.name == result.name && $0.input == result.input }) { - let comparison = result.compare(with: other) - let diff = (result.avg / other.avg - 1) * 100 - if diff < 0 { - switch comparison { - case 1: - print(String(format: "OK %f%% (p-value > 0.05)", diff)) - case nil: - print("Cannot compare due to unsupported iteration count.") - case -1: - print(String(format: "REG %f%% (p-value < 0.05)", diff)) - case 0: - print(String(format: "REG %f%% (p-value = 0.05)", diff)) - default: - swcompExit(.benchmarkUnknownCompResult) - } - } - else if diff > 0 { - switch comparison { - case 1: - print(String(format: "OK %f%% (p-value > 0.05)", diff)) - case nil: - print("Cannot compare due to unsupported iteration count.") - case -1: - print(String(format: "IMP %f%% (p-value < 0.05)", diff)) - case 0: - print(String(format: "IMP %f%% (p-value = 0.05)", diff)) - default: - swcompExit(.benchmarkUnknownCompResult) - } - } else { - print("OK (exact match of averages)") - } - } - results.append(result) - - print() - } - - if let savePath = self.savePath { - let encoder = JSONEncoder() - encoder.outputFormatting = .prettyPrinted - - let data = try encoder.encode(results) - try data.write(to: URL(fileURLWithPath: savePath)) - } - } - -} diff --git a/Sources/swcomp/Benchmarks/BenchmarkGroup.swift b/Sources/swcomp/Benchmarks/BenchmarkGroup.swift new file mode 100644 index 00000000..3bd1e587 --- /dev/null +++ b/Sources/swcomp/Benchmarks/BenchmarkGroup.swift @@ -0,0 +1,20 @@ +// Copyright (c) 2022 Timofey Solomko +// Licensed under MIT License +// +// See LICENSE for license information + +import Foundation +import SWCompression +import SwiftCLI + +final class BenchmarkGroup: CommandGroup { + + let name = "benchmark" + let shortDescription = "Benchmark-related commands" + + let children: [Routable] = [ + RunBenchmarkCommand(), + ShowBenchmarkCommand() + ] + +} diff --git a/Sources/swcomp/Benchmarks/BenchmarkMetadata.swift b/Sources/swcomp/Benchmarks/BenchmarkMetadata.swift new file mode 100644 index 00000000..e0302c0e --- /dev/null +++ b/Sources/swcomp/Benchmarks/BenchmarkMetadata.swift @@ -0,0 +1,83 @@ +// Copyright (c) 2022 Timofey Solomko +// Licensed under MIT License +// +// See LICENSE for license information + +import Foundation + +struct BenchmarkMetadata: Codable, Equatable { + + var timestamp: TimeInterval? + var osInfo: String + var swiftVersion: String + var swcVersion: String + var description: String? + + private static func run(command: URL, arguments: [String] = []) throws -> String { + let task = Process() + let pipe = Pipe() + + task.standardOutput = pipe + task.standardError = pipe + task.executableURL = command + task.arguments = arguments + task.standardInput = nil + + try task.run() + task.waitUntilExit() + let data = pipe.fileHandleForReading.readDataToEndOfFile() + let output = String(data: data, encoding: .utf8)! + return output + } + + private static func getExecURL(for command: String) throws -> URL { + let args = ["-c", "which \(command)"] + #if os(Windows) + swcompExit(.benchmarkCannotGetSubcommandPathWindows) + #else + let output = try BenchmarkMetadata.run(command: URL(fileURLWithPath: "/bin/sh"), arguments: args) + #endif + return URL(fileURLWithPath: String(output.dropLast())) + } + + private static func getOsInfo() throws -> String { + #if os(Linux) + return try BenchmarkMetadata.run(command: BenchmarkMetadata.getExecURL(for: "uname"), arguments: ["-a"]) + #else + #if os(Windows) + return "Unknown Windows OS" + #else + return try BenchmarkMetadata.run(command: BenchmarkMetadata.getExecURL(for: "sw_vers")) + #endif + #endif + } + + init(_ description: String?, _ preserveTimestamp: Bool) throws { + self.timestamp = preserveTimestamp ? Date.timeIntervalSinceReferenceDate : nil + self.osInfo = try BenchmarkMetadata.getOsInfo() + #if os(Windows) + self.swiftVersion = "Unknown Swift version on Windows" + #else + self.swiftVersion = try BenchmarkMetadata.run(command: BenchmarkMetadata.getExecURL(for: "swift"), + arguments: ["-version"]) + #endif + self.swcVersion = _SWC_VERSION + self.description = description + } + + func print() { + Swift.print("OS Info: \(self.osInfo)", terminator: "") + Swift.print("Swift version: \(self.swiftVersion)", terminator: "") + Swift.print("SWC version: \(self.swcVersion)") + if let timestamp = self.timestamp { + Swift.print("Timestamp: " + + DateFormatter.localizedString(from: Date(timeIntervalSinceReferenceDate: timestamp), + dateStyle: .short, timeStyle: .short)) + } + if let description = self.description { + Swift.print("Description: \(description)") + } + Swift.print() + } + +} diff --git a/Sources/swcomp/Benchmarks/BenchmarkResult.swift b/Sources/swcomp/Benchmarks/BenchmarkResult.swift index 2a5eb46e..f3a9fe5d 100644 --- a/Sources/swcomp/Benchmarks/BenchmarkResult.swift +++ b/Sources/swcomp/Benchmarks/BenchmarkResult.swift @@ -13,7 +13,46 @@ struct BenchmarkResult: Codable { var avg: Double var std: Double - func compare(with other: BenchmarkResult) -> Int? { + var id: String { + return [self.name, self.input, String(self.iterCount)].joined(separator: "<#>") + } + + func printComparison(with other: BenchmarkResult) { + let diff = (self.avg / other.avg - 1) * 100 + let comparison = self.compare(with: other) + if diff < 0 { + switch comparison { + case 1: + print(String(format: "OK %f%% (p-value > 0.05)", diff)) + case nil: + print("Cannot compare due to unsupported iteration count.") + case -1: + print(String(format: "REG %f%% (p-value < 0.05)", diff)) + case 0: + print(String(format: "REG %f%% (p-value = 0.05)", diff)) + default: + swcompExit(.benchmarkUnknownCompResult) + } + } + else if diff > 0 { + switch comparison { + case 1: + print(String(format: "OK %f%% (p-value > 0.05)", diff)) + case nil: + print("Cannot compare due to unsupported iteration count.") + case -1: + print(String(format: "IMP %f%% (p-value < 0.05)", diff)) + case 0: + print(String(format: "IMP %f%% (p-value = 0.05)", diff)) + default: + swcompExit(.benchmarkUnknownCompResult) + } + } else { + print("OK (exact match of averages)") + } + } + + private func compare(with other: BenchmarkResult) -> Int? { let degreesOfFreedom = Double(self.iterCount + other.iterCount - 2) let t1: Double = Double(self.iterCount - 1) * pow(self.std, 2) let t2: Double = Double(other.iterCount - 1) * pow(other.std, 2) diff --git a/Sources/swcomp/Benchmarks/Benchmarks.swift b/Sources/swcomp/Benchmarks/Benchmarks.swift index 87a56d98..9fdecf05 100644 --- a/Sources/swcomp/Benchmarks/Benchmarks.swift +++ b/Sources/swcomp/Benchmarks/Benchmarks.swift @@ -135,9 +135,9 @@ struct UnGzip: Benchmark { func measure() -> Double { do { - let startTime = CFAbsoluteTimeGetCurrent() + let startTime = DispatchTime.now().uptimeNanoseconds _ = try GzipArchive.unarchive(archive: self.data) - let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime + let timeElapsed = Double(DispatchTime.now().uptimeNanoseconds - startTime) / 1_000_000_000 return self.size / timeElapsed } catch let error { swcompExit(.benchmarkCannotMeasure(Self.self, error)) @@ -163,9 +163,9 @@ struct UnBz2: Benchmark { func measure() -> Double { do { - let startTime = CFAbsoluteTimeGetCurrent() + let startTime = DispatchTime.now().uptimeNanoseconds _ = try BZip2.decompress(data: self.data) - let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime + let timeElapsed = Double(DispatchTime.now().uptimeNanoseconds - startTime) / 1_000_000_000 return self.size / timeElapsed } catch let error { swcompExit(.benchmarkCannotMeasure(Self.self, error)) @@ -191,9 +191,9 @@ struct UnLz4: Benchmark { func measure() -> Double { do { - let startTime = CFAbsoluteTimeGetCurrent() + let startTime = DispatchTime.now().uptimeNanoseconds _ = try LZ4.decompress(data: self.data) - let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime + let timeElapsed = Double(DispatchTime.now().uptimeNanoseconds - startTime) / 1_000_000_000 return self.size / timeElapsed } catch let error { swcompExit(.benchmarkCannotMeasure(Self.self, error)) @@ -219,9 +219,9 @@ struct UnXz: Benchmark { func measure() -> Double { do { - let startTime = CFAbsoluteTimeGetCurrent() + let startTime = DispatchTime.now().uptimeNanoseconds _ = try XZArchive.unarchive(archive: self.data) - let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime + let timeElapsed = Double(DispatchTime.now().uptimeNanoseconds - startTime) / 1_000_000_000 return self.size / timeElapsed } catch let error { swcompExit(.benchmarkCannotMeasure(Self.self, error)) @@ -246,9 +246,9 @@ struct CompDeflate: Benchmark { } func measure() -> Double { - let startTime = CFAbsoluteTimeGetCurrent() + let startTime = DispatchTime.now().uptimeNanoseconds _ = Deflate.compress(data: self.data) - let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime + let timeElapsed = Double(DispatchTime.now().uptimeNanoseconds - startTime) / 1_000_000_000 return self.size / timeElapsed } @@ -301,9 +301,9 @@ struct CompBz2: Benchmark { } func measure() -> Double { - let startTime = CFAbsoluteTimeGetCurrent() + let startTime = DispatchTime.now().uptimeNanoseconds _ = BZip2.compress(data: self.data) - let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime + let timeElapsed = Double(DispatchTime.now().uptimeNanoseconds - startTime) / 1_000_000_000 return self.size / timeElapsed } @@ -356,9 +356,9 @@ struct CompLz4: Benchmark { } func measure() -> Double { - let startTime = CFAbsoluteTimeGetCurrent() + let startTime = DispatchTime.now().uptimeNanoseconds _ = LZ4.compress(data: self.data) - let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime + let timeElapsed = Double(DispatchTime.now().uptimeNanoseconds - startTime) / 1_000_000_000 return self.size / timeElapsed } @@ -411,10 +411,10 @@ struct CompLz4Bd: Benchmark { } func measure() -> Double { - let startTime = CFAbsoluteTimeGetCurrent() + let startTime = DispatchTime.now().uptimeNanoseconds _ = LZ4.compress(data: self.data, independentBlocks: false, blockChecksums: false, contentChecksum: true, contentSize: false, blockSize: 4 * 1024 * 1024, dictionary: nil, dictionaryID: nil) - let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime + let timeElapsed = Double(DispatchTime.now().uptimeNanoseconds - startTime) / 1_000_000_000 return self.size / timeElapsed } @@ -469,9 +469,9 @@ struct Info7z: Benchmark { func measure() -> Double { do { - let startTime = CFAbsoluteTimeGetCurrent() + let startTime = DispatchTime.now().uptimeNanoseconds _ = try SevenZipContainer.info(container: self.data) - let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime + let timeElapsed = Double(DispatchTime.now().uptimeNanoseconds - startTime) / 1_000_000_000 return self.size / timeElapsed } catch let error { swcompExit(.benchmarkCannotMeasure(Self.self, error)) @@ -497,9 +497,9 @@ struct InfoTar: Benchmark { func measure() -> Double { do { - let startTime = CFAbsoluteTimeGetCurrent() + let startTime = DispatchTime.now().uptimeNanoseconds _ = try TarContainer.info(container: self.data) - let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime + let timeElapsed = Double(DispatchTime.now().uptimeNanoseconds - startTime) / 1_000_000_000 return self.size / timeElapsed } catch let error { swcompExit(.benchmarkCannotMeasure(Self.self, error)) @@ -525,9 +525,9 @@ struct InfoZip: Benchmark { func measure() -> Double { do { - let startTime = CFAbsoluteTimeGetCurrent() + let startTime = DispatchTime.now().uptimeNanoseconds _ = try ZipContainer.info(container: self.data) - let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime + let timeElapsed = Double(DispatchTime.now().uptimeNanoseconds - startTime) / 1_000_000_000 return self.size / timeElapsed } catch let error { swcompExit(.benchmarkCannotMeasure(Self.self, error)) @@ -551,9 +551,9 @@ struct CreateTar: Benchmark { } func measure() -> Double { - let startTime = CFAbsoluteTimeGetCurrent() + let startTime = DispatchTime.now().uptimeNanoseconds _ = TarContainer.create(from: self.entries) - let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime + let timeElapsed = Double(DispatchTime.now().uptimeNanoseconds - startTime) / 1_000_000_000 return self.size / timeElapsed } @@ -581,7 +581,7 @@ struct ReaderTar: Benchmark { func measure() -> Double { do { let handle = try FileHandle(forReadingFrom: self.url) - let startTime = CFAbsoluteTimeGetCurrent() + let startTime = DispatchTime.now().uptimeNanoseconds var reader = TarReader(fileHandle: handle) var isFinished = false var infos = [TarEntryInfo]() @@ -593,7 +593,7 @@ struct ReaderTar: Benchmark { return false } } - let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime + let timeElapsed = Double(DispatchTime.now().uptimeNanoseconds - startTime) / 1_000_000_000 try handle.closeCompat() return self.size / timeElapsed } catch let error { @@ -622,13 +622,13 @@ struct WriterTar: Benchmark { let url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString, isDirectory: false) try "".write(to: url, atomically: true, encoding: .utf8) let handle = try FileHandle(forWritingTo: url) - let startTime = CFAbsoluteTimeGetCurrent() + let startTime = DispatchTime.now().uptimeNanoseconds var writer = TarWriter(fileHandle: handle) for entry in self.entries { try writer.append(entry) } try writer.finalize() - let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime + let timeElapsed = Double(DispatchTime.now().uptimeNanoseconds - startTime) / 1_000_000_000 try handle.closeCompat() try FileManager.default.removeItem(at: url) return self.size / timeElapsed diff --git a/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift b/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift new file mode 100644 index 00000000..fcd27d88 --- /dev/null +++ b/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift @@ -0,0 +1,160 @@ +// Copyright (c) 2022 Timofey Solomko +// Licensed under MIT License +// +// See LICENSE for license information + +#if os(Linux) + import CoreFoundation +#endif + +import Foundation +import SWCompression +import SwiftCLI + +final class RunBenchmarkCommand: Command { + + let name = "run" + let shortDescription = "Run the specified benchmark" + let longDescription = "Runs the specified benchmark using external files.\nAvailable benchmarks: \(Benchmarks.allBenchmarks)" + + @Key("-i", "--iteration-count", description: "Sets the amount of the benchmark iterations") + var iterationCount: Int? + + @Key("-s", "--save", description: "Saves results into the specified file") + var savePath: String? + + @Flag("-a", "--append", description: "Appends results to a file instead of overwriting it when saving results") + var append: Bool + + @Key("-c", "--compare", description: "Compares results with other results saved in the specified file") + var comparePath: String? + + @Key("-d", "--description", description: "Adds a custom description when saving results") + var description: String? + + @Flag("-t", "--preserve-timestamp", description: "Adds a timestamp when saving a result") + var preserveTimestamp: Bool + + @Flag("-W", "--no-warmup", description: "Disables warmup iteration") + var noWarmup: Bool + + @Param var selectedBenchmark: Benchmarks + @CollectedParam(minCount: 1) var inputs: [String] + + func execute() throws { + guard self.iterationCount == nil || self.iterationCount! >= 1 + else { swcompExit(.benchmarkSmallIterCount) } + + var baseResults = [String: [(BenchmarkResult, UUID)]]() + var baseMetadatas = [UUID: String]() + if let comparePath = comparePath { + let baseSaveFile = try SaveFile.load(from: comparePath) + + baseMetadatas = Dictionary(uniqueKeysWithValues: zip(baseSaveFile.metadatas.keys, (1...baseSaveFile.metadatas.count).map { "(\($0))" })) + if baseMetadatas.count == 1 { + baseMetadatas[baseMetadatas.first!.key] = "" + } + for (metadataUUID, index) in baseMetadatas.sorted(by: { $0.value < $1.value }) { + print("BASE\(index) Metadata") + print("----------------") + baseSaveFile.metadatas[metadataUUID]!.print() + } + + for baseRun in baseSaveFile.runs { + baseResults.merge(Dictionary(grouping: baseRun.results.map { ($0, baseRun.metadataUUID) }, by: { $0.0.id }), + uniquingKeysWith: { $0 + $1 }) + } + } + + let title = "\(self.selectedBenchmark.titleName) Benchmark\n" + print(String(repeating: "=", count: title.count)) + print(title) + + var newResults = [BenchmarkResult]() + + for input in self.inputs { + print("Input: \(input)") + let benchmark = self.selectedBenchmark.initialized(input) + let iterationCount = self.iterationCount ?? benchmark.defaultIterationCount + + if !self.noWarmup { + print("Warmup iteration...") + // Zeroth (excluded) iteration. + benchmark.warmupIteration() + } + + var sum = 0.0 + var squareSum = 0.0 + + print("Iterations: ", terminator: "") + #if !os(Linux) + fflush(__stdoutp) + #endif + for i in 1...iterationCount { + if i > 1 { + print(", ", terminator: "") + } + let speed = benchmark.measure() + print(benchmark.format(speed), terminator: "") + #if !os(Linux) + fflush(__stdoutp) + #endif + sum += speed + squareSum += speed * speed + } + + let avg = sum / Double(iterationCount) + let std = sqrt(squareSum / Double(iterationCount) - sum * sum / Double(iterationCount * iterationCount)) + let result = BenchmarkResult(name: self.selectedBenchmark.rawValue, input: input, iterCount: iterationCount, + avg: avg, std: std) + + if let baseResults = baseResults[result.id] { + print("\nNEW: average = \(benchmark.format(avg)), standard deviation = \(benchmark.format(std))") + for (other, baseUUID) in baseResults { + print("BASE\(baseMetadatas[baseUUID]!): average = \(benchmark.format(other.avg)), standard deviation = \(benchmark.format(other.std))") + result.printComparison(with: other) + } + } else { + print("\nAverage = \(benchmark.format(avg)), standard deviation = \(benchmark.format(std))") + } + newResults.append(result) + + print() + } + + if let savePath = self.savePath { + let metadata = try BenchmarkMetadata(self.description, self.preserveTimestamp) + var saveFile: SaveFile + + var isDir = ObjCBool(false) + let saveFileExists = FileManager.default.fileExists(atPath: savePath, isDirectory: &isDir) + + if self.append && saveFileExists { + if isDir.boolValue { + swcompExit(.benchmarkCannotAppendToDirectory) + } + saveFile = try SaveFile.load(from: savePath) + var uuid: UUID + if let foundUUID = saveFile.metadatas.first(where: { $0.value == metadata })?.key { + uuid = foundUUID + } else { + repeat { + uuid = UUID() + } while saveFile.metadatas[uuid] != nil + saveFile.metadatas[uuid] = metadata + } + saveFile.runs.append(SaveFile.Run(metadataUUID: uuid, results: newResults)) + } else { + let uuid = UUID() + saveFile = SaveFile(metadatas: [uuid: metadata], runs: [SaveFile.Run(metadataUUID: uuid, results: newResults)]) + } + + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + + let data = try encoder.encode(saveFile) + try data.write(to: URL(fileURLWithPath: savePath)) + } + } + +} diff --git a/Sources/swcomp/Benchmarks/SaveFile.swift b/Sources/swcomp/Benchmarks/SaveFile.swift new file mode 100644 index 00000000..9aa778cc --- /dev/null +++ b/Sources/swcomp/Benchmarks/SaveFile.swift @@ -0,0 +1,27 @@ +// Copyright (c) 2022 Timofey Solomko +// Licensed under MIT License +// +// See LICENSE for license information + +import Foundation + +struct SaveFile: Codable { + + struct Run: Codable { + + var metadataUUID: UUID + var results: [BenchmarkResult] + + } + + var metadatas: [UUID: BenchmarkMetadata] + + var runs: [Run] + + static func load(from path: String) throws -> SaveFile { + let decoder = JSONDecoder() + let data = try Data(contentsOf: URL(fileURLWithPath: path)) + return try decoder.decode(SaveFile.self, from: data) + } + +} diff --git a/Sources/swcomp/Benchmarks/ShowBenchmarkCommand.swift b/Sources/swcomp/Benchmarks/ShowBenchmarkCommand.swift new file mode 100644 index 00000000..f6513e52 --- /dev/null +++ b/Sources/swcomp/Benchmarks/ShowBenchmarkCommand.swift @@ -0,0 +1,95 @@ +// Copyright (c) 2022 Timofey Solomko +// Licensed under MIT License +// +// See LICENSE for license information + +#if os(Linux) + import CoreFoundation +#endif + +import Foundation +import SwiftCLI + +final class ShowBenchmarkCommand: Command { + + let name = "show" + let shortDescription = "Print saved benchmarks results" + + @Key("-c", "--compare", description: "Compare with other saved benchmarks results") + var comparePath: String? + + @Param var path: String + + func execute() throws { + let newSaveFile = try SaveFile.load(from: self.path) + var newMetadatas = Dictionary(uniqueKeysWithValues: zip(newSaveFile.metadatas.keys, (1...newSaveFile.metadatas.count).map { "(\($0))" })) + if newMetadatas.count == 1 { + newMetadatas[newMetadatas.first!.key] = "" + } + for (metadataUUID, index) in newMetadatas.sorted(by: { $0.value < $1.value }) { + print("NEW\(index) Metadata") + print("---------------") + newSaveFile.metadatas[metadataUUID]!.print() + } + + var newResults = [String: [(BenchmarkResult, UUID)]]() + for newRun in newSaveFile.runs { + newResults.merge(Dictionary(grouping: newRun.results.map { ($0, newRun.metadataUUID) }, by: { $0.0.id }), + uniquingKeysWith: { $0 + $1 }) + } + + var baseResults = [String: [(BenchmarkResult, UUID)]]() + var baseMetadatas = [UUID: String]() + if let comparePath = comparePath { + let baseSaveFile = try SaveFile.load(from: comparePath) + + baseMetadatas = Dictionary(uniqueKeysWithValues: zip(baseSaveFile.metadatas.keys, (1...baseSaveFile.metadatas.count).map { "(\($0))" })) + if baseMetadatas.count == 1 { + baseMetadatas[baseMetadatas.first!.key] = "" + } + for (metadataUUID, index) in baseMetadatas.sorted(by: { $0.value < $1.value }) { + print("BASE\(index) Metadata") + print("----------------") + baseSaveFile.metadatas[metadataUUID]!.print() + } + + for baseRun in baseSaveFile.runs { + baseResults.merge(Dictionary(grouping: baseRun.results.map { ($0, baseRun.metadataUUID) }, by: { $0.0.id }), + uniquingKeysWith: { $0 + $1 }) + } + } + + for resultId in newResults.keys.sorted() { + let results = newResults[resultId]! + for (result, metadataUUID) in results { + let benchmark = Benchmarks(rawValue: result.name)?.initialized(result.input) + + print("\(result.name) => \(result.input), iterations = \(result.iterCount)") + + print("NEW\(newMetadatas[metadataUUID]!): average = \(benchmark.format(result.avg)), standard deviation = \(benchmark.format(result.std))") + if let baseResults = baseResults[resultId] { + for (other, baseUUID) in baseResults { + print("BASE\(baseMetadatas[baseUUID]!): average = \(benchmark.format(other.avg)), standard deviation = \(benchmark.format(other.std))") + result.printComparison(with: other) + } + } + + print() + } + } + } + +} + +fileprivate extension Optional where Wrapped == Benchmark { + + func format(_ value: Double) -> String { + switch self { + case .some(let benchmark): + return benchmark.format(value) + case .none: + return String(value) + } + } + +} diff --git a/Sources/swcomp/Containers/CommonFunctions.swift b/Sources/swcomp/Containers/CommonFunctions.swift index 39fcd607..0822fb06 100644 --- a/Sources/swcomp/Containers/CommonFunctions.swift +++ b/Sources/swcomp/Containers/CommonFunctions.swift @@ -102,12 +102,21 @@ func writeFile(_ entry: T, _ outputURL: URL, _ verbose: Bool) } guard destinationPath != nil else { swcompExit(.containerSymLinkDestPath(entryName)) } - let endURL = entryFullURL.deletingLastPathComponent().appendingPathComponent(destinationPath!) if verbose { - print("l: \(entryName) -> \(endURL.path)") + print("l: \(entryName) -> \(destinationPath!)") } - try fileManager.createSymbolicLink(atPath: entryFullURL.path, withDestinationPath: endURL.path) - // We cannot apply attributes to symbolic link. + try fileManager.createSymbolicLink(atPath: entryFullURL.path, withDestinationPath: destinationPath!) + // We cannot apply attributes to symbolic links. + return + } else if entry.info.type == .hardLink { + guard let destinationPath = (entry as? TarEntry)?.info.linkName + else { swcompExit(.containerHardLinkDestPath(entryName)) } + if verbose { + print("hl: \(entryName) -> \(destinationPath)") + } + // Note that the order of parameters is inversed for hard links. + try fileManager.linkItem(atPath: destinationPath, toPath: entryFullURL.path) + // We cannot apply attributes to hard links. return } else if entry.info.type == .regular { if verbose { diff --git a/Sources/swcomp/Containers/TarCommand.swift b/Sources/swcomp/Containers/TarCommand.swift index 51cb3ff3..eb75a210 100644 --- a/Sources/swcomp/Containers/TarCommand.swift +++ b/Sources/swcomp/Containers/TarCommand.swift @@ -228,10 +228,3 @@ final class TarCommand: Command { } } - -#if os(Linux) || os(Windows) - @discardableResult - fileprivate func autoreleasepool(_ block: () throws -> T) rethrows -> T { - return try block() - } -#endif diff --git a/Sources/swcomp/Extensions/TarEntry+Create.swift b/Sources/swcomp/Extensions/TarEntry+Create.swift index c8b7921b..af166616 100644 --- a/Sources/swcomp/Extensions/TarEntry+Create.swift +++ b/Sources/swcomp/Extensions/TarEntry+Create.swift @@ -174,3 +174,10 @@ extension TarEntry { } } + +#if os(Linux) || os(Windows) + @discardableResult + fileprivate func autoreleasepool(_ block: () throws -> T) rethrows -> T { + return try block() + } +#endif diff --git a/Sources/swcomp/SwcompError.swift b/Sources/swcomp/SwcompError.swift index 717445ff..58127595 100644 --- a/Sources/swcomp/SwcompError.swift +++ b/Sources/swcomp/SwcompError.swift @@ -17,7 +17,10 @@ enum SwcompError { case benchmarkCannotMeasure(Benchmark.Type, Error) case benchmarkCannotMeasureBadOutSize(Benchmark.Type) case benchmarkReaderTarNoInputSize(String) + case benchmarkCannotGetSubcommandPathWindows + case benchmarkCannotAppendToDirectory case containerSymLinkDestPath(String) + case containerHardLinkDestPath(String) case containerNoEntryData(String) case containerOutPathExistsNotDir case fileHandleCannotOpen @@ -47,8 +50,14 @@ enum SwcompError { return 214 case .benchmarkReaderTarNoInputSize: return 205 + case .benchmarkCannotGetSubcommandPathWindows: + return 206 + case .benchmarkCannotAppendToDirectory: + return 207 case .containerSymLinkDestPath: return 301 + case .containerHardLinkDestPath: + return 311 case .containerNoEntryData: return 302 case .containerOutPathExistsNotDir: @@ -77,7 +86,7 @@ enum SwcompError { case .benchmarkSmallIterCount: return "Iteration count, if set, must be not less than 1." case .benchmarkUnknownCompResult: - return "Unknown comparison " + return "Unknown comparison." case .benchmarkCannotSetup(let benchmark, let input, let error): return "Unable to set up benchmark \(benchmark): input=\(input), error=\(error)." case .benchmarkCannotMeasure(let benchmark, let error): @@ -86,8 +95,14 @@ enum SwcompError { return "Unable to measure benchmark \(benchmark): outputData.count is not greater than zero." case .benchmarkReaderTarNoInputSize(let input): return "ReaderTAR.benchmarkSetUp(): file size is not available for input=\(input)." + case .benchmarkCannotGetSubcommandPathWindows: + return "Cannot get subcommand path on Windows. (This error should never be shown!)" + case .benchmarkCannotAppendToDirectory: + return "Cannot append results to the save path since it is a directory." case .containerSymLinkDestPath(let entryName): return "Unable to get destination path for symbolic link \(entryName)." + case .containerHardLinkDestPath(let entryName): + return "Unable to get destination path for hard link \(entryName)." case .containerNoEntryData(let entryName): return "Unable to get data for the entry \(entryName)." case .containerOutPathExistsNotDir: diff --git a/Sources/swcomp/main.swift b/Sources/swcomp/main.swift index 0b64009d..e023b90b 100644 --- a/Sources/swcomp/main.swift +++ b/Sources/swcomp/main.swift @@ -7,7 +7,9 @@ import Foundation import SWCompression import SwiftCLI -let cli = CLI(name: "swcomp", version: "4.8.2", +let _SWC_VERSION = "4.8.3" + +let cli = CLI(name: "swcomp", version: _SWC_VERSION, description: """ swcomp - a small command-line client for SWCompression framework. Serves as an example of SWCompression usage. @@ -21,5 +23,5 @@ cli.commands = [XZCommand(), ZipCommand(), TarCommand(), SevenZipCommand(), - BenchmarkCommand()] + BenchmarkGroup()] cli.goAndExit() diff --git a/Tests/GzipTests.swift b/Tests/GzipTests.swift index 203c47fe..db02a4f9 100644 --- a/Tests/GzipTests.swift +++ b/Tests/GzipTests.swift @@ -19,6 +19,7 @@ class GzipTests: XCTestCase { XCTAssertEqual(testGzipHeader.osType, .unix) XCTAssertEqual(testGzipHeader.fileName, "\(testName).answer") XCTAssertEqual(testGzipHeader.comment, nil) + XCTAssertTrue(testGzipHeader.extraFields.isEmpty) } func unarchive(test testName: String) throws { @@ -36,13 +37,21 @@ class GzipTests: XCTestCase { let mtimeDate = Date(timeIntervalSinceNow: 0.0) let mtime = mtimeDate.timeIntervalSince1970.rounded(.towardZero) + // Random extra field. + let si1 = UInt8.random(in: 0...255) + let si2 = UInt8.random(in: 1...255) // 0 is a reserved value here. + let len = UInt16.random(in: 0...(UInt16.max - 4)) + var extraFieldBytes = [UInt8]() + for _ in 0.. Downloading BitByteData dependency using Carthage") script = ["carthage", "bootstrap", "--no-use-binaries"] @@ -72,6 +72,42 @@ def action_dbm(args): script += ["--use-xcframeworks"] _sprun(script) +def action_pr(args): + _sprun(["agvtool", "next-version", "-all"]) + _sprun(["agvtool", "new-marketing-version", args.version]) + + f = open("SWCompression.podspec", "r", encoding="utf-8") + lines = f.readlines() + f.close() + f = open("SWCompression.podspec", "w", encoding="utf-8") + for line in lines: + if line.startswith(" s.version = "): + line = " s.version = \"" + args.version + "\"\n" + f.write(line) + f.close() + + f = open(".jazzy.yaml", "r", encoding="utf-8") + lines = f.readlines() + f.close() + f = open(".jazzy.yaml", "w", encoding="utf-8") + for line in lines: + if line.startswith("module_version: "): + line = "module_version: " + args.version + "\n" + elif line.startswith("github_file_prefix: "): + line = "github_file_prefix: https://github.com/tsolomko/SWCompression/tree/" + args.version + "\n" + f.write(line) + f.close() + + f = open("Sources/swcomp/main.swift", "r", encoding="utf-8") + lines = f.readlines() + f.close() + f = open("Sources/swcomp/main.swift", "w", encoding="utf-8") + for line in lines: + if line.startswith("let _SWC_VERSION = "): + line = "let _SWC_VERSION = \"" + args.version + "\"\n" + f.write(line) + f.close() + parser = argparse.ArgumentParser(description="A tool with useful commands for developing SWCompression") subparsers = parser.add_subparsers(title="commands", help="a command to perform", metavar="CMD") @@ -96,5 +132,11 @@ def action_dbm(args): help="build BitByteData as a XCFramework") parser_dbm.set_defaults(func=action_dbm) +# Parser for 'prepare-release' command. +parser_pr = subparsers.add_parser("prepare-release", help="prepare next release", + description="prepare next release of SWCompression") +parser_pr.add_argument("version", metavar="VERSION", help="next version number") +parser_pr.set_defaults(func=action_pr) + args = parser.parse_args() args.func(args)