Skip to content

Commit 2d0a85c

Browse files
authored
tar: Refactor tar writer, add a struct to represent tar headers (#69)
Motivation ---------- _[Explain here the context, and why you're making that change. What is the problem you're trying to solve.]_ Modifications ------------- * Add a TarHeader struct representing a standard tar archive member * Convert type flags to an enum * Wrap fields in a namespacing enum Result ------ No functional change. Tar writer is more modular, easier to test and extend. Test Plan --------- Existing tests continue to pass.
1 parent 95fce6b commit 2d0a85c

File tree

2 files changed

+189
-78
lines changed

2 files changed

+189
-78
lines changed

Sources/Tar/tar.swift

Lines changed: 186 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -97,22 +97,24 @@ func octal11(_ value: Int) -> String {
9797
}
9898

9999
// These ranges define the offsets of the standard fields in a Tar header.
100-
let name = 0..<100
101-
let mode = 100..<108
102-
let uid = 108..<116
103-
let gid = 116..<124
104-
let size = 124..<136
105-
let mtime = 136..<148
106-
let chksum = 148..<156
107-
let typeflag = 156..<157
108-
let linkname = 157..<257
109-
let magic = 257..<264
110-
let version = 263..<265
111-
let uname = 265..<297
112-
let gname = 297..<329
113-
let devmajor = 329..<337
114-
let devminor = 337..<345
115-
let prefix = 345..<500
100+
enum Field {
101+
static let name = 0..<100
102+
static let mode = 100..<108
103+
static let uid = 108..<116
104+
static let gid = 116..<124
105+
static let size = 124..<136
106+
static let mtime = 136..<148
107+
static let chksum = 148..<156
108+
static let typeflag = 156..<157
109+
static let linkname = 157..<257
110+
static let magic = 257..<264
111+
static let version = 263..<265
112+
static let uname = 265..<297
113+
static let gname = 297..<329
114+
static let devmajor = 329..<337
115+
static let devminor = 337..<345
116+
static let prefix = 345..<500
117+
}
116118

117119
/// Calculates a checksum over the contents of a tar header.
118120
/// - Parameter header: Tar header to checksum.
@@ -142,63 +144,172 @@ let TVERSION = "00" // Version used by macOS tar
142144

143145
let INIT_CHECKSUM = " " // Initial value of the checksum field before checksum calculation
144146

145-
// Typeflag values
146-
let REGTYPE = "0" // regular file
147-
let AREGTYPE = "\0" // regular file
148-
let LNKTYPE = "1" // link
149-
let SYMTYPE = "2" // reserved
150-
let CHRTYPE = "3" // character special
151-
let BLKTYPE = "4" // block special
152-
let DIRTYPE = "5" // directory
153-
let FIFOTYPE = "6" // FIFO special
154-
let CONTTYPE = "7" // reserved
155-
let XHDTYPE = "x" // Extended header referring to the next file in the archive
156-
let XGLTYPE = "g" // Global extended header
157-
158-
/// Creates a tar header for a single file
159-
/// - Parameters:
160-
/// - filesize: The size of the file
161-
/// - filename: The file's name in the archive
162-
/// - Returns: A tar header representing the file
163-
/// - Throws: If the filename is invalid
164-
public func tarHeader(filesize: Int, filename: String = "app") throws -> [UInt8] {
165-
// A file entry consists of a file header followed by the
166-
// contents of the file. The header includes information such as
167-
// the file name, size and permissions. Different versions of
168-
// tar added extra header fields.
169-
//
170-
// The file data is padded with nulls to a multiple of 512 bytes.
147+
/// Represents the type of a tar archive member
148+
public enum MemberType: String {
149+
/// Regular file
150+
case REGTYPE = "0"
151+
152+
/// Regular file (alternative)
153+
case AREGTYPE = "\0"
154+
155+
/// Link
156+
case LNKTYPE = "1"
157+
158+
/// Reserved
159+
case SYMTYPE = "2"
160+
161+
/// Character special
162+
case CHRTYPE = "3"
163+
164+
/// Block special
165+
case BLKTYPE = "4"
166+
167+
/// Directory
168+
case DIRTYPE = "5"
169+
170+
/// FIFO special
171+
case FIFOTYPE = "6"
172+
173+
/// Reserved
174+
case CONTTYPE = "7"
175+
176+
/// Extended header referring to the next file in the archive
177+
case XHDTYPE = "x"
178+
179+
/// Global extended header
180+
case XGLTYPE = "g"
181+
}
182+
183+
// maybe limited string, octal6 and octal11 should be separate types
184+
185+
/// Represents a single tar archive member
186+
public struct TarHeader {
187+
/// Member file name when unpacked
188+
var name: String
189+
190+
/// Access mode
191+
var mode: Int = 555
192+
193+
/// User ID of the file's owner
194+
var uid: Int = 0
195+
196+
/// Group ID of the file's owner
197+
var gid: Int = 0
171198

172-
// Archive member name cannot be empty because a Unix filename cannot be the empty string
173-
// https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_170
174-
guard filename.count > 0 else {
175-
throw TarError.invalidName(filename)
199+
/// File size in bytes
200+
var size: Int = 0
201+
202+
/// Last modification time
203+
var mtime: Int = 0
204+
205+
/// Tar header checksum
206+
var checksum: String = INIT_CHECKSUM
207+
208+
/// Type of this member
209+
var typeflag: MemberType = .REGTYPE
210+
211+
/// Name of the linked file
212+
var linkname: String = ""
213+
214+
/// Tar header magic number
215+
var magic: String = TMAGIC
216+
217+
/// Tar header format version
218+
var version: String = TVERSION
219+
220+
/// Username of the file's owner
221+
var uname: String = ""
222+
223+
/// Group name of the file's owner
224+
var gname: String = ""
225+
226+
/// Major device number
227+
var devmajor: Int = 0
228+
229+
/// Minor device number
230+
var devminor: Int = 0
231+
232+
/// Filename prefix - prepended to name
233+
var prefix: String = ""
234+
235+
init(
236+
name: String,
237+
mode: Int = 0o555,
238+
uid: Int = 0,
239+
gid: Int = 0,
240+
size: Int = 0,
241+
mtime: Int = 0,
242+
typeflag: MemberType = .REGTYPE,
243+
linkname: String = "",
244+
uname: String = "",
245+
gname: String = "",
246+
devmajor: Int = 0,
247+
devminor: Int = 0,
248+
prefix: String = ""
249+
) throws {
250+
// Archive member name cannot be empty because a Unix filename cannot be the empty string
251+
// https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_170
252+
guard name.count > 0 else {
253+
throw TarError.invalidName(name)
254+
}
255+
256+
self.name = name
257+
self.mode = mode
258+
self.uid = uid
259+
self.gid = gid
260+
self.size = size
261+
self.mtime = mtime
262+
self.checksum = INIT_CHECKSUM
263+
self.typeflag = typeflag
264+
self.linkname = linkname
265+
self.magic = TMAGIC
266+
self.version = TVERSION
267+
self.uname = uname
268+
self.gname = gname
269+
self.devmajor = devmajor
270+
self.devminor = devminor
271+
self.prefix = prefix
176272
}
273+
}
177274

178-
var hdr = [UInt8](repeating: 0, count: 512)
179-
180-
// Construct a POSIX ustar header for the file
181-
hdr.writeString(filename, inField: name, withTermination: .null)
182-
hdr.writeString(octal6(0o555), inField: mode, withTermination: .spaceAndNull)
183-
hdr.writeString(octal6(0o000000), inField: uid, withTermination: .spaceAndNull)
184-
hdr.writeString(octal6(0o000000), inField: gid, withTermination: .spaceAndNull)
185-
hdr.writeString(octal11(filesize), inField: size, withTermination: .space)
186-
hdr.writeString(octal11(0), inField: mtime, withTermination: .space)
187-
hdr.writeString(INIT_CHECKSUM, inField: chksum, withTermination: .none)
188-
hdr.writeString(REGTYPE, inField: typeflag, withTermination: .none)
189-
hdr.writeString("", inField: linkname, withTermination: .null)
190-
hdr.writeString(TMAGIC, inField: magic, withTermination: .null)
191-
hdr.writeString(TVERSION, inField: version, withTermination: .none)
192-
hdr.writeString("", inField: uname, withTermination: .null)
193-
hdr.writeString("", inField: gname, withTermination: .null)
194-
hdr.writeString(octal6(0o000000), inField: devmajor, withTermination: .spaceAndNull)
195-
hdr.writeString(octal6(0o000000), inField: devminor, withTermination: .spaceAndNull)
196-
hdr.writeString("", inField: prefix, withTermination: .null)
197-
198-
// Fill in the checksum.
199-
hdr.writeString(octal6(checksum(header: hdr)), inField: chksum, withTermination: .nullAndSpace)
200-
201-
return hdr
275+
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
280+
var bytes: [UInt8] {
281+
// A file entry consists of a file header followed by the
282+
// contents of the file. The header includes information such as
283+
// the file name, size and permissions. Different versions of
284+
// tar added extra header fields.
285+
//
286+
// The file data is padded with nulls to a multiple of 512 bytes.
287+
288+
var bytes = [UInt8](repeating: 0, count: 512)
289+
290+
// Construct a POSIX ustar header for the file
291+
bytes.writeString(self.name, inField: Field.name, withTermination: .null)
292+
bytes.writeString(octal6(self.mode), inField: Field.mode, withTermination: .spaceAndNull)
293+
bytes.writeString(octal6(self.uid), inField: Field.uid, withTermination: .spaceAndNull)
294+
bytes.writeString(octal6(self.gid), inField: Field.gid, withTermination: .spaceAndNull)
295+
bytes.writeString(octal11(self.size), inField: Field.size, withTermination: .space)
296+
bytes.writeString(octal11(self.mtime), inField: Field.mtime, withTermination: .space)
297+
bytes.writeString(INIT_CHECKSUM, inField: Field.chksum, withTermination: .none)
298+
bytes.writeString(self.typeflag.rawValue, inField: Field.typeflag, withTermination: .none)
299+
bytes.writeString(self.linkname, inField: Field.linkname, withTermination: .null)
300+
bytes.writeString(TMAGIC, inField: Field.magic, withTermination: .null)
301+
bytes.writeString(TVERSION, inField: Field.version, withTermination: .none)
302+
bytes.writeString(self.uname, inField: Field.uname, withTermination: .null)
303+
bytes.writeString(self.gname, inField: Field.gname, withTermination: .null)
304+
bytes.writeString(octal6(self.devmajor), inField: Field.devmajor, withTermination: .spaceAndNull)
305+
bytes.writeString(octal6(self.devminor), inField: Field.devminor, withTermination: .spaceAndNull)
306+
bytes.writeString(self.prefix, inField: Field.prefix, withTermination: .null)
307+
308+
// Fill in the checksum.
309+
bytes.writeString(octal6(Tar.checksum(header: bytes)), inField: Field.chksum, withTermination: .nullAndSpace)
310+
311+
return bytes
312+
}
202313
}
203314

204315
let blockSize = 512
@@ -218,19 +329,19 @@ func padding(_ len: Int) -> Int {
218329
/// - Returns: A tar archive containing the file
219330
/// - Throws: If the filename is invalid
220331
public func tar(_ bytes: [UInt8], filename: String = "app") throws -> [UInt8] {
221-
var hdr = try tarHeader(filesize: bytes.count, filename: filename)
332+
var archive = try TarHeader(name: filename, size: bytes.count).bytes
222333

223334
// Append the file data to the header
224-
hdr.append(contentsOf: bytes)
335+
archive.append(contentsOf: bytes)
225336

226337
// Pad the file data to a multiple of 512 bytes
227338
let padding = [UInt8](repeating: 0, count: padding(bytes.count))
228-
hdr.append(contentsOf: padding)
339+
archive.append(contentsOf: padding)
229340

230341
// Append the end of file marker
231342
let marker = [UInt8](repeating: 0, count: 2 * 512)
232-
hdr.append(contentsOf: marker)
233-
return hdr
343+
archive.append(contentsOf: marker)
344+
return archive
234345
}
235346

236347
/// Creates a tar archive containing a single file

Tests/TarTests/TarUnitTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,12 @@ let trailerLen = 2 * blocksize
101101

102102
@Test func testEmptyName() async throws {
103103
#expect(throws: TarError.invalidName("")) {
104-
let _ = try tarHeader(filesize: 0, filename: "")
104+
let _ = try TarHeader(name: "", size: 0)
105105
}
106106
}
107107

108108
@Test func testSingleEmptyFile() async throws {
109-
let hdr = try tarHeader(filesize: 0, filename: "filename")
109+
let hdr = try TarHeader(name: "filename", size: 0).bytes
110110
#expect(hdr.count == 512)
111111
#expect(
112112
hdr == [
@@ -131,7 +131,7 @@ let trailerLen = 2 * blocksize
131131
}
132132

133133
@Test func testSingle1kBFile() async throws {
134-
let hdr = try tarHeader(filesize: 1024, filename: "filename")
134+
let hdr = try TarHeader(name: "filename", size: 1024).bytes
135135
#expect(hdr.count == 512)
136136
#expect(
137137
hdr == [

0 commit comments

Comments
 (0)