@@ -179,6 +179,94 @@ extension ArchiveWriter {
179179}
180180
181181extension ArchiveWriter {
182+ private func archive( _ relativePath: FilePath , dirPath: FilePath ) throws {
183+ let fm = FileManager . default
184+
185+ let fullPath = dirPath. appending ( relativePath. string)
186+
187+ var statInfo = stat ( )
188+ guard lstat ( fullPath. string, & statInfo) == 0 else {
189+ let errNo = errno
190+ let err = POSIXErrorCode ( rawValue: errNo) ?? . EINVAL
191+ throw ArchiveError . failedToCreateArchive ( " lstat failed for ' \( fullPath) ': \( POSIXError ( err) ) " )
192+ }
193+
194+ let mode = statInfo. st_mode
195+ let uid = statInfo. st_uid
196+ let gid = statInfo. st_gid
197+ var size : Int64 = 0
198+ let type : URLFileResourceType
199+
200+ if ( mode & S_IFMT) == S_IFREG {
201+ type = . regular
202+ size = Int64 ( statInfo. st_size)
203+ } else if ( mode & S_IFMT) == S_IFDIR {
204+ type = . directory
205+ } else if ( mode & S_IFMT) == S_IFLNK {
206+ type = . symbolicLink
207+ } else {
208+ return
209+ }
210+
211+ #if os(macOS)
212+ let created = Date ( timeIntervalSince1970: Double ( statInfo. st_ctimespec. tv_sec) )
213+ let access = Date ( timeIntervalSince1970: Double ( statInfo. st_atimespec. tv_sec) )
214+ let modified = Date ( timeIntervalSince1970: Double ( statInfo. st_mtimespec. tv_sec) )
215+ #else
216+ let created = Date ( timeIntervalSince1970: Double ( statInfo. st_ctim. tv_sec) )
217+ let access = Date ( timeIntervalSince1970: Double ( statInfo. st_atim. tv_sec) )
218+ let modified = Date ( timeIntervalSince1970: Double ( statInfo. st_mtim. tv_sec) )
219+ #endif
220+
221+ let entry = WriteEntry ( )
222+ if type == . symbolicLink {
223+ let targetPath = try fm. destinationOfSymbolicLink ( atPath: fullPath. string)
224+ // Resolve the target relative to the symlink's parent, not the archive root.
225+ let symlinkParent = fullPath. removingLastComponent ( )
226+ let resolvedFull = symlinkParent. appending ( targetPath) . lexicallyNormalized ( )
227+ guard resolvedFull. starts ( with: dirPath) else {
228+ return
229+ }
230+ entry. symlinkTarget = targetPath
231+ }
232+
233+ entry. path = relativePath. string
234+ entry. size = size
235+ entry. creationDate = created
236+ entry. modificationDate = modified
237+ entry. contentAccessDate = access
238+ entry. fileType = type
239+ entry. group = gid
240+ entry. owner = uid
241+ entry. permissions = mode
242+ if type == . regular {
243+ let buf = UnsafeMutableRawBufferPointer . allocate ( byteCount: Self . chunkSize, alignment: 1 )
244+ guard let baseAddress = buf. baseAddress else {
245+ throw ArchiveError . failedToCreateArchive ( " cannot create temporary buffer of size \( Self . chunkSize) " )
246+ }
247+ defer { buf. deallocate ( ) }
248+ let fd = Foundation . open ( fullPath. string, O_RDONLY)
249+ guard fd >= 0 else {
250+ let err = POSIXErrorCode ( rawValue: errno) ?? . EINVAL
251+ throw ArchiveError . failedToCreateArchive ( " cannot open file \( fullPath. string) for reading: \( err) " )
252+ }
253+ defer { close ( fd) }
254+ try self . writeHeader ( entry: entry)
255+ while true {
256+ let n = read ( fd, baseAddress, Self . chunkSize)
257+ if n == 0 { break }
258+ if n < 0 {
259+ let err = POSIXErrorCode ( rawValue: errno) ?? . EIO
260+ throw ArchiveError . failedToCreateArchive ( " failed to read from file \( fullPath. string) : \( err) " )
261+ }
262+ try self . writeData ( data: UnsafeRawBufferPointer ( start: baseAddress, count: n) )
263+ }
264+ try self . finishEntry ( )
265+ } else {
266+ try self . writeEntry ( entry: entry, data: nil )
267+ }
268+ }
269+
182270 /// Recursively archives the content of a directory. Regular files, symlinks and directories are added into the archive.
183271 /// Note: Symlinks are added to the archive if both the source and target for the symlink are both contained in the top level directory.
184272 public func archiveDirectory( _ dir: URL ) throws {
@@ -214,88 +302,37 @@ extension ArchiveWriter {
214302 try self . writeHeader ( entry: rootEntry)
215303
216304 for case let relativePath as String in enumerator {
217- let fullPath = dirPath. appending ( relativePath)
305+ try archive ( FilePath ( relativePath) , dirPath: dirPath)
306+ }
307+ }
218308
219- var statInfo = stat ( )
220- guard lstat ( fullPath. string, & statInfo) == 0 else {
221- let errNo = errno
222- let err = POSIXErrorCode ( rawValue: errNo) ?? . EINVAL
223- throw ArchiveError . failedToCreateArchive ( " lstat failed for ' \( fullPath) ': \( POSIXError ( err) ) " )
224- }
309+ public func archive( _ paths: [ FilePath ] , base: FilePath ) throws {
310+ let fm = FileManager . default
311+ let base = base. lexicallyNormalized ( )
225312
226- let mode = statInfo. st_mode
227- let uid = statInfo. st_uid
228- let gid = statInfo. st_gid
229- var size : Int64 = 0
230- let type : URLFileResourceType
231-
232- if ( mode & S_IFMT) == S_IFREG {
233- type = . regular
234- size = Int64 ( statInfo. st_size)
235- } else if ( mode & S_IFMT) == S_IFDIR {
236- type = . directory
237- } else if ( mode & S_IFMT) == S_IFLNK {
238- type = . symbolicLink
239- } else {
240- continue
313+ for path in paths {
314+ guard path. starts ( with: base) else {
315+ throw ArchiveError . failedToCreateArchive ( " ' \( path. string) ' is not under ' \( base. string) ' " )
241316 }
242317
243- #if os(macOS)
244- let created = Date ( timeIntervalSince1970: Double ( statInfo. st_ctimespec. tv_sec) )
245- let access = Date ( timeIntervalSince1970: Double ( statInfo. st_atimespec. tv_sec) )
246- let modified = Date ( timeIntervalSince1970: Double ( statInfo. st_mtimespec. tv_sec) )
247- #else
248- let created = Date ( timeIntervalSince1970: Double ( statInfo. st_ctim. tv_sec) )
249- let access = Date ( timeIntervalSince1970: Double ( statInfo. st_atim. tv_sec) )
250- let modified = Date ( timeIntervalSince1970: Double ( statInfo. st_mtim. tv_sec) )
251- #endif
252-
253- let entry = WriteEntry ( )
254- if type == . symbolicLink {
255- let targetPath = try fm. destinationOfSymbolicLink ( atPath: fullPath. string)
256- // Resolve the target relative to the symlink's parent, not the archive root.
257- let symlinkParent = fullPath. removingLastComponent ( )
258- let resolvedFull = symlinkParent. appending ( targetPath) . lexicallyNormalized ( )
259- guard resolvedFull. starts ( with: dirPath) else {
260- continue
261- }
262- entry. symlinkTarget = targetPath
263- }
318+ let relativePath = path. components. dropFirst ( base. components. count)
319+ . reduce ( into: FilePath ( " " ) ) { $0. append ( $1) }
264320
265- entry. path = relativePath
266- entry. size = size
267- entry. creationDate = created
268- entry. modificationDate = modified
269- entry. contentAccessDate = access
270- entry. fileType = type
271- entry. group = gid
272- entry. owner = uid
273- entry. permissions = mode
274- if type == . regular {
275- let buf = UnsafeMutableRawBufferPointer . allocate ( byteCount: Self . chunkSize, alignment: 1 )
276- guard let baseAddress = buf. baseAddress else {
277- throw ArchiveError . failedToCreateArchive ( " cannot create temporary buffer of size \( Self . chunkSize) " )
278- }
279- defer { buf. deallocate ( ) }
280- let fd = Foundation . open ( fullPath. string, O_RDONLY)
281- guard fd >= 0 else {
282- let err = POSIXErrorCode ( rawValue: errno) ?? . EINVAL
283- throw ArchiveError . failedToCreateArchive ( " cannot open file \( fullPath. string) for reading: \( err) " )
321+ var isDir : ObjCBool = false
322+ _ = fm. fileExists ( atPath: path. string, isDirectory: & isDir)
323+ if isDir. boolValue {
324+ guard let enumerator = fm. enumerator ( atPath: path. string) else {
325+ throw POSIXError ( . ENOTDIR)
284326 }
285- defer { close ( fd) }
286- try self . writeHeader ( entry: entry)
287- while true {
288- let n = read ( fd, baseAddress, Self . chunkSize)
289- if n == 0 { break }
290- if n < 0 {
291- let err = POSIXErrorCode ( rawValue: errno) ?? . EIO
292- throw ArchiveError . failedToCreateArchive ( " failed to read from file \( fullPath. string) : \( err) " )
293- }
294- try self . writeData ( data: UnsafeRawBufferPointer ( start: baseAddress, count: n) )
327+
328+ try archive ( relativePath, dirPath: base)
329+ for case let child as String in enumerator {
330+ let childPath = relativePath. appending ( child)
331+
332+ try archive ( childPath, dirPath: base)
295333 }
296- try self . finishEntry ( )
297334 } else {
298- try self . writeEntry ( entry : entry , data : nil )
335+ try archive ( relativePath , dirPath : base )
299336 }
300337 }
301338 }
0 commit comments