@@ -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+
2537enum 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
186198public 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
275287extension 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] {
353352public 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+ }
0 commit comments