From 56f9533372f5594c02abf534d51ab275c4e14cef Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Wed, 5 Oct 2022 15:07:03 +0300 Subject: [PATCH 01/42] [CI] Upgrade to Xcode 14.0.1 and upgrade deploy setup --- azure-pipelines.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index e84f2ba9..20c1105d 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -46,7 +46,7 @@ stages: UTILS_PW_XCF_FLAG: '--xcf' macosSwift57: imageName: 'macOS-12' - DEVELOPER_DIR: '/Applications/Xcode_14.0.app' + DEVELOPER_DIR: '/Applications/Xcode_14.0.1.app' WATCHOS_ACTIONS: 'clean test' WATCHOS_SIMULATOR: 'Apple Watch Series 6 (44mm)' UTILS_PW_XCF_FLAG: '--xcf' @@ -172,9 +172,9 @@ stages: - job: ghPages displayName: 'Publish API docs to GH Pages' pool: - vmImage: 'macOS-11' + vmImage: 'macOS-12' variables: - DEVELOPER_DIR: '/Applications/Xcode_12.4.app' + DEVELOPER_DIR: '/Applications/Xcode_14.0.1.app' steps: - script: | set -e -o xtrace From dca723d9402bf44b6687ef5b5cea58b2f62a389b Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Wed, 5 Oct 2022 15:07:38 +0300 Subject: [PATCH 02/42] [swcomp] Rename benchmark command to run command and add a long description --- .../{BenchmarkCommand.swift => RunBenchmarkCommand.swift} | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) rename Sources/swcomp/Benchmarks/{BenchmarkCommand.swift => RunBenchmarkCommand.swift} (95%) diff --git a/Sources/swcomp/Benchmarks/BenchmarkCommand.swift b/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift similarity index 95% rename from Sources/swcomp/Benchmarks/BenchmarkCommand.swift rename to Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift index 1aa21f12..65cacf0c 100644 --- a/Sources/swcomp/Benchmarks/BenchmarkCommand.swift +++ b/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift @@ -11,10 +11,11 @@ import Foundation import SWCompression import SwiftCLI -final class BenchmarkCommand: Command { +final class RunBenchmarkCommand: Command { - let name = "benchmark" - let shortDescription = "Perform the specified benchmark using external files, available benchmarks: \(Benchmarks.allBenchmarks)" + 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? From e84040b15696f42fdcae74bb9997e1cb73031344 Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Wed, 5 Oct 2022 15:07:54 +0300 Subject: [PATCH 03/42] [swcomp] Add benchmark command group --- .../swcomp/Benchmarks/BenchmarkGroup.swift | 19 +++++++++++++++++++ Sources/swcomp/main.swift | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 Sources/swcomp/Benchmarks/BenchmarkGroup.swift diff --git a/Sources/swcomp/Benchmarks/BenchmarkGroup.swift b/Sources/swcomp/Benchmarks/BenchmarkGroup.swift new file mode 100644 index 00000000..45d9c9b2 --- /dev/null +++ b/Sources/swcomp/Benchmarks/BenchmarkGroup.swift @@ -0,0 +1,19 @@ +// 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() + ] + +} diff --git a/Sources/swcomp/main.swift b/Sources/swcomp/main.swift index 0b64009d..65e4513b 100644 --- a/Sources/swcomp/main.swift +++ b/Sources/swcomp/main.swift @@ -21,5 +21,5 @@ cli.commands = [XZCommand(), ZipCommand(), TarCommand(), SevenZipCommand(), - BenchmarkCommand()] + BenchmarkGroup()] cli.goAndExit() From 61eb547202dbfbf0db034997b9cde9c64a3520da Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Wed, 5 Oct 2022 18:07:47 +0300 Subject: [PATCH 04/42] [swcomp] Extract comparison printing as a new function --- .../swcomp/Benchmarks/BenchmarkResult.swift | 37 ++++++++++++++++++- .../Benchmarks/RunBenchmarkCommand.swift | 35 +----------------- 2 files changed, 38 insertions(+), 34 deletions(-) diff --git a/Sources/swcomp/Benchmarks/BenchmarkResult.swift b/Sources/swcomp/Benchmarks/BenchmarkResult.swift index 2a5eb46e..f5ef820c 100644 --- a/Sources/swcomp/Benchmarks/BenchmarkResult.swift +++ b/Sources/swcomp/Benchmarks/BenchmarkResult.swift @@ -13,7 +13,42 @@ struct BenchmarkResult: Codable { var avg: Double var std: Double - func compare(with other: BenchmarkResult) -> Int? { + 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/RunBenchmarkCommand.swift b/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift index 65cacf0c..402e1530 100644 --- a/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift +++ b/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift @@ -23,7 +23,7 @@ final class RunBenchmarkCommand: Command { @Key("-s", "--save", description: "Saves the results into the specified file") var savePath: String? - @Key("-c", "--compare", description: "Compares the results with the results saved in the specified file") + @Key("-c", "--compare", description: "Compares the results with other results saved in the specified file") var comparePath: String? @Flag("-W", "--no-warmup", description: "Disables warmup iteration") @@ -87,38 +87,7 @@ final class RunBenchmarkCommand: Command { 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)") - } + result.printComparison(with: other) } results.append(result) From f47cf14f26158a54b0baf4efb9a1d1b1fb726cdb Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Wed, 5 Oct 2022 20:33:27 +0300 Subject: [PATCH 05/42] [swcomp] Improve loading and printing of base benchmark results --- .../swcomp/Benchmarks/BenchmarkResult.swift | 11 +++++++ .../Benchmarks/RunBenchmarkCommand.swift | 29 ++++++++++++------- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/Sources/swcomp/Benchmarks/BenchmarkResult.swift b/Sources/swcomp/Benchmarks/BenchmarkResult.swift index f5ef820c..ce3175a3 100644 --- a/Sources/swcomp/Benchmarks/BenchmarkResult.swift +++ b/Sources/swcomp/Benchmarks/BenchmarkResult.swift @@ -13,6 +13,17 @@ struct BenchmarkResult: Codable { var avg: Double var std: Double + var id: String { + return [self.name, self.input, String(self.iterCount)].joined(separator: "<#>") + } + + static func load(from path: String) throws -> [String : [BenchmarkResult]] { + let decoder = JSONDecoder() + let data = try Data(contentsOf: URL(fileURLWithPath: path)) + let decodedResults = try decoder.decode(Array.self, from: data) + return Dictionary(grouping: decodedResults, by: { $0.id }) + } + func printComparison(with other: BenchmarkResult) { let diff = (self.avg / other.avg - 1) * 100 let comparison = self.compare(with: other) diff --git a/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift b/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift index 402e1530..d920215e 100644 --- a/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift +++ b/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift @@ -41,11 +41,9 @@ final class RunBenchmarkCommand: Command { print(title) var results = [BenchmarkResult]() - var otherResults: [BenchmarkResult]? = nil + var otherResults: [String : [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) + otherResults = try BenchmarkResult.load(from: comparePath) } for input in self.inputs { @@ -79,15 +77,24 @@ final class RunBenchmarkCommand: Command { squareSum += speed * speed } - let avgSpeed = sum / Double(iterationCount) - print("\nAverage: " + benchmark.format(avgSpeed)) + let avg = sum / Double(iterationCount) 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 }) { - result.printComparison(with: other) + avg: avg, std: std) + + if let otherResults = otherResults?[result.id] { + if otherResults.count > 1 { + print("\nAverage = \(benchmark.format(avg)), standard deviation = \(benchmark.format(std))") + print("WARNING: There is more than one result with the same id=\(result.id) in the file \(self.comparePath!)") + print("Skipped comparison...\n") + } else { + let other = otherResults.first! + print("\nNEW: average = \(benchmark.format(avg)), standard deviation = \(benchmark.format(std))") + print("BASE: 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))") } results.append(result) From 4e4f734ab0c8d5d39c8f5f53a9e985e2ba123cbc Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Wed, 5 Oct 2022 20:44:05 +0300 Subject: [PATCH 06/42] [swcomp] Add "benchmark show" command --- .../swcomp/Benchmarks/BenchmarkGroup.swift | 3 +- .../Benchmarks/ShowBenchmarkCommand.swift | 75 +++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 Sources/swcomp/Benchmarks/ShowBenchmarkCommand.swift diff --git a/Sources/swcomp/Benchmarks/BenchmarkGroup.swift b/Sources/swcomp/Benchmarks/BenchmarkGroup.swift index 45d9c9b2..3bd1e587 100644 --- a/Sources/swcomp/Benchmarks/BenchmarkGroup.swift +++ b/Sources/swcomp/Benchmarks/BenchmarkGroup.swift @@ -13,7 +13,8 @@ final class BenchmarkGroup: CommandGroup { let shortDescription = "Benchmark-related commands" let children: [Routable] = [ - RunBenchmarkCommand() + RunBenchmarkCommand(), + ShowBenchmarkCommand() ] } diff --git a/Sources/swcomp/Benchmarks/ShowBenchmarkCommand.swift b/Sources/swcomp/Benchmarks/ShowBenchmarkCommand.swift new file mode 100644 index 00000000..4135a042 --- /dev/null +++ b/Sources/swcomp/Benchmarks/ShowBenchmarkCommand.swift @@ -0,0 +1,75 @@ +// 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 loadedResults = try BenchmarkResult.load(from: self.path) + var otherResults: [String : [BenchmarkResult]]? = nil + if let comparePath = comparePath { + otherResults = try BenchmarkResult.load(from: comparePath) + } + + for resultId in loadedResults.keys.sorted() { + let results = loadedResults[resultId]! + if results.count > 1 { + print("WARNING: There is more than one result with the same id=\(resultId) in the file \(self.path)") + print("Skipped...\n") + continue + } + + let result = results.first! + let benchmark = Benchmarks(rawValue: result.name)?.initialized(result.input) + + print("\(result.name) => \(result.input), iterations = \(result.iterCount)") + + if let otherResults = otherResults?[resultId] { + if otherResults.count > 1 { + print("Average = \(benchmark.format(result.avg)), standard deviation = \(benchmark.format(result.std))") + print("WARNING: There is more than one result with the same id=\(resultId) in the file \(self.comparePath!)") + print("Skipped comparison...\n") + } else { + let other = otherResults.first! + print("NEW: average = \(benchmark.format(result.avg)), standard deviation = \(benchmark.format(result.std))") + print("BASE: average = \(benchmark.format(other.avg)), standard deviation = \(benchmark.format(other.std))") + result.printComparison(with: other) + } + } else { + print("Average = \(benchmark.format(result.avg)), standard deviation = \(benchmark.format(result.std))") + } + + 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) + } + } + +} From 1c98d5316d11cc38297690db4ecfa9951911e371 Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Thu, 6 Oct 2022 12:55:15 +0300 Subject: [PATCH 07/42] [swcomp] Perform comparison with the first result when there are multiple Previosuly, the comparsion was skipped entirely. --- Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift | 12 +++++------- Sources/swcomp/Benchmarks/ShowBenchmarkCommand.swift | 12 +++++------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift b/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift index d920215e..0a7ce6fa 100644 --- a/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift +++ b/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift @@ -84,15 +84,13 @@ final class RunBenchmarkCommand: Command { if let otherResults = otherResults?[result.id] { if otherResults.count > 1 { - print("\nAverage = \(benchmark.format(avg)), standard deviation = \(benchmark.format(std))") print("WARNING: There is more than one result with the same id=\(result.id) in the file \(self.comparePath!)") - print("Skipped comparison...\n") - } else { - let other = otherResults.first! - print("\nNEW: average = \(benchmark.format(avg)), standard deviation = \(benchmark.format(std))") - print("BASE: average = \(benchmark.format(other.avg)), standard deviation = \(benchmark.format(other.std))") - result.printComparison(with: other) + print("Comparing with the first one...\n") } + let other = otherResults.first! + print("\nNEW: average = \(benchmark.format(avg)), standard deviation = \(benchmark.format(std))") + print("BASE: 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))") } diff --git a/Sources/swcomp/Benchmarks/ShowBenchmarkCommand.swift b/Sources/swcomp/Benchmarks/ShowBenchmarkCommand.swift index 4135a042..4dcdea22 100644 --- a/Sources/swcomp/Benchmarks/ShowBenchmarkCommand.swift +++ b/Sources/swcomp/Benchmarks/ShowBenchmarkCommand.swift @@ -42,15 +42,13 @@ final class ShowBenchmarkCommand: Command { if let otherResults = otherResults?[resultId] { if otherResults.count > 1 { - print("Average = \(benchmark.format(result.avg)), standard deviation = \(benchmark.format(result.std))") print("WARNING: There is more than one result with the same id=\(resultId) in the file \(self.comparePath!)") - print("Skipped comparison...\n") - } else { - let other = otherResults.first! - print("NEW: average = \(benchmark.format(result.avg)), standard deviation = \(benchmark.format(result.std))") - print("BASE: average = \(benchmark.format(other.avg)), standard deviation = \(benchmark.format(other.std))") - result.printComparison(with: other) + print("Comparing with the first one...\n") } + let other = otherResults.first! + print("NEW: average = \(benchmark.format(result.avg)), standard deviation = \(benchmark.format(result.std))") + print("BASE: average = \(benchmark.format(other.avg)), standard deviation = \(benchmark.format(other.std))") + result.printComparison(with: other) } else { print("Average = \(benchmark.format(result.avg)), standard deviation = \(benchmark.format(result.std))") } From 269ae9b1f3b46eb3c987d9f6ca7c3c8dda1fad00 Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Thu, 6 Oct 2022 13:31:07 +0300 Subject: [PATCH 08/42] [swcomp] SWC version is now stored as a global variable --- Sources/swcomp/main.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/swcomp/main.swift b/Sources/swcomp/main.swift index 65e4513b..0c4247f7 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.2" + +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. From a3e439aff5d885ffc455827ddef429e085b2a396 Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Thu, 6 Oct 2022 14:26:29 +0300 Subject: [PATCH 09/42] [swcomp] Add SaveFile struct for saving additional benchmark metadata --- Sources/swcomp/Benchmarks/SaveFile.swift | 76 ++++++++++++++++++++++++ Sources/swcomp/SwcompError.swift | 5 ++ 2 files changed, 81 insertions(+) create mode 100644 Sources/swcomp/Benchmarks/SaveFile.swift diff --git a/Sources/swcomp/Benchmarks/SaveFile.swift b/Sources/swcomp/Benchmarks/SaveFile.swift new file mode 100644 index 00000000..755880da --- /dev/null +++ b/Sources/swcomp/Benchmarks/SaveFile.swift @@ -0,0 +1,76 @@ +// Copyright (c) 2022 Timofey Solomko +// Licensed under MIT License +// +// See LICENSE for license information + +import Foundation + +struct SaveFile: Codable { + + var timestamp: TimeInterval + var osInfo: String + var swiftVersion: String + var swcVersion: String + var description: String? + var results: [BenchmarkResult] + + 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 SaveFile.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 SaveFile.run(command: SaveFile.getExecURL(for: "uname"), arguments: ["-a"]) + #else + #if os(Windows) + return "Unknown Windows OS" + #else + return try SaveFile.run(command: SaveFile.getExecURL(for: "sw_vers")) + #endif + #endif + } + + init(_ description: String?, _ results: [BenchmarkResult]) throws { + self.timestamp = Date.timeIntervalSinceReferenceDate + self.osInfo = try SaveFile.getOsInfo() + #if os(Windows) + self.swiftVersion = "Unknown Swift version on Windows" + #else + self.swiftVersion = try SaveFile.run(command: SaveFile.getExecURL(for: "swift"), arguments: ["-version"]) + #endif + self.swcVersion = _SWC_VERSION + self.description = description + self.results = results + } + + static func loadResults(from path: String) throws -> [String : [BenchmarkResult]] { + let decoder = JSONDecoder() + let data = try Data(contentsOf: URL(fileURLWithPath: path)) + let decodedResults = try decoder.decode(SaveFile.self, from: data).results + return Dictionary(grouping: decodedResults, by: { $0.id }) + } + +} diff --git a/Sources/swcomp/SwcompError.swift b/Sources/swcomp/SwcompError.swift index 717445ff..a9d0407b 100644 --- a/Sources/swcomp/SwcompError.swift +++ b/Sources/swcomp/SwcompError.swift @@ -17,6 +17,7 @@ enum SwcompError { case benchmarkCannotMeasure(Benchmark.Type, Error) case benchmarkCannotMeasureBadOutSize(Benchmark.Type) case benchmarkReaderTarNoInputSize(String) + case benchmarkCannotGetSubcommandPathWindows case containerSymLinkDestPath(String) case containerNoEntryData(String) case containerOutPathExistsNotDir @@ -47,6 +48,8 @@ enum SwcompError { return 214 case .benchmarkReaderTarNoInputSize: return 205 + case .benchmarkCannotGetSubcommandPathWindows: + return 206 case .containerSymLinkDestPath: return 301 case .containerNoEntryData: @@ -86,6 +89,8 @@ 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 .containerSymLinkDestPath(let entryName): return "Unable to get destination path for symbolic link \(entryName)." case .containerNoEntryData(let entryName): From 577531c4be3456aaba6667d3a2564ea3cfe2531c Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Thu, 6 Oct 2022 14:26:59 +0300 Subject: [PATCH 10/42] [swcomp] Use SaveFile for saving and loading benchmark results --- Sources/swcomp/Benchmarks/BenchmarkResult.swift | 7 ------- Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift | 5 +++-- Sources/swcomp/Benchmarks/ShowBenchmarkCommand.swift | 4 ++-- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/Sources/swcomp/Benchmarks/BenchmarkResult.swift b/Sources/swcomp/Benchmarks/BenchmarkResult.swift index ce3175a3..f3a9fe5d 100644 --- a/Sources/swcomp/Benchmarks/BenchmarkResult.swift +++ b/Sources/swcomp/Benchmarks/BenchmarkResult.swift @@ -17,13 +17,6 @@ struct BenchmarkResult: Codable { return [self.name, self.input, String(self.iterCount)].joined(separator: "<#>") } - static func load(from path: String) throws -> [String : [BenchmarkResult]] { - let decoder = JSONDecoder() - let data = try Data(contentsOf: URL(fileURLWithPath: path)) - let decodedResults = try decoder.decode(Array.self, from: data) - return Dictionary(grouping: decodedResults, by: { $0.id }) - } - func printComparison(with other: BenchmarkResult) { let diff = (self.avg / other.avg - 1) * 100 let comparison = self.compare(with: other) diff --git a/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift b/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift index 0a7ce6fa..5a598a7e 100644 --- a/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift +++ b/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift @@ -43,7 +43,7 @@ final class RunBenchmarkCommand: Command { var results = [BenchmarkResult]() var otherResults: [String : [BenchmarkResult]]? = nil if let comparePath = comparePath { - otherResults = try BenchmarkResult.load(from: comparePath) + otherResults = try SaveFile.loadResults(from: comparePath) } for input in self.inputs { @@ -100,10 +100,11 @@ final class RunBenchmarkCommand: Command { } if let savePath = self.savePath { + let saveFile = try SaveFile(nil, results) let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted - let data = try encoder.encode(results) + let data = try encoder.encode(saveFile) try data.write(to: URL(fileURLWithPath: savePath)) } } diff --git a/Sources/swcomp/Benchmarks/ShowBenchmarkCommand.swift b/Sources/swcomp/Benchmarks/ShowBenchmarkCommand.swift index 4dcdea22..7c2f9839 100644 --- a/Sources/swcomp/Benchmarks/ShowBenchmarkCommand.swift +++ b/Sources/swcomp/Benchmarks/ShowBenchmarkCommand.swift @@ -21,10 +21,10 @@ final class ShowBenchmarkCommand: Command { @Param var path: String func execute() throws { - let loadedResults = try BenchmarkResult.load(from: self.path) + let loadedResults = try SaveFile.loadResults(from: self.path) var otherResults: [String : [BenchmarkResult]]? = nil if let comparePath = comparePath { - otherResults = try BenchmarkResult.load(from: comparePath) + otherResults = try SaveFile.loadResults(from: comparePath) } for resultId in loadedResults.keys.sorted() { From b660005a7c924939d54d8bca47b33460c7070dd3 Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Thu, 6 Oct 2022 15:08:27 +0300 Subject: [PATCH 11/42] [swcomp] Print saved metadata --- .../Benchmarks/RunBenchmarkCommand.swift | 22 ++++++++++------- Sources/swcomp/Benchmarks/SaveFile.swift | 22 ++++++++++++++--- .../Benchmarks/ShowBenchmarkCommand.swift | 24 ++++++++++++------- 3 files changed, 48 insertions(+), 20 deletions(-) diff --git a/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift b/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift index 5a598a7e..d0ef7ee1 100644 --- a/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift +++ b/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift @@ -36,16 +36,20 @@ final class RunBenchmarkCommand: Command { guard self.iterationCount == nil || self.iterationCount! >= 1 else { swcompExit(.benchmarkSmallIterCount) } - let title = "\(self.selectedBenchmark.titleName) Benchmark\n" - print(String(repeating: "=", count: title.count)) - print(title) - var results = [BenchmarkResult]() - var otherResults: [String : [BenchmarkResult]]? = nil + var baseResults: [String: [BenchmarkResult]]? = nil if let comparePath = comparePath { - otherResults = try SaveFile.loadResults(from: comparePath) + let baseSaveFile = try SaveFile.load(from: comparePath) + print("BASE Metadata") + print("-------------") + baseSaveFile.printMetadata() + baseResults = baseSaveFile.groupedResults } + let title = "\(self.selectedBenchmark.titleName) Benchmark\n" + print(String(repeating: "=", count: title.count)) + print(title) + for input in self.inputs { print("Input: \(input)") let benchmark = self.selectedBenchmark.initialized(input) @@ -82,12 +86,12 @@ final class RunBenchmarkCommand: Command { let result = BenchmarkResult(name: self.selectedBenchmark.rawValue, input: input, iterCount: iterationCount, avg: avg, std: std) - if let otherResults = otherResults?[result.id] { - if otherResults.count > 1 { + if let baseResults = baseResults?[result.id] { + if baseResults.count > 1 { print("WARNING: There is more than one result with the same id=\(result.id) in the file \(self.comparePath!)") print("Comparing with the first one...\n") } - let other = otherResults.first! + let other = baseResults.first! print("\nNEW: average = \(benchmark.format(avg)), standard deviation = \(benchmark.format(std))") print("BASE: average = \(benchmark.format(other.avg)), standard deviation = \(benchmark.format(other.std))") result.printComparison(with: other) diff --git a/Sources/swcomp/Benchmarks/SaveFile.swift b/Sources/swcomp/Benchmarks/SaveFile.swift index 755880da..27114016 100644 --- a/Sources/swcomp/Benchmarks/SaveFile.swift +++ b/Sources/swcomp/Benchmarks/SaveFile.swift @@ -14,6 +14,10 @@ struct SaveFile: Codable { var description: String? var results: [BenchmarkResult] + var groupedResults: [String: [BenchmarkResult]] { + return Dictionary(grouping: self.results, by: { $0.id }) + } + private static func run(command: URL, arguments: [String] = []) throws -> String { let task = Process() let pipe = Pipe() @@ -66,11 +70,23 @@ struct SaveFile: Codable { self.results = results } - static func loadResults(from path: String) throws -> [String : [BenchmarkResult]] { + func printMetadata() { + print("OS Info: \(self.osInfo)", terminator: "") + print("Swift version: \(self.swiftVersion)", terminator: "") + print("SWC version: \(self.swcVersion)") + print("Timestamp: " + + DateFormatter.localizedString(from: Date(timeIntervalSinceReferenceDate: self.timestamp), + dateStyle: .short, timeStyle: .short)) + if let description = self.description { + print("Description: \(description)") + } + print() + } + + static func load(from path: String) throws -> SaveFile { let decoder = JSONDecoder() let data = try Data(contentsOf: URL(fileURLWithPath: path)) - let decodedResults = try decoder.decode(SaveFile.self, from: data).results - return Dictionary(grouping: decodedResults, by: { $0.id }) + return try decoder.decode(SaveFile.self, from: data) } } diff --git a/Sources/swcomp/Benchmarks/ShowBenchmarkCommand.swift b/Sources/swcomp/Benchmarks/ShowBenchmarkCommand.swift index 7c2f9839..49f40924 100644 --- a/Sources/swcomp/Benchmarks/ShowBenchmarkCommand.swift +++ b/Sources/swcomp/Benchmarks/ShowBenchmarkCommand.swift @@ -21,14 +21,22 @@ final class ShowBenchmarkCommand: Command { @Param var path: String func execute() throws { - let loadedResults = try SaveFile.loadResults(from: self.path) - var otherResults: [String : [BenchmarkResult]]? = nil + let newSaveFile = try SaveFile.load(from: self.path) + print("NEW Metadata") + print("------------") + newSaveFile.printMetadata() + let newResults = newSaveFile.groupedResults + var baseResults: [String: [BenchmarkResult]]? = nil if let comparePath = comparePath { - otherResults = try SaveFile.loadResults(from: comparePath) + let baseSaveFile = try SaveFile.load(from: comparePath) + print("BASE Metadata") + print("-------------") + baseSaveFile.printMetadata() + baseResults = baseSaveFile.groupedResults } - for resultId in loadedResults.keys.sorted() { - let results = loadedResults[resultId]! + for resultId in newResults.keys.sorted() { + let results = newResults[resultId]! if results.count > 1 { print("WARNING: There is more than one result with the same id=\(resultId) in the file \(self.path)") print("Skipped...\n") @@ -40,12 +48,12 @@ final class ShowBenchmarkCommand: Command { print("\(result.name) => \(result.input), iterations = \(result.iterCount)") - if let otherResults = otherResults?[resultId] { - if otherResults.count > 1 { + if let baseResults = baseResults?[resultId] { + if baseResults.count > 1 { print("WARNING: There is more than one result with the same id=\(resultId) in the file \(self.comparePath!)") print("Comparing with the first one...\n") } - let other = otherResults.first! + let other = baseResults.first! print("NEW: average = \(benchmark.format(result.avg)), standard deviation = \(benchmark.format(result.std))") print("BASE: average = \(benchmark.format(other.avg)), standard deviation = \(benchmark.format(other.std))") result.printComparison(with: other) From 899cf8a815a32424dcf15a019aca24f1c23d3dd3 Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Thu, 6 Oct 2022 15:14:04 +0300 Subject: [PATCH 12/42] [swcomp] Add --description option to the benchmark run command --- Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift b/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift index d0ef7ee1..96098ee9 100644 --- a/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift +++ b/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift @@ -26,6 +26,9 @@ final class RunBenchmarkCommand: Command { @Key("-c", "--compare", description: "Compares the results with other results saved in the specified file") var comparePath: String? + @Key("-d", "--description", description: "Add a custom description when saving results") + var description: String? + @Flag("-W", "--no-warmup", description: "Disables warmup iteration") var noWarmup: Bool @@ -104,7 +107,7 @@ final class RunBenchmarkCommand: Command { } if let savePath = self.savePath { - let saveFile = try SaveFile(nil, results) + let saveFile = try SaveFile(self.description, results) let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted From f47d5418824013cdad7a190f4c2280e78750626d Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Mon, 10 Oct 2022 17:37:56 +0300 Subject: [PATCH 13/42] [swcomp] Introduce a new, more flexible version of the save file format It features a separate struct for results metadata and also allows multi-way comparisons with previously saved results. --- .../swcomp/Benchmarks/BenchmarkMetadata.swift | 81 +++++++++++++++++++ .../Benchmarks/RunBenchmarkCommand.swift | 45 +++++++---- Sources/swcomp/Benchmarks/SaveFile.swift | 75 ++--------------- .../Benchmarks/ShowBenchmarkCommand.swift | 81 +++++++++++-------- 4 files changed, 164 insertions(+), 118 deletions(-) create mode 100644 Sources/swcomp/Benchmarks/BenchmarkMetadata.swift diff --git a/Sources/swcomp/Benchmarks/BenchmarkMetadata.swift b/Sources/swcomp/Benchmarks/BenchmarkMetadata.swift new file mode 100644 index 00000000..22af7815 --- /dev/null +++ b/Sources/swcomp/Benchmarks/BenchmarkMetadata.swift @@ -0,0 +1,81 @@ +// Copyright (c) 2022 Timofey Solomko +// Licensed under MIT License +// +// See LICENSE for license information + +import Foundation + +struct BenchmarkMetadata: Codable { + + 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?) throws { + self.timestamp = Date.timeIntervalSinceReferenceDate + 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)") + Swift.print("Timestamp: " + + DateFormatter.localizedString(from: Date(timeIntervalSinceReferenceDate: self.timestamp), + dateStyle: .short, timeStyle: .short)) + if let description = self.description { + Swift.print("Description: \(description)") + } + Swift.print() + } + +} diff --git a/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift b/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift index 96098ee9..a27fca11 100644 --- a/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift +++ b/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift @@ -39,20 +39,33 @@ final class RunBenchmarkCommand: Command { guard self.iterationCount == nil || self.iterationCount! >= 1 else { swcompExit(.benchmarkSmallIterCount) } - var results = [BenchmarkResult]() - var baseResults: [String: [BenchmarkResult]]? = nil + var baseResults = [String: [(BenchmarkResult, UUID)]]() + var baseMetadatas = [UUID: String]() if let comparePath = comparePath { let baseSaveFile = try SaveFile.load(from: comparePath) - print("BASE Metadata") - print("-------------") - baseSaveFile.printMetadata() - baseResults = baseSaveFile.groupedResults + + baseMetadatas = Dictionary(uniqueKeysWithValues: zip(baseSaveFile.metadatas.keys, (1...baseSaveFile.metadatas.count).map { String($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) @@ -89,25 +102,25 @@ final class RunBenchmarkCommand: Command { let result = BenchmarkResult(name: self.selectedBenchmark.rawValue, input: input, iterCount: iterationCount, avg: avg, std: std) - if let baseResults = baseResults?[result.id] { - if baseResults.count > 1 { - print("WARNING: There is more than one result with the same id=\(result.id) in the file \(self.comparePath!)") - print("Comparing with the first one...\n") - } - let other = baseResults.first! + if let baseResults = baseResults[result.id] { print("\nNEW: average = \(benchmark.format(avg)), standard deviation = \(benchmark.format(std))") - print("BASE: average = \(benchmark.format(other.avg)), standard deviation = \(benchmark.format(other.std))") - result.printComparison(with: other) + 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))") } - results.append(result) + newResults.append(result) print() } if let savePath = self.savePath { - let saveFile = try SaveFile(self.description, results) + let uuid = UUID() + let metadata = try BenchmarkMetadata(self.description) + let saveFile = SaveFile(metadatas: [uuid: metadata], runs: [SaveFile.Run(metadataUUID: uuid, results: newResults)]) + let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted diff --git a/Sources/swcomp/Benchmarks/SaveFile.swift b/Sources/swcomp/Benchmarks/SaveFile.swift index 27114016..9aa778cc 100644 --- a/Sources/swcomp/Benchmarks/SaveFile.swift +++ b/Sources/swcomp/Benchmarks/SaveFile.swift @@ -7,81 +7,16 @@ import Foundation struct SaveFile: Codable { - var timestamp: TimeInterval - var osInfo: String - var swiftVersion: String - var swcVersion: String - var description: String? - var results: [BenchmarkResult] + struct Run: Codable { - var groupedResults: [String: [BenchmarkResult]] { - return Dictionary(grouping: self.results, by: { $0.id }) - } - - 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 + var metadataUUID: UUID + var results: [BenchmarkResult] - 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 SaveFile.run(command: URL(fileURLWithPath: "/bin/sh"), arguments: args) - #endif - return URL(fileURLWithPath: String(output.dropLast())) - } + var metadatas: [UUID: BenchmarkMetadata] - private static func getOsInfo() throws -> String { - #if os(Linux) - return try SaveFile.run(command: SaveFile.getExecURL(for: "uname"), arguments: ["-a"]) - #else - #if os(Windows) - return "Unknown Windows OS" - #else - return try SaveFile.run(command: SaveFile.getExecURL(for: "sw_vers")) - #endif - #endif - } - - init(_ description: String?, _ results: [BenchmarkResult]) throws { - self.timestamp = Date.timeIntervalSinceReferenceDate - self.osInfo = try SaveFile.getOsInfo() - #if os(Windows) - self.swiftVersion = "Unknown Swift version on Windows" - #else - self.swiftVersion = try SaveFile.run(command: SaveFile.getExecURL(for: "swift"), arguments: ["-version"]) - #endif - self.swcVersion = _SWC_VERSION - self.description = description - self.results = results - } - - func printMetadata() { - print("OS Info: \(self.osInfo)", terminator: "") - print("Swift version: \(self.swiftVersion)", terminator: "") - print("SWC version: \(self.swcVersion)") - print("Timestamp: " + - DateFormatter.localizedString(from: Date(timeIntervalSinceReferenceDate: self.timestamp), - dateStyle: .short, timeStyle: .short)) - if let description = self.description { - print("Description: \(description)") - } - print() - } + var runs: [Run] static func load(from path: String) throws -> SaveFile { let decoder = JSONDecoder() diff --git a/Sources/swcomp/Benchmarks/ShowBenchmarkCommand.swift b/Sources/swcomp/Benchmarks/ShowBenchmarkCommand.swift index 49f40924..eb4376bc 100644 --- a/Sources/swcomp/Benchmarks/ShowBenchmarkCommand.swift +++ b/Sources/swcomp/Benchmarks/ShowBenchmarkCommand.swift @@ -22,46 +22,63 @@ final class ShowBenchmarkCommand: Command { func execute() throws { let newSaveFile = try SaveFile.load(from: self.path) - print("NEW Metadata") - print("------------") - newSaveFile.printMetadata() - let newResults = newSaveFile.groupedResults - var baseResults: [String: [BenchmarkResult]]? = nil + var newMetadatas = Dictionary(uniqueKeysWithValues: zip(newSaveFile.metadatas.keys, (1...newSaveFile.metadatas.count).map { String($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) - print("BASE Metadata") - print("-------------") - baseSaveFile.printMetadata() - baseResults = baseSaveFile.groupedResults - } - for resultId in newResults.keys.sorted() { - let results = newResults[resultId]! - if results.count > 1 { - print("WARNING: There is more than one result with the same id=\(resultId) in the file \(self.path)") - print("Skipped...\n") - continue + baseMetadatas = Dictionary(uniqueKeysWithValues: zip(baseSaveFile.metadatas.keys, (1...baseSaveFile.metadatas.count).map { String($0) })) + if baseMetadatas.count == 1 { + baseMetadatas[baseMetadatas.first!.key] = "" + } + // TODO: The order of printing is potentially non-stable between executions. + for (metadataUUID, index) in baseMetadatas { + print("BASE\(index) Metadata") + print("----------------") + baseSaveFile.metadatas[metadataUUID]!.print() } - let result = results.first! - let benchmark = Benchmarks(rawValue: result.name)?.initialized(result.input) - - print("\(result.name) => \(result.input), iterations = \(result.iterCount)") + for baseRun in baseSaveFile.runs { + baseResults.merge(Dictionary(grouping: baseRun.results.map { ($0, baseRun.metadataUUID) }, by: { $0.0.id }), + uniquingKeysWith: { $0 + $1 }) + } + } - if let baseResults = baseResults?[resultId] { - if baseResults.count > 1 { - print("WARNING: There is more than one result with the same id=\(resultId) in the file \(self.comparePath!)") - print("Comparing with the first one...\n") + 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)") + + if let baseResults = baseResults[resultId] { + print("NEW\(newMetadatas[metadataUUID]!): average = \(benchmark.format(result.avg)), standard deviation = \(benchmark.format(result.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("Average = \(benchmark.format(result.avg)), standard deviation = \(benchmark.format(result.std))") } - let other = baseResults.first! - print("NEW: average = \(benchmark.format(result.avg)), standard deviation = \(benchmark.format(result.std))") - print("BASE: average = \(benchmark.format(other.avg)), standard deviation = \(benchmark.format(other.std))") - result.printComparison(with: other) - } else { - print("Average = \(benchmark.format(result.avg)), standard deviation = \(benchmark.format(result.std))") - } - print() + print() + } } } From f3ca7a7ff524fd32a55f1c7ea8d2573571a5c45d Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Mon, 10 Oct 2022 21:59:25 +0300 Subject: [PATCH 14/42] [swcomp] Print brackets around metadata numbers --- Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift | 2 +- Sources/swcomp/Benchmarks/ShowBenchmarkCommand.swift | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift b/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift index a27fca11..befc39ce 100644 --- a/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift +++ b/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift @@ -44,7 +44,7 @@ final class RunBenchmarkCommand: Command { if let comparePath = comparePath { let baseSaveFile = try SaveFile.load(from: comparePath) - baseMetadatas = Dictionary(uniqueKeysWithValues: zip(baseSaveFile.metadatas.keys, (1...baseSaveFile.metadatas.count).map { String($0) })) + baseMetadatas = Dictionary(uniqueKeysWithValues: zip(baseSaveFile.metadatas.keys, (1...baseSaveFile.metadatas.count).map { "(\($0))" })) if baseMetadatas.count == 1 { baseMetadatas[baseMetadatas.first!.key] = "" } diff --git a/Sources/swcomp/Benchmarks/ShowBenchmarkCommand.swift b/Sources/swcomp/Benchmarks/ShowBenchmarkCommand.swift index eb4376bc..f42742dd 100644 --- a/Sources/swcomp/Benchmarks/ShowBenchmarkCommand.swift +++ b/Sources/swcomp/Benchmarks/ShowBenchmarkCommand.swift @@ -22,7 +22,7 @@ final class ShowBenchmarkCommand: Command { func execute() throws { let newSaveFile = try SaveFile.load(from: self.path) - var newMetadatas = Dictionary(uniqueKeysWithValues: zip(newSaveFile.metadatas.keys, (1...newSaveFile.metadatas.count).map { String($0) })) + var newMetadatas = Dictionary(uniqueKeysWithValues: zip(newSaveFile.metadatas.keys, (1...newSaveFile.metadatas.count).map { "(\($0))" })) if newMetadatas.count == 1 { newMetadatas[newMetadatas.first!.key] = "" } @@ -43,12 +43,11 @@ final class ShowBenchmarkCommand: Command { if let comparePath = comparePath { let baseSaveFile = try SaveFile.load(from: comparePath) - baseMetadatas = Dictionary(uniqueKeysWithValues: zip(baseSaveFile.metadatas.keys, (1...baseSaveFile.metadatas.count).map { String($0) })) + baseMetadatas = Dictionary(uniqueKeysWithValues: zip(baseSaveFile.metadatas.keys, (1...baseSaveFile.metadatas.count).map { "(\($0))" })) if baseMetadatas.count == 1 { baseMetadatas[baseMetadatas.first!.key] = "" } - // TODO: The order of printing is potentially non-stable between executions. - for (metadataUUID, index) in baseMetadatas { + for (metadataUUID, index) in baseMetadatas.sorted(by: { $0.value < $1.value }) { print("BASE\(index) Metadata") print("----------------") baseSaveFile.metadatas[metadataUUID]!.print() @@ -74,7 +73,7 @@ final class ShowBenchmarkCommand: Command { result.printComparison(with: other) } } else { - print("Average = \(benchmark.format(result.avg)), standard deviation = \(benchmark.format(result.std))") + print("NEW\(newMetadatas[metadataUUID]!): average = \(benchmark.format(result.avg)), standard deviation = \(benchmark.format(result.std))") } print() From 708e1bbade2762ea321c3ffcd29d62a8480d7296 Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Mon, 10 Oct 2022 22:00:21 +0300 Subject: [PATCH 15/42] [swcomp] Add --append option to benchmark run --- .../Benchmarks/RunBenchmarkCommand.swift | 25 +++++++++++++++++-- Sources/swcomp/SwcompError.swift | 5 ++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift b/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift index befc39ce..28585daf 100644 --- a/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift +++ b/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift @@ -23,6 +23,9 @@ final class RunBenchmarkCommand: Command { @Key("-s", "--save", description: "Saves the results into the specified file") var savePath: String? + @Flag("-a", "--append", description: "Appends results to a file instead of overwriting it") + var append: Bool + @Key("-c", "--compare", description: "Compares the results with other results saved in the specified file") var comparePath: String? @@ -117,9 +120,27 @@ final class RunBenchmarkCommand: Command { } if let savePath = self.savePath { - let uuid = UUID() let metadata = try BenchmarkMetadata(self.description) - let saveFile = SaveFile(metadatas: [uuid: metadata], runs: [SaveFile.Run(metadataUUID: uuid, results: newResults)]) + 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 + 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 diff --git a/Sources/swcomp/SwcompError.swift b/Sources/swcomp/SwcompError.swift index a9d0407b..671a7c7a 100644 --- a/Sources/swcomp/SwcompError.swift +++ b/Sources/swcomp/SwcompError.swift @@ -18,6 +18,7 @@ enum SwcompError { case benchmarkCannotMeasureBadOutSize(Benchmark.Type) case benchmarkReaderTarNoInputSize(String) case benchmarkCannotGetSubcommandPathWindows + case benchmarkCannotAppendToDirectory case containerSymLinkDestPath(String) case containerNoEntryData(String) case containerOutPathExistsNotDir @@ -50,6 +51,8 @@ enum SwcompError { return 205 case .benchmarkCannotGetSubcommandPathWindows: return 206 + case .benchmarkCannotAppendToDirectory: + return 207 case .containerSymLinkDestPath: return 301 case .containerNoEntryData: @@ -91,6 +94,8 @@ enum SwcompError { 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 .containerNoEntryData(let entryName): From 9a948f85845273ed510b05b2c1659bdc3dc696de Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Tue, 11 Oct 2022 10:38:17 +0300 Subject: [PATCH 16/42] [swcomp] Make benchmark results timestamp optional by introducing --preserve-timestamp option --- .../swcomp/Benchmarks/BenchmarkMetadata.swift | 16 +++++++++------- .../swcomp/Benchmarks/RunBenchmarkCommand.swift | 13 ++++++++----- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/Sources/swcomp/Benchmarks/BenchmarkMetadata.swift b/Sources/swcomp/Benchmarks/BenchmarkMetadata.swift index 22af7815..e0302c0e 100644 --- a/Sources/swcomp/Benchmarks/BenchmarkMetadata.swift +++ b/Sources/swcomp/Benchmarks/BenchmarkMetadata.swift @@ -5,9 +5,9 @@ import Foundation -struct BenchmarkMetadata: Codable { +struct BenchmarkMetadata: Codable, Equatable { - var timestamp: TimeInterval + var timestamp: TimeInterval? var osInfo: String var swiftVersion: String var swcVersion: String @@ -52,8 +52,8 @@ struct BenchmarkMetadata: Codable { #endif } - init(_ description: String?) throws { - self.timestamp = Date.timeIntervalSinceReferenceDate + 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" @@ -69,9 +69,11 @@ struct BenchmarkMetadata: Codable { Swift.print("OS Info: \(self.osInfo)", terminator: "") Swift.print("Swift version: \(self.swiftVersion)", terminator: "") Swift.print("SWC version: \(self.swcVersion)") - Swift.print("Timestamp: " + - DateFormatter.localizedString(from: Date(timeIntervalSinceReferenceDate: self.timestamp), - dateStyle: .short, timeStyle: .short)) + 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)") } diff --git a/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift b/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift index 28585daf..f28c294a 100644 --- a/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift +++ b/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift @@ -20,18 +20,21 @@ final class RunBenchmarkCommand: Command { @Key("-i", "--iteration-count", description: "Sets the amount of the benchmark iterations") var iterationCount: Int? - @Key("-s", "--save", description: "Saves the results into the specified file") + @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") + @Flag("-a", "--append", description: "Appends results to a file instead of overwriting it when saving results") var append: Bool - @Key("-c", "--compare", description: "Compares the results with other results saved in the specified file") + @Key("-c", "--compare", description: "Compares results with other results saved in the specified file") var comparePath: String? - @Key("-d", "--description", description: "Add a custom description when saving results") + @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 @@ -120,7 +123,7 @@ final class RunBenchmarkCommand: Command { } if let savePath = self.savePath { - let metadata = try BenchmarkMetadata(self.description) + let metadata = try BenchmarkMetadata(self.description, self.preserveTimestamp) var saveFile: SaveFile var isDir = ObjCBool(false) From 5187f34a9ddac26631122c635601243736f883a8 Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Tue, 11 Oct 2022 10:38:43 +0300 Subject: [PATCH 17/42] [swcomp] Reuse saved benchmark metadata if possible --- Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift b/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift index f28c294a..fcd27d88 100644 --- a/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift +++ b/Sources/swcomp/Benchmarks/RunBenchmarkCommand.swift @@ -135,10 +135,14 @@ final class RunBenchmarkCommand: Command { } saveFile = try SaveFile.load(from: savePath) var uuid: UUID - repeat { - uuid = UUID() - } while saveFile.metadatas[uuid] != nil - saveFile.metadatas[uuid] = metadata + 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() From d3143d58b4cccc047bc03a932aac403f1f152a35 Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Tue, 11 Oct 2022 21:14:11 +0300 Subject: [PATCH 18/42] [swcomp] Perform benchmark speed computations using nanosecond time resolution --- Sources/swcomp/Benchmarks/Benchmarks.swift | 56 +++++++++++----------- 1 file changed, 28 insertions(+), 28 deletions(-) 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 From 7cf26da9cb9c9d96aace295f810bfd8b0cc71d3c Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Tue, 11 Oct 2022 21:25:21 +0300 Subject: [PATCH 19/42] [swcomp] Optimize printing in benchmark show --- Sources/swcomp/Benchmarks/ShowBenchmarkCommand.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Sources/swcomp/Benchmarks/ShowBenchmarkCommand.swift b/Sources/swcomp/Benchmarks/ShowBenchmarkCommand.swift index f42742dd..f6513e52 100644 --- a/Sources/swcomp/Benchmarks/ShowBenchmarkCommand.swift +++ b/Sources/swcomp/Benchmarks/ShowBenchmarkCommand.swift @@ -66,14 +66,12 @@ final class ShowBenchmarkCommand: Command { 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] { - print("NEW\(newMetadatas[metadataUUID]!): average = \(benchmark.format(result.avg)), standard deviation = \(benchmark.format(result.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("NEW\(newMetadatas[metadataUUID]!): average = \(benchmark.format(result.avg)), standard deviation = \(benchmark.format(result.std))") } print() From 987111c620487b71e70908c390bd17f5967e1516 Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Wed, 12 Oct 2022 13:13:59 +0300 Subject: [PATCH 20/42] [swcomp] Fix build issues on linux and windows due to absence of autoreleasepool --- Sources/swcomp/Containers/TarCommand.swift | 7 ------- Sources/swcomp/Extensions/TarEntry+Create.swift | 7 +++++++ 2 files changed, 7 insertions(+), 7 deletions(-) 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 From 9d4ad9296f316406c4d5b6f7404f629eb207cb77 Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Wed, 12 Oct 2022 13:15:17 +0300 Subject: [PATCH 21/42] [CI] SPM builds include swcomp on macos and linux --- azure-pipelines.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 20c1105d..0b606032 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -86,7 +86,7 @@ stages: displayName: 'Download BitByteData' - script: ./utils.py ci script-macos displayName: 'Build & Test' - - script: swift build -c release --target SWCompression + - script: swift build -c release displayName: 'Build SPM Release' - job: linux strategy: @@ -116,8 +116,8 @@ stages: - script: | set -e -o xtrace swift --version - swift build --target SWCompression - swift build -c release --target SWCompression # Check Release build just in case. + swift build + swift build -c release # Check Release build just in case. displayName: 'Build SPM Debug & Release' - job: windows strategy: From 5becb3f6dbf3371a0a3ce510de84487b012a914a Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Wed, 12 Oct 2022 13:40:07 +0300 Subject: [PATCH 22/42] utils.py: add a command which automates bumping release version --- utils.py | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/utils.py b/utils.py index 08508205..bd19bd26 100755 --- a/utils.py +++ b/utils.py @@ -62,7 +62,7 @@ def action_cw(args): _sprun(["rm", "-f", "docs.json"]) _sprun(["rm", "-f", "Package.resolved"]) _sprun(["rm", "-f", "SWCompression.framework.zip"]) - + def action_dbm(args): print("=> 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) From ae6a99ff153e06976b92e56fed34fb24b64fd880 Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Wed, 12 Oct 2022 14:18:54 +0300 Subject: [PATCH 23/42] Add minimum deployment version for Darwin platforms in SPM manifest --- Package.swift | 6 ++++++ 1 file changed, 6 insertions(+) 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", From f9297d96a119ea752c6996274525b2c91f98d8d0 Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Fri, 14 Oct 2022 19:30:39 +0300 Subject: [PATCH 24/42] [GZip] Reject non byte-aligned members The reasoning here is that GZip format is "byte-oriented" and not "bit-oriented", so seemingly GZip members (headers) that begin not on a byte boundary should be disallowed. --- Sources/GZip/GzipArchive.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/GZip/GzipArchive.swift b/Sources/GZip/GzipArchive.swift index 0cafe099..59aa6305 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) From c5362651dd4da1237b4a9af47b03ff59e542662c Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Fri, 14 Oct 2022 19:32:23 +0300 Subject: [PATCH 25/42] [GZip] Change GzipHeader properties to var instead of let --- Sources/GZip/GzipHeader.swift | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/Sources/GZip/GzipHeader.swift b/Sources/GZip/GzipHeader.swift index c5869288..e0629b79 100644 --- a/Sources/GZip/GzipHeader.swift +++ b/Sources/GZip/GzipHeader.swift @@ -24,25 +24,25 @@ 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 /** Initializes the structure with the values from the first 'member' of GZip `archive`. @@ -64,12 +64,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 @@ -109,7 +111,7 @@ public struct GzipHeader { } } - // Some archives may contain source file name (this part ends with zero byte) + // Some archives may contain source file name (this part ends with a zero byte) if flags.contains(.fname) { var fnameBytes: [UInt8] = [] while true { @@ -139,9 +141,10 @@ public struct GzipHeader { // Some archives may contain 2-bytes checksum if flags.contains(.fhcrc) { - // Note: it is not actual CRC-16, it is just two least significant bytes of CRC-32. + // It is not an actual CRC-16, it's just two least significant bytes of CRC-32. let crc16 = reader.uint16() - guard CheckSums.crc32(headerBytes) & 0xFFFF == crc16 else { throw GzipError.wrongHeaderCRC } + guard CheckSums.crc32(headerBytes) & 0xFFFF == crc16 + else { throw GzipError.wrongHeaderCRC } } } From 241e523ba7dec7bd8a311b244d86881c4526f486 Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Fri, 14 Oct 2022 19:57:20 +0300 Subject: [PATCH 26/42] [GZip] Add a struct that represents header's extra field --- SWCompression.xcodeproj/project.pbxproj | 4 ++++ Sources/GZip/GzipHeader+ExtraField.swift | 29 ++++++++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 Sources/GZip/GzipHeader+ExtraField.swift diff --git a/SWCompression.xcodeproj/project.pbxproj b/SWCompression.xcodeproj/project.pbxproj index befe7713..2e1b090c 100644 --- a/SWCompression.xcodeproj/project.pbxproj +++ b/SWCompression.xcodeproj/project.pbxproj @@ -213,6 +213,7 @@ 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 */; }; + 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 */; }; @@ -497,6 +498,7 @@ 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 = ""; }; + 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 = ""; }; @@ -714,6 +716,7 @@ children = ( 063364E21DC52979007E313F /* GzipArchive.swift */, 061C062A1F0E8A1D00832F0C /* GzipHeader.swift */, + E6023B1C28F9C92200D6F3DC /* GzipHeader+ExtraField.swift */, 061C06251F0E8A0300832F0C /* GzipError.swift */, 0686A63F1FA4C18800E89C9E /* FileSystemType+Gzip.swift */, ); @@ -1406,6 +1409,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 */, 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 + } + + } + +} From 03e23231fa85fd15cc1fb1c46110a328374e4a14 Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Fri, 14 Oct 2022 19:59:18 +0300 Subject: [PATCH 27/42] [GZip] Add processing of extra fields in GzipHeader --- Sources/GZip/GzipHeader.swift | 37 ++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/Sources/GZip/GzipHeader.swift b/Sources/GZip/GzipHeader.swift index e0629b79..0bfb0143 100644 --- a/Sources/GZip/GzipHeader.swift +++ b/Sources/GZip/GzipHeader.swift @@ -44,6 +44,14 @@ public struct GzipHeader { /// True, if file is likely to be text file or ASCII-file. 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`. @@ -98,7 +106,8 @@ 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) { var xlen = 0 for i in 0..<2 { @@ -106,6 +115,32 @@ public struct GzipHeader { xlen |= byte.toInt() << (8 * i) headerBytes.append(byte) } + while xlen > 0 { + 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 + var extraFieldBytes = [UInt8]() + for _ in 0.. Date: Fri, 14 Oct 2022 20:02:02 +0300 Subject: [PATCH 28/42] [Tests] Add tests for gzip header's extra fields and other optional format features --- SWCompression.xcodeproj/project.pbxproj | 4 ++++ Tests/GzipTests.swift | 18 ++++++++++++++++++ Tests/Test Files | 2 +- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/SWCompression.xcodeproj/project.pbxproj b/SWCompression.xcodeproj/project.pbxproj index 2e1b090c..fa4acdb1 100644 --- a/SWCompression.xcodeproj/project.pbxproj +++ b/SWCompression.xcodeproj/project.pbxproj @@ -213,6 +213,7 @@ 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 */; }; @@ -498,6 +499,7 @@ 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 = ""; }; @@ -878,6 +880,7 @@ 06F065BF1FFB763300312A82 /* test2.gz */, 06F065BC1FFB763300312A82 /* test3.gz */, 06F065BB1FFB763300312A82 /* test4.gz */, + E6023B1A28F9B60000D6F3DC /* test4_extra_field.gz */, 06F065B91FFB763300312A82 /* test5.gz */, 06F065BE1FFB763300312A82 /* test6.gz */, 06F065BD1FFB763300312A82 /* test7.gz */, @@ -1281,6 +1284,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 */, diff --git a/Tests/GzipTests.swift b/Tests/GzipTests.swift index 203c47fe..7a4b08ba 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 { @@ -53,6 +54,7 @@ class GzipTests: XCTestCase { XCTAssertEqual(testGzipHeader.fileName, "\(testName).answer") XCTAssertEqual(testGzipHeader.comment, "some file comment") XCTAssertTrue(testGzipHeader.isTextFile) + XCTAssertTrue(testGzipHeader.extraFields.isEmpty) // Test output GZip archive content. let decompressedData = try GzipArchive.unarchive(archive: archiveData) @@ -80,6 +82,22 @@ class GzipTests: XCTestCase { try self.unarchive(test: "test4") } + func testGzip4ExtraField() throws { + let testData = try Constants.data(forTest: "test4_extra_field", withType: GzipTests.testType) + let testGzipHeader = try GzipHeader(archive: testData) + + XCTAssertEqual(testGzipHeader.compressionMethod, .deflate) + XCTAssertEqual(testGzipHeader.modificationTime?.timeIntervalSince1970, 1665760462) + XCTAssertEqual(testGzipHeader.osType, .macintosh) + XCTAssertEqual(testGzipHeader.fileName, "test4.answer") + XCTAssertEqual(testGzipHeader.comment, "some file comment") + XCTAssertTrue(testGzipHeader.isTextFile) + XCTAssertEqual(testGzipHeader.extraFields.count, 1) + XCTAssertEqual(testGzipHeader.extraFields[0].si1, 0x54) + XCTAssertEqual(testGzipHeader.extraFields[0].si2, 0x53) + XCTAssertEqual(testGzipHeader.extraFields[0].bytes, [0x12, 0x34, 0x56, 0x78]) + } + func testGzip5() throws { try self.header(test: "test5", mtime: 1482698242) try self.unarchive(test: "test5") diff --git a/Tests/Test Files b/Tests/Test Files index fd8ea9a9..0bfa6825 160000 --- a/Tests/Test Files +++ b/Tests/Test Files @@ -1 +1 @@ -Subproject commit fd8ea9a9d415e01e66f464edf4212e51fe07d9c2 +Subproject commit 0bfa6825ac41cf37c561005275faeb4339c22e58 From dafd474b9683d42aa32711291340e766950e0c4d Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Fri, 14 Oct 2022 20:23:19 +0300 Subject: [PATCH 29/42] [GZip] Add a new parameter with a default value to GzipArchive.archive which allows to set any extra fields --- Sources/GZip/GzipArchive.swift | 37 +++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/Sources/GZip/GzipArchive.swift b/Sources/GZip/GzipArchive.swift index 59aa6305..209cb7de 100644 --- a/Sources/GZip/GzipArchive.swift +++ b/Sources/GZip/GzipArchive.swift @@ -111,14 +111,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() @@ -147,6 +153,10 @@ public class GzipArchive: Archive { } } + if !extraFields.isEmpty { + flags |= 1 << 2 + } + if writeHeaderCRC { flags |= 1 << 1 } @@ -168,7 +178,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]) @@ -176,6 +186,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) From d0479a19e79246a8f17fb6596675ee8e3ac75c0a Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Fri, 14 Oct 2022 20:26:04 +0300 Subject: [PATCH 30/42] [Tests] Change gzip archive test to also generate a random extra field --- Tests/GzipTests.swift | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/Tests/GzipTests.swift b/Tests/GzipTests.swift index 7a4b08ba..22a1bf69 100644 --- a/Tests/GzipTests.swift +++ b/Tests/GzipTests.swift @@ -37,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.. Date: Fri, 21 Oct 2022 18:57:57 +0300 Subject: [PATCH 31/42] [Tests] Expand extra field in extra field GZip test In addition, prevent potential crashes in tests when an extra field incorrectly parsed. --- Tests/GzipTests.swift | 15 +++++++++------ Tests/Test Files | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Tests/GzipTests.swift b/Tests/GzipTests.swift index 22a1bf69..d2a0967c 100644 --- a/Tests/GzipTests.swift +++ b/Tests/GzipTests.swift @@ -63,9 +63,9 @@ class GzipTests: XCTestCase { XCTAssertEqual(testGzipHeader.comment, "some file comment") XCTAssertTrue(testGzipHeader.isTextFile) XCTAssertEqual(testGzipHeader.extraFields.count, 1) - XCTAssertEqual(testGzipHeader.extraFields[0].si1, si1) - XCTAssertEqual(testGzipHeader.extraFields[0].si2, si2) - XCTAssertEqual(testGzipHeader.extraFields[0].bytes, extraFieldBytes) + XCTAssertEqual(testGzipHeader.extraFields.first?.si1, si1) + XCTAssertEqual(testGzipHeader.extraFields.first?.si2, si2) + XCTAssertEqual(testGzipHeader.extraFields.first?.bytes, extraFieldBytes) // Test output GZip archive content. let decompressedData = try GzipArchive.unarchive(archive: archiveData) @@ -104,9 +104,12 @@ class GzipTests: XCTestCase { XCTAssertEqual(testGzipHeader.comment, "some file comment") XCTAssertTrue(testGzipHeader.isTextFile) XCTAssertEqual(testGzipHeader.extraFields.count, 1) - XCTAssertEqual(testGzipHeader.extraFields[0].si1, 0x54) - XCTAssertEqual(testGzipHeader.extraFields[0].si2, 0x53) - XCTAssertEqual(testGzipHeader.extraFields[0].bytes, [0x12, 0x34, 0x56, 0x78]) + XCTAssertEqual(testGzipHeader.extraFields.first?.si1, 0x54) + XCTAssertEqual(testGzipHeader.extraFields.first?.si2, 0x53) + XCTAssertEqual(testGzipHeader.extraFields.first?.bytes, [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, + 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22, 0x33, + 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, + 0xDD, 0xEE, 0xFF]) } func testGzip5() throws { diff --git a/Tests/Test Files b/Tests/Test Files index 0bfa6825..eb6c1830 160000 --- a/Tests/Test Files +++ b/Tests/Test Files @@ -1 +1 @@ -Subproject commit 0bfa6825ac41cf37c561005275faeb4339c22e58 +Subproject commit eb6c18304cd05b8e672513b16d87bef318f12277 From 32d6dc8ae6e8e9f30e46f31b1114ab6329270bc3 Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Fri, 21 Oct 2022 19:27:25 +0300 Subject: [PATCH 32/42] [GZip] Improve truncation detection in the footer and optional parts of the header --- Sources/GZip/GzipArchive.swift | 2 ++ Sources/GZip/GzipHeader.swift | 22 +++++++++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/Sources/GZip/GzipArchive.swift b/Sources/GZip/GzipArchive.swift index 209cb7de..af6c2403 100644 --- a/Sources/GZip/GzipArchive.swift +++ b/Sources/GZip/GzipArchive.swift @@ -88,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 diff --git a/Sources/GZip/GzipHeader.swift b/Sources/GZip/GzipHeader.swift index 0bfb0143..b9a7c386 100644 --- a/Sources/GZip/GzipHeader.swift +++ b/Sources/GZip/GzipHeader.swift @@ -109,13 +109,21 @@ public struct GzipHeader { // 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) } + while xlen > 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) @@ -132,6 +140,11 @@ public struct GzipHeader { 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 } From df55c2e4e2af73848e978d974e6f5fc3eefe81e7 Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Fri, 21 Oct 2022 19:27:53 +0300 Subject: [PATCH 33/42] [Tests] Add tests for truncation GZip-specific parts --- Tests/GzipTests.swift | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/Tests/GzipTests.swift b/Tests/GzipTests.swift index d2a0967c..d11d4fd6 100644 --- a/Tests/GzipTests.swift +++ b/Tests/GzipTests.swift @@ -246,6 +246,36 @@ class GzipTests: XCTestCase { } } + func testGzipTruncation() throws { + // In this test we check the handling of truncation inside the optional elements (name, comment, "extra field", + // crc) of a GZip header, as well as in the "checksum" information of the archive (last 8 bytes). The sample + // file used is "test4_extra_field" since it contains a header which utilizes all format features. + let testData = try Constants.data(forTest: "test4_extra_field", withType: GzipTests.testType) + + // We test all possible truncation points since there are very few of them. + // The header takes first 79 bytes. + for truncationIndex in 1..<79 { + var thrownError: Error? = nil + XCTAssertThrowsError(try GzipArchive.unarchive(archive: testData[.. Date: Fri, 21 Oct 2022 19:28:13 +0300 Subject: [PATCH 34/42] [Tests] Correctly check for error type in Deflate truncation testing --- Tests/GzipTests.swift | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Tests/GzipTests.swift b/Tests/GzipTests.swift index d11d4fd6..db02a4f9 100644 --- a/Tests/GzipTests.swift +++ b/Tests/GzipTests.swift @@ -290,8 +290,10 @@ class GzipTests: XCTestCase { var thrownError: Error? = nil XCTAssertThrowsError(try GzipArchive.unarchive(archive: testData[.. Date: Fri, 21 Oct 2022 19:33:33 +0300 Subject: [PATCH 35/42] [TAR] Remove outdated code comment --- Sources/TAR/TarEntryInfo.swift | 8 -------- 1 file changed, 8 deletions(-) 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 From f191db24948393143f5d62b860475aa708bb02e2 Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Fri, 21 Oct 2022 21:27:47 +0300 Subject: [PATCH 36/42] [swcomp] Use saved (relative) paths when extracting symlinks --- Sources/swcomp/Containers/CommonFunctions.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Sources/swcomp/Containers/CommonFunctions.swift b/Sources/swcomp/Containers/CommonFunctions.swift index 39fcd607..a44a395c 100644 --- a/Sources/swcomp/Containers/CommonFunctions.swift +++ b/Sources/swcomp/Containers/CommonFunctions.swift @@ -102,11 +102,10 @@ 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) + try fileManager.createSymbolicLink(atPath: entryFullURL.path, withDestinationPath: destinationPath!) // We cannot apply attributes to symbolic link. return } else if entry.info.type == .regular { From 8c58f1840e8fe3e8271f4e8da59b0476ba127b31 Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Sat, 22 Oct 2022 15:05:53 +0300 Subject: [PATCH 37/42] Add a warning about swcomp to README --- README.md | 2 ++ 1 file changed, 2 insertions(+) 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 From efd67feb07081097fc559ed3efa2236a430ea656 Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Sat, 22 Oct 2022 18:42:44 +0300 Subject: [PATCH 38/42] [TAR] Handle PAX headers with non-utf8 values and values with newline characters For example, Apple's implementation of TAR on macOS produces such containers, which technically do not conform to the (informal) specifications. --- Sources/TAR/TarExtendedHeader.swift | 48 +++++++++++++++++++---------- 1 file changed, 32 insertions(+), 16 deletions(-) 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.. Date: Sat, 22 Oct 2022 18:44:46 +0300 Subject: [PATCH 39/42] [Tests] Add a test for a PAX header with a newline character inside a record value --- SWCompression.xcodeproj/project.pbxproj | 4 ++++ Tests/TarTests.swift | 32 +++++++++++++++++++++++++ Tests/Test Files | 2 +- 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/SWCompression.xcodeproj/project.pbxproj b/SWCompression.xcodeproj/project.pbxproj index fa4acdb1..45674ab7 100644 --- a/SWCompression.xcodeproj/project.pbxproj +++ b/SWCompression.xcodeproj/project.pbxproj @@ -268,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 */; }; @@ -555,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 = ""; }; @@ -959,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 = ""; @@ -1219,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 */, diff --git a/Tests/TarTests.swift b/Tests/TarTests.swift index 5ebcd5f6..ae9f845e 100644 --- a/Tests/TarTests.swift +++ b/Tests/TarTests.swift @@ -69,6 +69,38 @@ class TarTests: XCTestCase { } } + func testPaxRecordNewline() throws { + // In this test we check the handling of a PAX header record with a newline character inside a record value. + let testData = try Constants.data(forTest: "test_pax_record_newline", withType: TarTests.testType) + + XCTAssertEqual(try TarContainer.formatOf(container: testData), .pax) + + let entries = try TarContainer.open(container: testData) + + XCTAssertEqual(entries.count, 1) + XCTAssertEqual(entries.first?.info.name, "test_file") + XCTAssertEqual(entries.first?.data, Data("Hello, weird pax record value!".utf8)) + XCTAssertEqual(entries.first?.info.type, .regular) + XCTAssertEqual(entries.first?.info.size, 30) + XCTAssertNil(entries.first?.info.accessTime) + XCTAssertNil(entries.first?.info.creationTime) + XCTAssertEqual(entries.first?.info.modificationTime, Date(timeIntervalSince1970: 1666443016)) + XCTAssertEqual(entries.first?.info.permissions, Permissions(rawValue: 420)) + XCTAssertEqual(entries.first?.info.ownerID, 501) + XCTAssertEqual(entries.first?.info.groupID, 20) + XCTAssertEqual(entries.first?.info.ownerUserName, "tsolomko") + XCTAssertEqual(entries.first?.info.ownerGroupName, "staff") + XCTAssertEqual(entries.first?.info.deviceMajorNumber, 0) + XCTAssertEqual(entries.first?.info.deviceMinorNumber, 0) + XCTAssertNil(entries.first?.info.charset) + XCTAssertNil(entries.first?.info.comment) + XCTAssertEqual(entries.first?.info.linkName, "") + XCTAssertEqual(entries.first?.info.unknownExtendedHeaderRecords?.count, 3) + XCTAssertEqual(entries.first?.info.unknownExtendedHeaderRecords?["normal1"], "test_record_value1") + XCTAssertEqual(entries.first?.info.unknownExtendedHeaderRecords?["newline"], "test1\ntest2") + XCTAssertEqual(entries.first?.info.unknownExtendedHeaderRecords?["normal2"], "test_record_value2") + } + func testFormats() throws { let formatTestNames = ["test_gnu", "test_oldgnu", "test_pax", "test_ustar", "test_v7"] diff --git a/Tests/Test Files b/Tests/Test Files index eb6c1830..83a2bcad 160000 --- a/Tests/Test Files +++ b/Tests/Test Files @@ -1 +1 @@ -Subproject commit eb6c18304cd05b8e672513b16d87bef318f12277 +Subproject commit 83a2bcadb7efb27c1835a2b1324b1a0617bcf901 From 9263cd32843f139942dc45f28b7d577dc033e05f Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Sat, 22 Oct 2022 19:28:54 +0300 Subject: [PATCH 40/42] [swcomp] Correctly extract hard links from TAR containers Note that creating containers with hard links is still not supported (I do not know how to detect hard links with FileManager). --- Sources/swcomp/Containers/CommonFunctions.swift | 12 +++++++++++- Sources/swcomp/SwcompError.swift | 7 ++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/Sources/swcomp/Containers/CommonFunctions.swift b/Sources/swcomp/Containers/CommonFunctions.swift index a44a395c..0822fb06 100644 --- a/Sources/swcomp/Containers/CommonFunctions.swift +++ b/Sources/swcomp/Containers/CommonFunctions.swift @@ -106,7 +106,17 @@ func writeFile(_ entry: T, _ outputURL: URL, _ verbose: Bool) print("l: \(entryName) -> \(destinationPath!)") } try fileManager.createSymbolicLink(atPath: entryFullURL.path, withDestinationPath: destinationPath!) - // We cannot apply attributes to symbolic link. + // 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/SwcompError.swift b/Sources/swcomp/SwcompError.swift index 671a7c7a..58127595 100644 --- a/Sources/swcomp/SwcompError.swift +++ b/Sources/swcomp/SwcompError.swift @@ -20,6 +20,7 @@ enum SwcompError { case benchmarkCannotGetSubcommandPathWindows case benchmarkCannotAppendToDirectory case containerSymLinkDestPath(String) + case containerHardLinkDestPath(String) case containerNoEntryData(String) case containerOutPathExistsNotDir case fileHandleCannotOpen @@ -55,6 +56,8 @@ enum SwcompError { return 207 case .containerSymLinkDestPath: return 301 + case .containerHardLinkDestPath: + return 311 case .containerNoEntryData: return 302 case .containerOutPathExistsNotDir: @@ -83,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): @@ -98,6 +101,8 @@ enum SwcompError { 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: From d445479b82f40d88e55adff9eb2d84e2b78268e5 Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Sat, 22 Oct 2022 20:04:56 +0300 Subject: [PATCH 41/42] Gitignore API baselines --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) 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/ From c7f4665394dd668c1953817c4567c314eb97fda9 Mon Sep 17 00:00:00 2001 From: Timofey Solomko Date: Sat, 22 Oct 2022 20:06:24 +0300 Subject: [PATCH 42/42] Prepare for 4.8.3 release --- .jazzy.yaml | 4 ++-- CHANGELOG.md | 23 +++++++++++++++++++ SWCompression.podspec | 2 +- SWCompression.xcodeproj/SWCompression.plist | 4 ++-- .../TestSWCompression.plist | 4 ++-- SWCompression.xcodeproj/project.pbxproj | 8 +++---- Sources/swcomp/main.swift | 2 +- 7 files changed, 35 insertions(+), 12 deletions(-) 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/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 45674ab7..b83aea5c 100644 --- a/SWCompression.xcodeproj/project.pbxproj +++ b/SWCompression.xcodeproj/project.pbxproj @@ -1528,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; @@ -1613,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; @@ -1678,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"; @@ -1705,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/swcomp/main.swift b/Sources/swcomp/main.swift index 0c4247f7..e023b90b 100644 --- a/Sources/swcomp/main.swift +++ b/Sources/swcomp/main.swift @@ -7,7 +7,7 @@ import Foundation import SWCompression import SwiftCLI -let _SWC_VERSION = "4.8.2" +let _SWC_VERSION = "4.8.3" let cli = CLI(name: "swcomp", version: _SWC_VERSION, description: """