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)