Skip to content

Commit 4590771

Browse files
authored
tests: Add interoperability tests using the system's tar (#70)
Motivation ---------- Tar archives must be read by a variety of other tar implementations. As the archives produced by `containertool` become more complex, we need interoperability tests to make sure they still comply with the expectations of readers. Modifications ------------- * Add a new TarInteropTests suite which generates a variety of tar archives and verifies that BSD tar can read them. * Add tests which demonstrate rules of the tar format and show how BSD tar deal with corrupt archives Result ------ The output of the tar writer will be tested to ensure that BSD tar can read it. Test Plan --------- * Added new interoperability tests * Existing tests continue to pass
1 parent 2d0a85c commit 4590771

File tree

2 files changed

+333
-3
lines changed

2 files changed

+333
-3
lines changed

.github/workflows/pull_request.yml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ jobs:
2121
with:
2222
linux_5_9_enabled: false
2323
linux_5_10_enabled: false
24-
linux_6_0_arguments_override: "--skip SmokeTests"
25-
linux_nightly_6_1_arguments_override: "--skip SmokeTests"
26-
linux_nightly_main_arguments_override: "--skip SmokeTests"
24+
linux_6_0_arguments_override: "--skip SmokeTests --skip TarInteropTests"
25+
linux_nightly_6_1_arguments_override: "--skip SmokeTests --skip TarInteropTests"
26+
linux_nightly_main_arguments_override: "--skip SmokeTests --skip TarInteropTests"
2727

2828
# Test functions and modules against an separate registry
2929
integration-tests:
@@ -46,6 +46,10 @@ jobs:
4646
# https://github.com/actions/checkout/issues/766
4747
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
4848

49+
- name: Install bsdtar
50+
run: |
51+
which bsdtar || (apt-get -q update && apt-get -yq install libarchive-tools)
52+
4953
- name: Run test job
5054
env:
5155
REGISTRY_HOST: registry
Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftContainerPlugin open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the SwiftContainerPlugin project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Foundation
16+
import Testing
17+
@testable import Tar
18+
19+
@Suite struct TarInteropTests {
20+
// Use the system `tar` program to read the contents of a tar archive.
21+
// - Parameters input: A stream of bytes to be interpreted as a tar archive
22+
// - Returns: The output of the `tar` program.
23+
func tarListContents(_ input: [UInt8]) async throws -> String {
24+
let inPipe = Pipe()
25+
let outPipe = Pipe()
26+
27+
try inPipe.fileHandleForWriting.write(contentsOf: input)
28+
inPipe.fileHandleForWriting.closeFile()
29+
30+
let p = Process()
31+
p.executableURL = .init(fileURLWithPath: "/usr/bin/env")
32+
p.environment = ["LC_ALL": "C"] // Avoid locale-specific differences in output formatting
33+
p.arguments = ["bsdtar", "-t", "-v", "-f", "-"]
34+
p.standardInput = inPipe
35+
p.standardOutput = outPipe
36+
try p.run()
37+
38+
// bsdtar console listing includes a trailing newline
39+
let output = try #require(try outPipe.fileHandleForReading.readToEnd())
40+
return String(decoding: output, as: UTF8.self).trimmingCharacters(in: .whitespacesAndNewlines)
41+
}
42+
43+
@Test func testSingle4BFile() async throws {
44+
let data = "test"
45+
let result = try tar([UInt8](data.utf8), filename: "filename")
46+
#expect(result.count == headerLen + blocksize + trailerLen)
47+
48+
let output = try await tarListContents(result)
49+
#expect(output == "-r-xr-xr-x 0 0 0 4 Jan 1 1970 filename")
50+
}
51+
52+
@Test func testSingleEmptyFile() async throws {
53+
// An empty tar file created by bsd tar consumes 1536 bytes:
54+
// % tar -c --numeric-owner -f empty.tar empty
55+
// % cat empty.tar
56+
// empty000644 000765 000000 00000000000 14737460101 010320 0ustar00000000 000000 %
57+
// % wc -c empty.tar
58+
// 1536 empty.tar
59+
// This means 3 blocks are consumed: one for the file header and two for the file trailer; no blocks are stored for the empty file
60+
61+
let data = ""
62+
let result = try tar([UInt8](data.utf8), filename: "filename")
63+
#expect(result.count == headerLen + trailerLen)
64+
65+
let output = try await tarListContents(result)
66+
#expect(output == "-r-xr-xr-x 0 0 0 0 Jan 1 1970 filename")
67+
}
68+
69+
// Test a degenerate case where the archive has no trailer
70+
@Test func testEmptyFileHeaderNoTrailer() async throws {
71+
var hdr: [UInt8] = []
72+
hdr.append(contentsOf: try TarHeader(name: "filename1", size: 0).bytes)
73+
74+
// No file data, no padding, no end of file marker
75+
#expect(hdr.count == headerLen)
76+
77+
// bsdtar tolerates the lack of end of file marker
78+
let output = try await tarListContents(hdr)
79+
#expect(output == "-r-xr-xr-x 0 0 0 0 Jan 1 1970 filename1")
80+
}
81+
82+
@Test func testEmptyFileHeaderWithTrailer() async throws {
83+
var hdr = try TarHeader(name: "filename1", size: 0).bytes
84+
85+
// No file data, no padding
86+
87+
// Append end of file marker
88+
let marker = [UInt8](repeating: 0, count: 2 * 512)
89+
hdr.append(contentsOf: marker)
90+
91+
let output = try await tarListContents(hdr)
92+
#expect(output == "-r-xr-xr-x 0 0 0 0 Jan 1 1970 filename1")
93+
}
94+
95+
// Test tar's reaction to a multi-file archive with an unnecessary block of
96+
// zeros (caused by bad padding). Tar only sees the first archive member.
97+
@Test func testEmptyFileHeaderMultipleBadPaddingWithTrailer() async throws {
98+
let data: [UInt8] = []
99+
var archive: [UInt8] = []
100+
101+
// First archive member, with bad padding. An empty file should not be padded
102+
// to 512 bytes because this adds a completely empty block which tar interprets
103+
// as end of file.
104+
archive.append(contentsOf: try TarHeader(name: "filename1", size: 0).bytes)
105+
archive.append(contentsOf: data)
106+
let padding1 = [UInt8](repeating: 0, count: 512 - (data.count % 512))
107+
archive.append(contentsOf: padding1)
108+
109+
// Second archive member, also with bad padding.
110+
archive.append(contentsOf: try TarHeader(name: "filename2", size: 0).bytes)
111+
archive.append(contentsOf: data)
112+
let padding2 = [UInt8](repeating: 0, count: 512 - (data.count % 512))
113+
archive.append(contentsOf: padding2)
114+
115+
// End of file marker - 2 empty blocks
116+
let marker = [UInt8](repeating: 0, count: 2 * 512)
117+
archive.append(contentsOf: marker)
118+
119+
// Check length - tar will ignore trailing data and some errors within the archive file
120+
#expect(archive.count == 6 * 512) // 6 blocks: header, padding, header, padding, eof-marker
121+
122+
let output = try await tarListContents(archive)
123+
124+
// bsdtar only sees the first archive member. Although the archive
125+
// file end marker is usually two blocks of zeros, here a single block
126+
// of zeros is interpreted as end of file.
127+
let expected =
128+
"""
129+
-r-xr-xr-x 0 0 0 0 Jan 1 1970 filename1
130+
"""
131+
132+
#expect(output == expected)
133+
}
134+
135+
@Test func testEmptyFileHeaderMultipleWithTrailer() async throws {
136+
let data: [UInt8] = []
137+
var archive: [UInt8] = []
138+
139+
// First archive member
140+
archive.append(contentsOf: try TarHeader(name: "filename1", size: 0).bytes)
141+
archive.append(contentsOf: data)
142+
143+
// Second archive member
144+
archive.append(contentsOf: try TarHeader(name: "filename2", size: 0).bytes)
145+
archive.append(contentsOf: data)
146+
147+
// End of file marker - 2 empty blocks
148+
let marker = [UInt8](repeating: 0, count: 2 * 512)
149+
archive.append(contentsOf: marker)
150+
151+
// Check length - tar will ignore trailing data and some errors within the archive file
152+
#expect(archive.count == 4 * 512) // 4 blocks: header, header, eof-marker
153+
154+
let output = try await tarListContents(archive)
155+
156+
let expected =
157+
"""
158+
-r-xr-xr-x 0 0 0 0 Jan 1 1970 filename1
159+
-r-xr-xr-x 0 0 0 0 Jan 1 1970 filename2
160+
"""
161+
162+
// N.B.: bsdtar output always includes a trailing newline
163+
#expect(output == expected)
164+
}
165+
166+
@Test func testDirectory() async throws {
167+
var archive: [UInt8] = []
168+
169+
// First archive member
170+
archive.append(contentsOf: try TarHeader(name: "dir1", typeflag: .DIRTYPE).bytes)
171+
172+
// End of file marker - 2 empty blocks
173+
let marker = [UInt8](repeating: 0, count: 2 * 512)
174+
archive.append(contentsOf: marker)
175+
176+
// Check length - tar will ignore trailing data and some errors within the archive file
177+
#expect(archive.count == 3 * 512) // header, eof-marker
178+
179+
let output = try await tarListContents(archive)
180+
181+
let expected =
182+
"""
183+
dr-xr-xr-x 0 0 0 0 Jan 1 1970 dir1
184+
"""
185+
186+
// N.B.: bsdtar output always includes a trailing newline
187+
#expect(output == expected)
188+
}
189+
190+
@Test func testDirectoryAndFiles() async throws {
191+
var archive: [UInt8] = []
192+
193+
// Directory
194+
archive.append(contentsOf: try TarHeader(name: "dir1", typeflag: .DIRTYPE).bytes)
195+
196+
// File at root of archive
197+
archive.append(contentsOf: try TarHeader(name: "filename1", size: 0).bytes)
198+
199+
// File in the directory
200+
archive.append(contentsOf: try TarHeader(name: "dir1/filename2", size: 0).bytes)
201+
202+
// Another file in the directory, using `prefix`
203+
archive.append(contentsOf: try TarHeader(name: "filename3", size: 0, prefix: "dir1").bytes)
204+
205+
// End of file marker - 2 empty blocks
206+
let marker = [UInt8](repeating: 0, count: 2 * 512)
207+
archive.append(contentsOf: marker)
208+
209+
// Check length - tar will ignore trailing data and some errors within the archive file
210+
#expect(archive.count == 6 * 512) // header, header, header, eof-marker
211+
212+
let output = try await tarListContents(archive)
213+
214+
// It's common to see directories immediately followed by the files which they contain,
215+
// but nothing about the tar format requires that
216+
let expected =
217+
"""
218+
dr-xr-xr-x 0 0 0 0 Jan 1 1970 dir1
219+
-r-xr-xr-x 0 0 0 0 Jan 1 1970 filename1
220+
-r-xr-xr-x 0 0 0 0 Jan 1 1970 dir1/filename2
221+
-r-xr-xr-x 0 0 0 0 Jan 1 1970 dir1/filename3
222+
"""
223+
224+
// N.B.: bsdtar output always includes a trailing newline
225+
#expect(output == expected)
226+
}
227+
228+
@Test func testDirectoryAndFilesWithContents() async throws {
229+
var archive: [UInt8] = []
230+
231+
// Directory
232+
archive.append(contentsOf: try TarHeader(name: "dir1", typeflag: .DIRTYPE).bytes)
233+
234+
// File at root of archive
235+
archive.append(contentsOf: try TarHeader(name: "filename1", size: 4).bytes)
236+
237+
// There's no real need to write actual data, as long as we have a block
238+
archive.append(contentsOf: [UInt8]("abcd".utf8))
239+
archive.append(contentsOf: [UInt8](repeating: 0, count: padding(4)))
240+
241+
// File in the directory
242+
archive.append(contentsOf: try TarHeader(name: "dir1/filename2", size: 4).bytes)
243+
archive.append(contentsOf: [UInt8]("abcd".utf8))
244+
archive.append(contentsOf: [UInt8](repeating: 0, count: padding(4)))
245+
246+
// End of file marker - 2 empty blocks
247+
let marker = [UInt8](repeating: 0, count: 2 * 512)
248+
archive.append(contentsOf: marker)
249+
250+
// Check length - tar will ignore trailing data and some errors within the archive file
251+
#expect(archive.count == 7 * 512) // header, header, data, header, data, eof-marker
252+
253+
let output = try await tarListContents(archive)
254+
255+
// It's common to see directories immediately followed by the files which they contain,
256+
// but nothing about the tar format requires that
257+
let expected =
258+
"""
259+
dr-xr-xr-x 0 0 0 0 Jan 1 1970 dir1
260+
-r-xr-xr-x 0 0 0 4 Jan 1 1970 filename1
261+
-r-xr-xr-x 0 0 0 4 Jan 1 1970 dir1/filename2
262+
"""
263+
264+
// N.B.: bsdtar output always includes a trailing newline
265+
#expect(output == expected)
266+
}
267+
268+
// If the same filename is added several times, all of them appear in the listing, and all of them are extracted.
269+
// Later instances overwrite earlier instances.
270+
//
271+
// % echo foo > foo
272+
// % tar cvf archive foo
273+
// a foo
274+
// % wc -c archive
275+
// 2048 archive # 1 header block, 1 data block, 2 end of archive blocks
276+
// % tar tvf archive
277+
// -rw-r--r-- 0 user staff 4 28 Feb 15:55 foo
278+
//
279+
// % echo bar > foo
280+
// % tar rvf archive foo
281+
// a foo
282+
// % wc -c archive
283+
// 3072 archive # 1 header block, 1 data block, 1 header block, 1 data block, 2 end of archive blocks
284+
// % tar tvf archive
285+
// -rw-r--r-- 0 user staff 4 28 Feb 15:55 foo
286+
// -rw-r--r-- 0 user staff 4 28 Feb 15:55 foo
287+
//
288+
// % rm foo
289+
// % tar xvf archive
290+
// x foo
291+
// x foo
292+
// % cat foo
293+
// bar
294+
295+
@Test func testDuplicateMemberNames() async throws {
296+
var archive: [UInt8] = []
297+
298+
// First file
299+
archive.append(contentsOf: try TarHeader(name: "filename1", size: 4).bytes)
300+
archive.append(contentsOf: [UInt8]("abcd".utf8))
301+
archive.append(contentsOf: [UInt8](repeating: 0, count: padding(4)))
302+
303+
// Replacement file
304+
archive.append(contentsOf: try TarHeader(name: "filename1", size: 8).bytes)
305+
archive.append(contentsOf: [UInt8]("abcdefgh".utf8))
306+
archive.append(contentsOf: [UInt8](repeating: 0, count: padding(8)))
307+
308+
// End of file marker - 2 empty blocks
309+
let marker = [UInt8](repeating: 0, count: 2 * 512)
310+
archive.append(contentsOf: marker)
311+
312+
// Check length - tar will ignore trailing data and some errors within the archive file
313+
#expect(archive.count == 6 * 512) // header, data, header, data, eof-marker
314+
315+
let output = try await tarListContents(archive)
316+
317+
let expected =
318+
"""
319+
-r-xr-xr-x 0 0 0 4 Jan 1 1970 filename1
320+
-r-xr-xr-x 0 0 0 8 Jan 1 1970 filename1
321+
"""
322+
323+
// N.B.: bsdtar output always includes a trailing newline
324+
#expect(output == expected)
325+
}
326+
}

0 commit comments

Comments
 (0)