Skip to content

Commit 1840f9b

Browse files
committed
tar: Refactor tar writer, add a struct to represent tar headers
1 parent 95fce6b commit 1840f9b

File tree

2 files changed

+136
-78
lines changed

2 files changed

+136
-78
lines changed

Sources/Tar/tar.swift

Lines changed: 133 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,119 @@ 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+
// Typeflag
148+
enum MemberType: String {
149+
case REGTYPE = "0" // regular file
150+
case AREGTYPE = "\0" // regular file
151+
case LNKTYPE = "1" // link
152+
case SYMTYPE = "2" // reserved
153+
case CHRTYPE = "3" // character special
154+
case BLKTYPE = "4" // block special
155+
case DIRTYPE = "5" // directory
156+
case FIFOTYPE = "6" // FIFO special
157+
case CONTTYPE = "7" // reserved
158+
case XHDTYPE = "x" // Extended header referring to the next file in the archive
159+
case XGLTYPE = "g" // Global extended header
160+
}
161+
162+
// maybe limited string, octal6 and octal11 should be separate types
163+
164+
public struct TarHeader {
165+
var name: String // do we need an explicit constructor to check this and throw?
166+
var mode: Int = 555
167+
var uid: Int = 0
168+
var gid: Int = 0
169+
var size: Int = 0
170+
var mtime: Int = 0
171+
var checksum: String = INIT_CHECKSUM // maybe not
172+
var typeflag: MemberType = .REGTYPE // better as enum
173+
var linkname: String = ""
174+
var magic: String = TMAGIC
175+
var version: String = TVERSION
176+
var uname: String = ""
177+
var gname: String = ""
178+
var devmajor: Int = 0
179+
var devminor: Int = 0
180+
var prefix: String = ""
171181

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)
182+
init(
183+
name: String,
184+
mode: Int = 0o555,
185+
uid: Int = 0,
186+
gid: Int = 0,
187+
size: Int = 0,
188+
mtime: Int = 0,
189+
typeflag: MemberType = .REGTYPE,
190+
linkname: String = "",
191+
uname: String = "",
192+
gname: String = "",
193+
devmajor: Int = 0,
194+
devminor: Int = 0,
195+
prefix: String = ""
196+
) throws {
197+
// Archive member name cannot be empty because a Unix filename cannot be the empty string
198+
// https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_170
199+
guard name.count > 0 else {
200+
throw TarError.invalidName(name)
201+
}
202+
203+
self.name = name
204+
self.mode = mode
205+
self.uid = uid
206+
self.gid = gid
207+
self.size = size
208+
self.mtime = mtime
209+
self.checksum = INIT_CHECKSUM
210+
self.typeflag = typeflag
211+
self.linkname = linkname
212+
self.magic = TMAGIC
213+
self.version = TVERSION
214+
self.uname = uname
215+
self.gname = gname
216+
self.devmajor = devmajor
217+
self.devminor = devminor
218+
self.prefix = prefix
176219
}
220+
}
221+
222+
extension TarHeader {
223+
/// Creates a tar header for a single file
224+
/// - Parameters:
225+
/// - hdr: The header structure of the file
226+
/// - Returns: A tar header representing the file
227+
var bytes: [UInt8] {
228+
// A file entry consists of a file header followed by the
229+
// contents of the file. The header includes information such as
230+
// the file name, size and permissions. Different versions of
231+
// tar added extra header fields.
232+
//
233+
// The file data is padded with nulls to a multiple of 512 bytes.
177234

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
235+
var bytes = [UInt8](repeating: 0, count: 512)
236+
237+
// Construct a POSIX ustar header for the file
238+
bytes.writeString(self.name, inField: Field.name, withTermination: .null)
239+
bytes.writeString(octal6(self.mode), inField: Field.mode, withTermination: .spaceAndNull)
240+
bytes.writeString(octal6(self.uid), inField: Field.uid, withTermination: .spaceAndNull)
241+
bytes.writeString(octal6(self.gid), inField: Field.gid, withTermination: .spaceAndNull)
242+
bytes.writeString(octal11(self.size), inField: Field.size, withTermination: .space)
243+
bytes.writeString(octal11(self.mtime), inField: Field.mtime, withTermination: .space)
244+
bytes.writeString(INIT_CHECKSUM, inField: Field.chksum, withTermination: .none)
245+
bytes.writeString(self.typeflag.rawValue, inField: Field.typeflag, withTermination: .none)
246+
bytes.writeString(self.linkname, inField: Field.linkname, withTermination: .null)
247+
bytes.writeString(TMAGIC, inField: Field.magic, withTermination: .null)
248+
bytes.writeString(TVERSION, inField: Field.version, withTermination: .none)
249+
bytes.writeString(self.uname, inField: Field.uname, withTermination: .null)
250+
bytes.writeString(self.gname, inField: Field.gname, withTermination: .null)
251+
bytes.writeString(octal6(self.devmajor), inField: Field.devmajor, withTermination: .spaceAndNull)
252+
bytes.writeString(octal6(self.devminor), inField: Field.devminor, withTermination: .spaceAndNull)
253+
bytes.writeString(self.prefix, inField: Field.prefix, withTermination: .null)
254+
255+
// Fill in the checksum.
256+
bytes.writeString(octal6(Tar.checksum(header: bytes)), inField: Field.chksum, withTermination: .nullAndSpace)
257+
258+
return bytes
259+
}
202260
}
203261

204262
let blockSize = 512
@@ -218,19 +276,19 @@ func padding(_ len: Int) -> Int {
218276
/// - Returns: A tar archive containing the file
219277
/// - Throws: If the filename is invalid
220278
public func tar(_ bytes: [UInt8], filename: String = "app") throws -> [UInt8] {
221-
var hdr = try tarHeader(filesize: bytes.count, filename: filename)
279+
var archive = try TarHeader(name: filename, size: bytes.count).bytes
222280

223281
// Append the file data to the header
224-
hdr.append(contentsOf: bytes)
282+
archive.append(contentsOf: bytes)
225283

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

230288
// Append the end of file marker
231289
let marker = [UInt8](repeating: 0, count: 2 * 512)
232-
hdr.append(contentsOf: marker)
233-
return hdr
290+
archive.append(contentsOf: marker)
291+
return archive
234292
}
235293

236294
/// 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)