// Copyright (c) 2026 Timofey Solomko // Licensed under MIT License // // See LICENSE for license information import Foundation import Testing import SWCompression struct GzipTests { private static let testType: String = "gz" func header(test testName: String, mtime: Int) throws { let testData = try Constants.data(forTest: testName, withType: GzipTests.testType) let testGzipHeader = try GzipHeader(archive: testData) #expect(testGzipHeader.compressionMethod == .deflate) #expect(testGzipHeader.modificationTime == Date(timeIntervalSince1970: TimeInterval(mtime))) #expect(testGzipHeader.osType == .unix) #expect(testGzipHeader.fileName == "\(testName).answer") #expect(testGzipHeader.comment == nil) #expect(testGzipHeader.extraFields.isEmpty) } func unarchive(test testName: String) throws { let testData = try Constants.data(forTest: testName, withType: GzipTests.testType) let decompressedData = try GzipArchive.unarchive(archive: testData) let answerData = try Constants.data(forAnswer: testName) #expect(decompressedData == answerData) } func archive(test testName: String) throws { let answerData = try Constants.data(forAnswer: testName) // Options for archiving. 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..=6.1) let error = #expect(throws: GzipError.self) { try GzipArchive.unarchive(archive: testData) } if case let .some(.wrongCRC(members)) = error { try #require(members.count == 1) let answerData = try Constants.data(forAnswer: "test1") #expect(members.first!.data == answerData) } else { Issue.record("Unexpected error: \(error)") } #else #expect { try GzipArchive.unarchive(archive: testData) } throws: { error in if case let .some(.wrongCRC(members)) = error as? GzipError { let answerData = try Constants.data(forAnswer: "test1") return members.count == 1 && members.first!.data == answerData } return false } #endif } @Test func multiUnarchiveChecksumMismatch() throws { // Here we test that an error for checksum mismatch is thrown correctly and its associated value contains // expected data. We do this by programmatically adjusting the input: we change one of the bytes for the checkum, // which makes it incorrect. var testData = try Constants.data(forTest: "test_multi", withType: GzipTests.testType) // Here we modify the stored value of crc32. testData[2289] &+= 1 #if compiler(>=6.1) let error = #expect(throws: GzipError.self) { try GzipArchive.multiUnarchive(archive: testData) } if case let .some(.wrongCRC(members)) = error { try #require(members.count == 2) var answerData = try Constants.data(forAnswer: "test1") #expect(members[0].data == answerData) answerData = try Constants.data(forAnswer: "test2") #expect(members[1].data == answerData) } else { Issue.record("Unexpected error: \(error)") } #else #expect { try GzipArchive.multiUnarchive(archive: testData) } throws: { error in if case let .some(.wrongCRC(members)) = error as? GzipError { let answer1Data = try Constants.data(forAnswer: "test1") let answer2Data = try Constants.data(forAnswer: "test2") return members.count == 2 && members[0].data == answer1Data && members[1].data == answer2Data } return false } #endif } @Test func minimal() throws { // In this test we test several things: // - that the archive consisting only of the minimal header is successfully processed, // - that the mtime field with the value 0 correctly results in a `GzipHeader.modificationTime == nil`, // - that the `GzipArchive.multiUnarchive(archive:)` works on a single member archive. let testData = try Constants.data(forTest: "minimal", withType: GzipTests.testType) let members = try GzipArchive.multiUnarchive(archive: testData) try #require(members.count == 1) if let member = members.first { #expect(member.header.compressionMethod == .deflate) #expect(member.header.modificationTime == nil) #expect(member.header.osType == .unix) #expect(member.header.fileName == nil) #expect(member.header.comment == nil) #expect(!member.header.isTextFile) #expect(member.data == Data()) } } @Test func headerFooterTruncation() 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 { #expect(throws: GzipError.self, "Header truncated at \(truncationIndex)") { try GzipArchive.unarchive(archive: testData[..