// Copyright (c) 2026 Timofey Solomko // Licensed under MIT License // // See LICENSE for license information import XCTest import SWCompression class TarReaderTests: XCTestCase { private static let testType: String = "tar" func testBadFile_invalid() throws { // This is potentially a misleading test, since there is no way to guarantee that a file is not a TAR container. // We use randomly generated data, since the 0-filled data is processed as an empty container. let testHandle = try Constants.handle(forTest: "test7", withType: "answer") var reader = TarReader(fileHandle: testHandle) XCTAssertThrowsError(try reader.read()) try testHandle.close() } func test() throws { let testHandle = try Constants.handle(forTest: "test", withType: TarReaderTests.testType) var reader = TarReader(fileHandle: testHandle) var isFinished = false var entriesCount = 0 while !isFinished { isFinished = try reader.process { (entry: TarEntry?) -> Bool in if entry == nil { return true } XCTAssertEqual(entry!.info.name, "test5.answer") XCTAssertEqual(entry!.info.size, 0) XCTAssertEqual(entry!.info.type, .regular) XCTAssertEqual(entry!.info.ownerID, 501) XCTAssertEqual(entry!.info.groupID, 20) XCTAssertEqual(entry!.info.ownerUserName, "timofeysolomko") XCTAssertEqual(entry!.info.ownerGroupName, "staff") XCTAssertEqual(entry!.info.permissions, Permissions(rawValue: 420)) XCTAssertNil(entry!.info.comment) XCTAssertEqual(entry!.data, Data()) entriesCount += 1 return false } } XCTAssertEqual(entriesCount, 1) try testHandle.close() } func testPax() throws { let testHandle = try Constants.handle(forTest: "full_test", withType: TarReaderTests.testType) var reader = TarReader(fileHandle: testHandle) var isFinished = false var entriesCount = 0 while !isFinished { isFinished = try reader.process { (entry: TarEntry?) -> Bool in if entry == nil { return true } let name = entry!.info.name.components(separatedBy: ".")[0] let answerData = try Constants.data(forAnswer: name) XCTAssertEqual(entry!.data, answerData) XCTAssertEqual(entry!.info.type, .regular) XCTAssertEqual(entry!.info.ownerUserName, "tsolomko") XCTAssertEqual(entry!.info.ownerGroupName, "tsolomko") XCTAssertEqual(entry!.info.ownerID, 1001) XCTAssertEqual(entry!.info.groupID, 1001) XCTAssertEqual(entry!.info.permissions, Permissions(rawValue: 436)) XCTAssertNil(entry!.info.comment) // Checking times' values is a bit difficult since they are extremely precise. XCTAssertNotNil(entry!.info.modificationTime) XCTAssertNotNil(entry!.info.accessTime) XCTAssertNotNil(entry!.info.creationTime) entriesCount += 1 return false } } XCTAssertEqual(entriesCount, 5) try testHandle.close() } func testFormats() throws { let formatTestNames = ["test_gnu", "test_oldgnu", "test_pax", "test_ustar", "test_v7"] let answerData = try Constants.data(forAnswer: "test1") for testName in formatTestNames { let testHandle = try Constants.handle(forTest: testName, withType: TarReaderTests.testType) var reader = TarReader(fileHandle: testHandle) var isFinished = false var entriesCount = 0 while !isFinished { isFinished = try reader.process { (entry: TarEntry?) -> Bool in if entry == nil { return true } XCTAssertEqual(entry!.info.name, "test1.answer") XCTAssertEqual(entry!.info.size, 14) XCTAssertEqual(entry!.info.type, .regular) XCTAssertEqual(entry!.data, answerData) entriesCount += 1 return false } } XCTAssertEqual(entriesCount, 1) XCTAssertNil(try reader.read()) try testHandle.close() } } func testLongNames() throws { let formatTestNames = ["long_test_gnu", "long_test_oldgnu", "long_test_pax"] for testName in formatTestNames { let testHandle = try Constants.handle(forTest: testName, withType: TarReaderTests.testType) var reader = TarReader(fileHandle: testHandle) var isFinished = false var entriesCount = 0 while !isFinished { isFinished = try reader.process { (entry: TarEntry?) -> Bool in if entry == nil { return true } entriesCount += 1 return false } } XCTAssertEqual(entriesCount, 6) XCTAssertNil(try reader.read()) try testHandle.close() } } func testWinContainer() throws { let testHandle = try Constants.handle(forTest: "test_win", withType: TarReaderTests.testType) var reader = TarReader(fileHandle: testHandle) try reader.process { (entry: TarEntry?) in XCTAssertNotNil(entry) XCTAssertEqual(entry!.info.name, "dir/") XCTAssertEqual(entry!.info.type, .directory) XCTAssertEqual(entry!.info.size, 0) XCTAssertEqual(entry!.info.ownerUserName, "") XCTAssertEqual(entry!.info.ownerGroupName, "") XCTAssertEqual(entry!.info.ownerID, 0) XCTAssertEqual(entry!.info.groupID, 0) XCTAssertEqual(entry!.info.permissions, Permissions(rawValue: 511)) XCTAssertNil(entry!.info.comment) XCTAssertEqual(entry!.data, nil) } try reader.process { (entry: TarEntry?) in XCTAssertNotNil(entry) XCTAssertEqual(entry!.info.name, "text_win.txt") XCTAssertEqual(entry!.info.type, .regular) XCTAssertEqual(entry!.info.size, 15) XCTAssertEqual(entry!.info.ownerUserName, "") XCTAssertEqual(entry!.info.ownerGroupName, "") XCTAssertEqual(entry!.info.ownerID, 0) XCTAssertEqual(entry!.info.groupID, 0) XCTAssertEqual(entry!.info.permissions, Permissions(rawValue: 511)) XCTAssertNil(entry!.info.comment) XCTAssertEqual(entry!.data, "Hello, Windows!".data(using: .utf8)) } XCTAssertNil(try reader.read()) try testHandle.close() } func testEmptyFile() throws { let testHandle = try Constants.handle(forTest: "test_empty_file", withType: TarReaderTests.testType) var reader = TarReader(fileHandle: testHandle) try reader.process { (entry: TarEntry?) in XCTAssertNotNil(entry) XCTAssertEqual(entry!.info.name, "empty_file") XCTAssertEqual(entry!.info.type, .regular) XCTAssertEqual(entry!.info.size, 0) XCTAssertEqual(entry!.info.ownerID, 501) XCTAssertEqual(entry!.info.groupID, 20) XCTAssertEqual(entry!.info.ownerUserName, "timofeysolomko") XCTAssertEqual(entry!.info.ownerGroupName, "staff") XCTAssertEqual(entry!.info.permissions, Permissions(rawValue: 420)) XCTAssertNil(entry!.info.comment) XCTAssertEqual(entry!.data, Data()) } XCTAssertNil(try reader.read()) try testHandle.close() } func testEmptyDirectory() throws { let testHandle = try Constants.handle(forTest: "test_empty_dir", withType: TarReaderTests.testType) var reader = TarReader(fileHandle: testHandle) try reader.process { (entry: TarEntry?) in XCTAssertNotNil(entry) XCTAssertEqual(entry!.info.name, "empty_dir/") XCTAssertEqual(entry!.info.type, .directory) XCTAssertEqual(entry!.info.size, 0) XCTAssertEqual(entry!.info.ownerID, 501) XCTAssertEqual(entry!.info.groupID, 20) XCTAssertEqual(entry!.info.ownerUserName, "timofeysolomko") XCTAssertEqual(entry!.info.ownerGroupName, "staff") XCTAssertEqual(entry!.info.permissions, Permissions(rawValue: 493)) XCTAssertNil(entry!.info.comment) XCTAssertNil(entry!.data) } XCTAssertNil(try reader.read()) try testHandle.close() } func testOnlyDirectoryHeader() throws { // This tests the correct handling of the situation when there is nothing in the container but one basic header, // even no EOF marker (two blocks of zeros). let testHandle = try Constants.handle(forTest: "test_only_dir_header", withType: TarReaderTests.testType) var reader = TarReader(fileHandle: testHandle) try reader.process { (entry: TarEntry?) in XCTAssertNotNil(entry) XCTAssertEqual(entry!.info.name, "empty_dir/") XCTAssertEqual(entry!.info.type, .directory) XCTAssertEqual(entry!.info.size, 0) XCTAssertEqual(entry!.info.ownerID, 501) XCTAssertEqual(entry!.info.groupID, 20) XCTAssertEqual(entry!.info.ownerUserName, "timofeysolomko") XCTAssertEqual(entry!.info.ownerGroupName, "staff") XCTAssertEqual(entry!.info.permissions, Permissions(rawValue: 493)) XCTAssertNil(entry!.info.comment) XCTAssertNil(entry!.data) } XCTAssertNil(try reader.read()) try testHandle.close() } func testEmptyContainer() throws { let testHandle = try Constants.handle(forTest: "test_empty_cont", withType: TarReaderTests.testType) var reader = TarReader(fileHandle: testHandle) XCTAssertNil(try reader.read()) try testHandle.close() } func testBigContainer() throws { let testHandle = try Constants.handle(forTest: "SWCompressionSourceCode", withType: TarReaderTests.testType) var reader = TarReader(fileHandle: testHandle) while try reader.read() != nil { } try testHandle.close() } func testUnicodeUstar() throws { let testHandle = try Constants.handle(forTest: "test_unicode_ustar", withType: TarReaderTests.testType) var reader = TarReader(fileHandle: testHandle) try reader.process { (entry: TarEntry?) in XCTAssertNotNil(entry) XCTAssertEqual(entry!.info.name, "текстовый файл.answer") XCTAssertEqual(entry!.info.type, .regular) XCTAssertEqual(entry!.info.ownerID, 501) XCTAssertEqual(entry!.info.groupID, 20) XCTAssertEqual(entry!.info.ownerUserName, "timofeysolomko") XCTAssertEqual(entry!.info.ownerGroupName, "staff") XCTAssertEqual(entry!.info.permissions, Permissions(rawValue: 420)) XCTAssertNil(entry!.info.comment) XCTAssertEqual(entry!.data, Constants.текстовыйФайлData) } XCTAssertNil(try reader.read()) try testHandle.close() } func testUnicodePax() throws { let testHandle = try Constants.handle(forTest: "test_unicode_pax", withType: TarReaderTests.testType) var reader = TarReader(fileHandle: testHandle) try reader.process { (entry: TarEntry?) in XCTAssertNotNil(entry) XCTAssertEqual(entry!.info.name, "текстовый файл.answer") XCTAssertEqual(entry!.info.type, .regular) XCTAssertEqual(entry!.info.ownerID, 501) XCTAssertEqual(entry!.info.groupID, 20) XCTAssertEqual(entry!.info.ownerUserName, "timofeysolomko") XCTAssertEqual(entry!.info.ownerGroupName, "staff") XCTAssertEqual(entry!.info.permissions, Permissions(rawValue: 420)) XCTAssertNil(entry!.info.comment) XCTAssertEqual(entry!.data, Constants.текстовыйФайлData) } XCTAssertNil(try reader.read()) try testHandle.close() } func testGnuIncrementalFormat() throws { let testHandle = try Constants.handle(forTest: "test_gnu_inc_format", withType: TarReaderTests.testType) var reader = TarReader(fileHandle: testHandle) var isFinished = false var entriesCount = 0 while !isFinished { isFinished = try reader.process { (entry: TarEntry?) -> Bool in if entry == nil { return true } XCTAssertEqual(entry!.info.ownerID, 501) XCTAssertEqual(entry!.info.groupID, 20) XCTAssertEqual(entry!.info.ownerUserName, "timofeysolomko") XCTAssertEqual(entry!.info.ownerGroupName, "staff") XCTAssertNotNil(entry!.info.accessTime) XCTAssertNotNil(entry!.info.creationTime) entriesCount += 1 return false } } XCTAssertEqual(entriesCount, 3) XCTAssertNil(try reader.read()) try testHandle.close() } // This test is impossible to implement using TarReader since the test file doesn't contain actual entry data. // func testBigNumField() throws { } func testNegativeMtime() throws { let testHandle = try Constants.handle(forTest: "test_negative_mtime", withType: TarReaderTests.testType) var reader = TarReader(fileHandle: testHandle) try reader.process { (entry: TarEntry?) in XCTAssertEqual(entry!.info.name, "file") XCTAssertEqual(entry!.info.type, .regular) XCTAssertEqual(entry!.info.size, 27) XCTAssertEqual(entry!.info.ownerID, 501) XCTAssertEqual(entry!.info.groupID, 20) XCTAssertEqual(entry!.info.ownerUserName, "timofeysolomko") XCTAssertEqual(entry!.info.ownerGroupName, "staff") XCTAssertEqual(entry!.info.permissions, Permissions(rawValue: 420)) XCTAssertEqual(entry!.info.modificationTime, Date(timeIntervalSince1970: -313006414)) XCTAssertNil(entry!.info.comment) XCTAssertEqual(entry!.data, "File with negative mtime.\n\n".data(using: .utf8)) } // Test that reading after reaching EOF returns nil. XCTAssertNil(try reader.read()) try testHandle.close() } }