Skip to content

Commit 504050b

Browse files
committed
tar: Add support for archiving directories
1 parent 813a979 commit 504050b

File tree

3 files changed

+461
-26
lines changed

3 files changed

+461
-26
lines changed

Sources/Tar/tar.swift

Lines changed: 123 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,18 @@ import struct Foundation.Data
2222
// relatively straightforward; reading an arbitrary tar file is more
2323
// complicated because the reader must be prepared to handle all variants.
2424

25+
// Tar archives consist of 512-byte blocks, either containing member headers
26+
// or file data. Blocks shorter than 512 bytes are padded with zeros.
27+
let blockSize = 512
28+
29+
/// Returns the number of padding bytes to be appended to a file.
30+
/// Each file in a tar archive must be padded to a multiple of the 512 byte block size.
31+
/// - Parameter len: The length of the archive member.
32+
/// - Returns: The number of zero bytes to append as padding.
33+
func padding(_ len: Int) -> Int {
34+
(blockSize - len % blockSize) % blockSize
35+
}
36+
2537
enum TarError: Error, Equatable {
2638
case invalidName(String)
2739
}
@@ -134,7 +146,7 @@ func checksum(header: [UInt8]) -> Int {
134146
// The checksum calculation can't overflow (maximum possible value 776) so we can use
135147
// unchecked arithmetic.
136148

137-
precondition(header.count == 512)
149+
precondition(header.count == blockSize)
138150
return header.reduce(0) { $0 &+ Int($1) }
139151
}
140152

@@ -182,7 +194,7 @@ public enum MemberType: String {
182194

183195
// maybe limited string, octal6 and octal11 should be separate types
184196

185-
/// Represents a single tar archive member
197+
/// Represents a single tar archive member header
186198
public struct TarHeader {
187199
/// Member file name when unpacked
188200
var name: String
@@ -232,7 +244,7 @@ public struct TarHeader {
232244
/// Filename prefix - prepended to name
233245
var prefix: String = ""
234246

235-
init(
247+
public init(
236248
name: String,
237249
mode: Int = 0o555,
238250
uid: Int = 0,
@@ -273,10 +285,7 @@ public struct TarHeader {
273285
}
274286

275287
extension TarHeader {
276-
/// Creates a tar header for a single file
277-
/// - Parameters:
278-
/// - hdr: The header structure of the file
279-
/// - Returns: A tar header representing the file
288+
/// The serialized byte representation of the header.
280289
var bytes: [UInt8] {
281290
// A file entry consists of a file header followed by the
282291
// contents of the file. The header includes information such as
@@ -285,7 +294,7 @@ extension TarHeader {
285294
//
286295
// The file data is padded with nulls to a multiple of 512 bytes.
287296

288-
var bytes = [UInt8](repeating: 0, count: 512)
297+
var bytes = [UInt8](repeating: 0, count: blockSize)
289298

290299
// Construct a POSIX ustar header for the file
291300
bytes.writeString(self.name, inField: Field.name, withTermination: .null)
@@ -312,16 +321,6 @@ extension TarHeader {
312321
}
313322
}
314323

315-
let blockSize = 512
316-
317-
/// Returns the number of padding bytes to be appended to a file.
318-
/// Each file in a tar archive must be padded to a multiple of the 512 byte block size.
319-
/// - Parameter len: The length of the archive member.
320-
/// - Returns: The number of zero bytes to append as padding.
321-
func padding(_ len: Int) -> Int {
322-
(blockSize - len % blockSize) % blockSize
323-
}
324-
325324
/// Creates a tar archive containing a single file
326325
/// - Parameters:
327326
/// - bytes: The file's body data
@@ -339,7 +338,7 @@ public func tar(_ bytes: [UInt8], filename: String = "app") throws -> [UInt8] {
339338
archive.append(contentsOf: padding)
340339

341340
// Append the end of file marker
342-
let marker = [UInt8](repeating: 0, count: 2 * 512)
341+
let marker = [UInt8](repeating: 0, count: 2 * blockSize)
343342
archive.append(contentsOf: marker)
344343
return archive
345344
}
@@ -353,3 +352,108 @@ public func tar(_ bytes: [UInt8], filename: String = "app") throws -> [UInt8] {
353352
public func tar(_ data: Data, filename: String) throws -> [UInt8] {
354353
try tar([UInt8](data), filename: filename)
355354
}
355+
356+
/// Represents a tar archive
357+
public struct Archive {
358+
/// The files, directories and other members of the archive
359+
var members: [ArchiveMember]
360+
361+
/// Creates an empty Archive
362+
public init() {
363+
members = []
364+
}
365+
366+
/// Appends a member to the archive
367+
/// Parameters:
368+
/// - member: The member to append
369+
public mutating func append(_ member: ArchiveMember) {
370+
self.members.append(member)
371+
}
372+
373+
/// Returns a new archive made by appending a member to the receiver
374+
/// Parameters:
375+
/// - member: The member to append
376+
/// Returns: A new archive made by appending `member` to the receiver.
377+
public func appending(_ member: ArchiveMember) -> Self {
378+
var ret = self
379+
ret.members += [member]
380+
return ret
381+
}
382+
383+
/// The serialized byte representation of the archive, including padding and end-of-archive marker.
384+
public var bytes: [UInt8] {
385+
var ret: [UInt8] = []
386+
for member in members {
387+
ret.append(contentsOf: member.bytes)
388+
}
389+
390+
// Append the end of file marker
391+
let marker = [UInt8](repeating: 0, count: 2 * blockSize)
392+
ret.append(contentsOf: marker)
393+
394+
return ret
395+
}
396+
}
397+
398+
/// Represents a member of a tar archive
399+
public struct ArchiveMember {
400+
/// Member header containing metadata about the member
401+
var header: TarHeader
402+
403+
/// File content
404+
var contents: [UInt8]
405+
406+
/// Creates a new ArchiveMember
407+
/// Parameters:
408+
/// - header: Member header containing metadata about the member
409+
/// - data: File content
410+
public init(
411+
header: TarHeader,
412+
data: [UInt8] = []
413+
) {
414+
self.header = header
415+
self.contents = data
416+
}
417+
418+
/// The serialized byte representation of the member, including padding.
419+
public var bytes: [UInt8] {
420+
let padding = [UInt8](repeating: 0, count: padding(contents.count))
421+
return header.bytes + self.contents + padding
422+
}
423+
}
424+
425+
extension Archive {
426+
/// Adds a new file member at the end of the archive
427+
/// parameters:
428+
/// - name: File name
429+
/// - prefix: Path prefix
430+
/// - data: File contents
431+
public mutating func appendFile(name: String, prefix: String = "", data: [UInt8]) throws {
432+
try append(.init(header: .init(name: name, size: data.count, prefix: prefix), data: data))
433+
}
434+
435+
/// Adds a new file member at the end of the archive
436+
/// parameters:
437+
/// - name: File name
438+
/// - prefix: Path prefix
439+
/// - data: File contents
440+
public func appendingFile(name: String, prefix: String = "", data: [UInt8]) throws -> Self {
441+
try appending(.init(header: .init(name: name, size: data.count, prefix: prefix), data: data))
442+
}
443+
444+
/// Adds a new directory member at the end of the archive
445+
/// parameters:
446+
/// - name: Directory name
447+
/// - prefix: Path prefix
448+
public mutating func appendDirectory(name: String, prefix: String = "") throws {
449+
try append(.init(header: .init(name: name, typeflag: .DIRTYPE, prefix: prefix)))
450+
}
451+
452+
/// Adds a new directory member at the end of the archive
453+
/// parameters:
454+
/// - name: Directory name
455+
/// - prefix: Path prefix
456+
public func appendingDirectory(name: String, prefix: String = "") throws -> Self {
457+
try self.appending(.init(header: .init(name: name, typeflag: .DIRTYPE, prefix: prefix)))
458+
}
459+
}

Tests/TarTests/TarInteropTests.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15-
import Foundation
15+
import class Foundation.Pipe
16+
import class Foundation.Process
17+
1618
import Testing
1719
@testable import Tar
1820

@@ -43,7 +45,7 @@ import Testing
4345
@Test func testSingle4BFile() async throws {
4446
let data = "test"
4547
let result = try tar([UInt8](data.utf8), filename: "filename")
46-
#expect(result.count == headerLen + blocksize + trailerLen)
48+
#expect(result.count == headerSize + blockSize + trailerSize)
4749

4850
let output = try await tarListContents(result)
4951
#expect(output == "-r-xr-xr-x 0 0 0 4 Jan 1 1970 filename")
@@ -60,7 +62,7 @@ import Testing
6062

6163
let data = ""
6264
let result = try tar([UInt8](data.utf8), filename: "filename")
63-
#expect(result.count == headerLen + trailerLen)
65+
#expect(result.count == headerSize + trailerSize)
6466

6567
let output = try await tarListContents(result)
6668
#expect(output == "-r-xr-xr-x 0 0 0 0 Jan 1 1970 filename")
@@ -72,7 +74,7 @@ import Testing
7274
hdr.append(contentsOf: try TarHeader(name: "filename1", size: 0).bytes)
7375

7476
// No file data, no padding, no end of file marker
75-
#expect(hdr.count == headerLen)
77+
#expect(hdr.count == headerSize)
7678

7779
// bsdtar tolerates the lack of end of file marker
7880
let output = try await tarListContents(hdr)

0 commit comments

Comments
 (0)