Skip to content

Commit

Permalink
test: Add tests for the tar writer (#50)
Browse files Browse the repository at this point in the history
Motivation
----------

The `tar` writer is tested by the end to end tests, but unit tests are
more helpful for refactoring and extending it.

Modifications
-------------
 
* Extract `tar` into its own module
* Extract tar header construction into a separate function
* Add initial unit tests for the helper methods used to build `tar`
headers
* Stop using `Swift(format:)` to convert integers into octal strings
because of
swiftlang/swift-corelibs-foundation#5152

Result
------

The basic helper functions underpinning the tar writer will have unit
tests, making it easier to test, refactor and extend the tar writer in
future.

Test Plan
---------

This pull request adds new tests. All existing tests continue to pass.
  • Loading branch information
euanh authored Jan 6, 2025
1 parent ec08871 commit 9a3e7af
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 9 deletions.
2 changes: 1 addition & 1 deletion .swift-format
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"lineLength" : 120,
"maximumBlankLines" : 1,
"prioritizeKeepingFunctionOutputTogether" : false,
"respectsExistingLineBreaks" : false,
"respectsExistingLineBreaks" : true,
"rules" : {
"AllPublicDeclarationsHaveDocumentation" : true,
"AlwaysUseLowerCamelCase" : false,
Expand Down
5 changes: 3 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ let package = Package(
.executableTarget(
name: "containertool",
dependencies: [
.target(name: "ContainerRegistry"), .target(name: "VendorCNIOExtrasZlib"),
.target(name: "ContainerRegistry"), .target(name: "VendorCNIOExtrasZlib"), .target(name: "Tar"),
.product(name: "ArgumentParser", package: "swift-argument-parser"),
],
swiftSettings: [.swiftLanguageMode(.v5)]
Expand All @@ -51,7 +51,7 @@ let package = Package(
dependencies: [],
path: "Vendor/github.com/apple/swift-nio-extras/Sources/CNIOExtrasZlib",
linkerSettings: [.linkedLibrary("z")]
),
), .target(name: "Tar"),
.target(
// Vendored from https://github.com/apple/swift-package-manager with modifications
name: "Basics",
Expand Down Expand Up @@ -87,6 +87,7 @@ let package = Package(
dependencies: [.target(name: "ContainerRegistry")],
resources: [.process("Resources")]
), .testTarget(name: "containertoolTests", dependencies: [.target(name: "containertool")]),
.testTarget(name: "TarTests", dependencies: [.target(name: "Tar")]),
],
swiftLanguageModes: [.v6]
)
41 changes: 35 additions & 6 deletions Sources/containertool/tar.swift → Sources/Tar/tar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,24 @@ extension [UInt8] {
func octal6(_ value: Int) -> String {
precondition(value >= 0)
precondition(value < 0o777777)
return String(format: "%06o", value)
// String(format: "%06o", value) cannot be used because of a race in Foundation
// which causes it to return an empty string from time to time when running the tests
// in parallel using swift-testing: https://github.com/swiftlang/swift-corelibs-foundation/issues/5152
let str = String(value, radix: 8)
return String(repeating: "0", count: 6 - str.count).appending(str)
}

/// Serializes an integer to a 11 character octal representation.
/// Serializes an integer to an 11 character octal representation.
/// - Parameter value: The integer to serialize.
/// - Returns: The serialized form of `value`.
func octal11(_ value: Int) -> String {
precondition(value >= 0)
precondition(value < 0o777_7777_7777)
return String(format: "%011o", value)
// String(format: "%011o", value) cannot be used because of a race in Foundation
// which causes it to return an empty string from time to time when running the tests
// in parallel using swift-testing: https://github.com/swiftlang/swift-corelibs-foundation/issues/5152
let str = String(value, radix: 8)
return String(repeating: "0", count: 11 - str.count).appending(str)
}

// These ranges define the offsets of the standard fields in a Tar header.
Expand Down Expand Up @@ -143,7 +151,12 @@ let CONTTYPE = "7" // reserved
let XHDTYPE = "x" // Extended header referring to the next file in the archive
let XGLTYPE = "g" // Global extended header

func tar(_ bytes: [UInt8], filename: String = "app") -> [UInt8] {
/// Creates a tar header for a single file
/// - Parameters:
/// - filesize: The size of the file
/// - filename: The file's name in the archive
/// - Returns: A tar header representing the file
public func tarHeader(filesize: Int, filename: String = "app") -> [UInt8] {
// A file entry consists of a file header followed by the
// contents of the file. The header includes information such as
// the file name, size and permissions. Different versions of
Expand All @@ -158,7 +171,7 @@ func tar(_ bytes: [UInt8], filename: String = "app") -> [UInt8] {
hdr.writeString(octal6(0o555), inField: mode, withTermination: .spaceAndNull)
hdr.writeString(octal6(0o000000), inField: uid, withTermination: .spaceAndNull)
hdr.writeString(octal6(0o000000), inField: gid, withTermination: .spaceAndNull)
hdr.writeString(octal11(bytes.count), inField: size, withTermination: .space)
hdr.writeString(octal11(filesize), inField: size, withTermination: .space)
hdr.writeString(octal11(0), inField: mtime, withTermination: .space)
hdr.writeString(INIT_CHECKSUM, inField: chksum, withTermination: .none)
hdr.writeString(REGTYPE, inField: typeflag, withTermination: .none)
Expand All @@ -174,6 +187,17 @@ func tar(_ bytes: [UInt8], filename: String = "app") -> [UInt8] {
// Fill in the checksum.
hdr.writeString(octal6(checksum(header: hdr)), inField: chksum, withTermination: .nullAndSpace)

return hdr
}

/// Creates a tar archive containing a single file
/// - Parameters:
/// - bytes: The file's body data
/// - filename: The file's name in the archive
/// - Returns: A tar archive containing the file
public func tar(_ bytes: [UInt8], filename: String = "app") -> [UInt8] {
var hdr = tarHeader(filesize: bytes.count, filename: filename)

// Append the file data to the header
hdr.append(contentsOf: bytes)

Expand All @@ -187,4 +211,9 @@ func tar(_ bytes: [UInt8], filename: String = "app") -> [UInt8] {
return hdr
}

func tar(_ data: Data, filename: String) -> [UInt8] { tar([UInt8](data), filename: filename) }
/// Creates a tar archive containing a single file
/// - Parameters:
/// - data: The file's body data
/// - filename: The file's name in the archive
/// - Returns: A tar archive containing the file
public func tar(_ data: Data, filename: String) -> [UInt8] { tar([UInt8](data), filename: filename) }
1 change: 1 addition & 0 deletions Sources/containertool/containertool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import ArgumentParser
import Foundation
import ContainerRegistry
import Tar
import Basics

extension Swift.String: Swift.Error {}
Expand Down
78 changes: 78 additions & 0 deletions Tests/TarTests/TarUnitTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftContainerPlugin open source project
//
// Copyright (c) 2024 Apple Inc. and the SwiftContainerPlugin project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Foundation
import Testing

@testable import Tar

let blocksize = 512
let headerLen = blocksize
let trailerLen = 2 * blocksize

@Suite struct TarUnitTests {
@Test(arguments: [
(input: 0o000, expected: "000000"),
(input: 0o555, expected: "000555"),
(input: 0o750, expected: "000750"),
(input: 0o777, expected: "000777"),
(input: 0o1777, expected: "001777"),
])
func testOctal6(input: Int, expected: String) async throws {
#expect(octal6(input) == expected)
}

@Test(arguments: [
(input: 0, expected: "00000000000"),
(input: 1024, expected: "00000002000"),
(input: 0o2000, expected: "00000002000"),
(input: 1024 * 1024, expected: "00004000000"),
])
func testOctal11(input: Int, expected: String) async throws {
#expect(octal11(input) == expected)
}

@Test func testUInt8writeString() async throws {
// Fill the buffer with 0xFF to show null termination
var hdr = [UInt8](repeating: 255, count: 21)

// The typechecker timed out when these test cases were passed as arguments, in the style of the octal tests
hdr.writeString("abc", inField: 0..<5, withTermination: .none)
#expect(
hdr == [
97, 98, 99, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
]
)

hdr.writeString("def", inField: 3..<7, withTermination: .null)
#expect(
hdr == [97, 98, 99, 100, 101, 102, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255]
)

hdr.writeString("ghi", inField: 7..<11, withTermination: .space)
#expect(
hdr == [97, 98, 99, 100, 101, 102, 0, 103, 104, 105, 32, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255]
)

hdr.writeString("jkl", inField: 11..<16, withTermination: .nullAndSpace)
#expect(
hdr == [97, 98, 99, 100, 101, 102, 0, 103, 104, 105, 32, 106, 107, 108, 0, 32, 255, 255, 255, 255, 255]
)

hdr.writeString("mno", inField: 16..<21, withTermination: .spaceAndNull)
#expect(
hdr == [97, 98, 99, 100, 101, 102, 0, 103, 104, 105, 32, 106, 107, 108, 0, 32, 109, 110, 111, 32, 0]
)
}
}

0 comments on commit 9a3e7af

Please sign in to comment.