mirror of
https://github.com/apple/container.git
synced 2026-10-14 10:09:10 +00:00
Refactor ArchiveWriter to expose archiving each path independently (#693)
This PR refactors `ArchiveWriter` to add an API `archive(_ paths: base:)`. This API is used to archive the contents at each path independently, similar to doing `tar -cvf archive.tar foo.bin /bar/baz.txt`.
This commit is contained in:
1 parent
df125a2300
commit
f2c42402e7
2 files changed
+349
-77
No files matched your search
@@ -179,6 +179,94 @@ extension ArchiveWriter {
|
||||
}
|
||||
|
||||
extension ArchiveWriter {
|
||||
private func archive(_ relativePath: FilePath, dirPath: FilePath) throws {
|
||||
let fm = FileManager.default
|
||||
|
||||
let fullPath = dirPath.appending(relativePath.string)
|
||||
|
||||
var statInfo = stat()
|
||||
guard lstat(fullPath.string, &statInfo) == 0 else {
|
||||
let errNo = errno
|
||||
let err = POSIXErrorCode(rawValue: errNo) ?? .EINVAL
|
||||
throw ArchiveError.failedToCreateArchive("lstat failed for '\(fullPath)': \(POSIXError(err))")
|
||||
}
|
||||
|
||||
let mode = statInfo.st_mode
|
||||
let uid = statInfo.st_uid
|
||||
let gid = statInfo.st_gid
|
||||
var size: Int64 = 0
|
||||
let type: URLFileResourceType
|
||||
|
||||
if (mode & S_IFMT) == S_IFREG {
|
||||
type = .regular
|
||||
size = Int64(statInfo.st_size)
|
||||
} else if (mode & S_IFMT) == S_IFDIR {
|
||||
type = .directory
|
||||
} else if (mode & S_IFMT) == S_IFLNK {
|
||||
type = .symbolicLink
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
let created = Date(timeIntervalSince1970: Double(statInfo.st_ctimespec.tv_sec))
|
||||
let access = Date(timeIntervalSince1970: Double(statInfo.st_atimespec.tv_sec))
|
||||
let modified = Date(timeIntervalSince1970: Double(statInfo.st_mtimespec.tv_sec))
|
||||
#else
|
||||
let created = Date(timeIntervalSince1970: Double(statInfo.st_ctim.tv_sec))
|
||||
let access = Date(timeIntervalSince1970: Double(statInfo.st_atim.tv_sec))
|
||||
let modified = Date(timeIntervalSince1970: Double(statInfo.st_mtim.tv_sec))
|
||||
#endif
|
||||
|
||||
let entry = WriteEntry()
|
||||
if type == .symbolicLink {
|
||||
let targetPath = try fm.destinationOfSymbolicLink(atPath: fullPath.string)
|
||||
// Resolve the target relative to the symlink's parent, not the archive root.
|
||||
let symlinkParent = fullPath.removingLastComponent()
|
||||
let resolvedFull = symlinkParent.appending(targetPath).lexicallyNormalized()
|
||||
guard resolvedFull.starts(with: dirPath) else {
|
||||
return
|
||||
}
|
||||
entry.symlinkTarget = targetPath
|
||||
}
|
||||
|
||||
entry.path = relativePath.string
|
||||
entry.size = size
|
||||
entry.creationDate = created
|
||||
entry.modificationDate = modified
|
||||
entry.contentAccessDate = access
|
||||
entry.fileType = type
|
||||
entry.group = gid
|
||||
entry.owner = uid
|
||||
entry.permissions = mode
|
||||
if type == .regular {
|
||||
let buf = UnsafeMutableRawBufferPointer.allocate(byteCount: Self.chunkSize, alignment: 1)
|
||||
guard let baseAddress = buf.baseAddress else {
|
||||
throw ArchiveError.failedToCreateArchive("cannot create temporary buffer of size \(Self.chunkSize)")
|
||||
}
|
||||
defer { buf.deallocate() }
|
||||
let fd = Foundation.open(fullPath.string, O_RDONLY)
|
||||
guard fd >= 0 else {
|
||||
let err = POSIXErrorCode(rawValue: errno) ?? .EINVAL
|
||||
throw ArchiveError.failedToCreateArchive("cannot open file \(fullPath.string) for reading: \(err)")
|
||||
}
|
||||
defer { close(fd) }
|
||||
try self.writeHeader(entry: entry)
|
||||
while true {
|
||||
let n = read(fd, baseAddress, Self.chunkSize)
|
||||
if n == 0 { break }
|
||||
if n < 0 {
|
||||
let err = POSIXErrorCode(rawValue: errno) ?? .EIO
|
||||
throw ArchiveError.failedToCreateArchive("failed to read from file \(fullPath.string): \(err)")
|
||||
}
|
||||
try self.writeData(data: UnsafeRawBufferPointer(start: baseAddress, count: n))
|
||||
}
|
||||
try self.finishEntry()
|
||||
} else {
|
||||
try self.writeEntry(entry: entry, data: nil)
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively archives the content of a directory. Regular files, symlinks and directories are added into the archive.
|
||||
/// Note: Symlinks are added to the archive if both the source and target for the symlink are both contained in the top level directory.
|
||||
public func archiveDirectory(_ dir: URL) throws {
|
||||
@@ -214,88 +302,37 @@ extension ArchiveWriter {
|
||||
try self.writeHeader(entry: rootEntry)
|
||||
|
||||
for case let relativePath as String in enumerator {
|
||||
let fullPath = dirPath.appending(relativePath)
|
||||
try archive(FilePath(relativePath), dirPath: dirPath)
|
||||
}
|
||||
}
|
||||
|
||||
var statInfo = stat()
|
||||
guard lstat(fullPath.string, &statInfo) == 0 else {
|
||||
let errNo = errno
|
||||
let err = POSIXErrorCode(rawValue: errNo) ?? .EINVAL
|
||||
throw ArchiveError.failedToCreateArchive("lstat failed for '\(fullPath)': \(POSIXError(err))")
|
||||
public func archive(_ paths: [FilePath], base: FilePath) throws {
|
||||
let fm = FileManager.default
|
||||
let base = base.lexicallyNormalized()
|
||||
|
||||
for path in paths {
|
||||
guard path.starts(with: base) else {
|
||||
throw ArchiveError.failedToCreateArchive("'\(path.string)' is not under '\(base.string)'")
|
||||
}
|
||||
|
||||
let mode = statInfo.st_mode
|
||||
let uid = statInfo.st_uid
|
||||
let gid = statInfo.st_gid
|
||||
var size: Int64 = 0
|
||||
let type: URLFileResourceType
|
||||
let relativePath = path.components.dropFirst(base.components.count)
|
||||
.reduce(into: FilePath("")) { $0.append($1) }
|
||||
|
||||
if (mode & S_IFMT) == S_IFREG {
|
||||
type = .regular
|
||||
size = Int64(statInfo.st_size)
|
||||
} else if (mode & S_IFMT) == S_IFDIR {
|
||||
type = .directory
|
||||
} else if (mode & S_IFMT) == S_IFLNK {
|
||||
type = .symbolicLink
|
||||
var isDir: ObjCBool = false
|
||||
_ = fm.fileExists(atPath: path.string, isDirectory: &isDir)
|
||||
if isDir.boolValue {
|
||||
guard let enumerator = fm.enumerator(atPath: path.string) else {
|
||||
throw POSIXError(.ENOTDIR)
|
||||
}
|
||||
|
||||
try archive(relativePath, dirPath: base)
|
||||
for case let child as String in enumerator {
|
||||
let childPath = relativePath.appending(child)
|
||||
|
||||
try archive(childPath, dirPath: base)
|
||||
}
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
let created = Date(timeIntervalSince1970: Double(statInfo.st_ctimespec.tv_sec))
|
||||
let access = Date(timeIntervalSince1970: Double(statInfo.st_atimespec.tv_sec))
|
||||
let modified = Date(timeIntervalSince1970: Double(statInfo.st_mtimespec.tv_sec))
|
||||
#else
|
||||
let created = Date(timeIntervalSince1970: Double(statInfo.st_ctim.tv_sec))
|
||||
let access = Date(timeIntervalSince1970: Double(statInfo.st_atim.tv_sec))
|
||||
let modified = Date(timeIntervalSince1970: Double(statInfo.st_mtim.tv_sec))
|
||||
#endif
|
||||
|
||||
let entry = WriteEntry()
|
||||
if type == .symbolicLink {
|
||||
let targetPath = try fm.destinationOfSymbolicLink(atPath: fullPath.string)
|
||||
// Resolve the target relative to the symlink's parent, not the archive root.
|
||||
let symlinkParent = fullPath.removingLastComponent()
|
||||
let resolvedFull = symlinkParent.appending(targetPath).lexicallyNormalized()
|
||||
guard resolvedFull.starts(with: dirPath) else {
|
||||
continue
|
||||
}
|
||||
entry.symlinkTarget = targetPath
|
||||
}
|
||||
|
||||
entry.path = relativePath
|
||||
entry.size = size
|
||||
entry.creationDate = created
|
||||
entry.modificationDate = modified
|
||||
entry.contentAccessDate = access
|
||||
entry.fileType = type
|
||||
entry.group = gid
|
||||
entry.owner = uid
|
||||
entry.permissions = mode
|
||||
if type == .regular {
|
||||
let buf = UnsafeMutableRawBufferPointer.allocate(byteCount: Self.chunkSize, alignment: 1)
|
||||
guard let baseAddress = buf.baseAddress else {
|
||||
throw ArchiveError.failedToCreateArchive("cannot create temporary buffer of size \(Self.chunkSize)")
|
||||
}
|
||||
defer { buf.deallocate() }
|
||||
let fd = Foundation.open(fullPath.string, O_RDONLY)
|
||||
guard fd >= 0 else {
|
||||
let err = POSIXErrorCode(rawValue: errno) ?? .EINVAL
|
||||
throw ArchiveError.failedToCreateArchive("cannot open file \(fullPath.string) for reading: \(err)")
|
||||
}
|
||||
defer { close(fd) }
|
||||
try self.writeHeader(entry: entry)
|
||||
while true {
|
||||
let n = read(fd, baseAddress, Self.chunkSize)
|
||||
if n == 0 { break }
|
||||
if n < 0 {
|
||||
let err = POSIXErrorCode(rawValue: errno) ?? .EIO
|
||||
throw ArchiveError.failedToCreateArchive("failed to read from file \(fullPath.string): \(err)")
|
||||
}
|
||||
try self.writeData(data: UnsafeRawBufferPointer(start: baseAddress, count: n))
|
||||
}
|
||||
try self.finishEntry()
|
||||
} else {
|
||||
try self.writeEntry(entry: entry, data: nil)
|
||||
try archive(relativePath, dirPath: base)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SystemPackage
|
||||
import Testing
|
||||
|
||||
@testable import ContainerizationArchive
|
||||
@@ -559,6 +560,240 @@ struct ArchiveTests {
|
||||
#expect(try String(contentsOf: extractDir.appendingPathComponent("only.txt"), encoding: .utf8) == "only file")
|
||||
}
|
||||
|
||||
// MARK: - archive tests
|
||||
|
||||
@Test func archiveURLsBasic() throws {
|
||||
let testDir = createTemporaryDirectory(baseName: "ArchiveTests.archiveURLsBasic")!
|
||||
defer { try? FileManager.default.removeItem(at: testDir) }
|
||||
|
||||
try "alpha content".write(to: testDir.appendingPathComponent("alpha.txt"), atomically: true, encoding: .utf8)
|
||||
try "beta content".write(to: testDir.appendingPathComponent("beta.txt"), atomically: true, encoding: .utf8)
|
||||
|
||||
let files: [FilePath] = [
|
||||
FilePath(testDir.appendingPathComponent("alpha.txt").path),
|
||||
FilePath(testDir.appendingPathComponent("beta.txt").path),
|
||||
]
|
||||
let archiveURL = testDir.appendingPathComponent("test.tar.gz")
|
||||
let writer = try ArchiveWriter(format: .pax, filter: .gzip, file: archiveURL)
|
||||
try writer.archive(files, base: FilePath(testDir.path))
|
||||
try writer.finishEncoding()
|
||||
|
||||
var entries: [String: String] = [:]
|
||||
let reader = try ArchiveReader(file: archiveURL)
|
||||
for (entry, data) in reader {
|
||||
if let path = entry.path, let content = String(data: data, encoding: .utf8) {
|
||||
entries[path] = content
|
||||
}
|
||||
}
|
||||
#expect(entries["alpha.txt"] == "alpha content")
|
||||
#expect(entries["beta.txt"] == "beta content")
|
||||
}
|
||||
|
||||
@Test func archiveURLsEmpty() throws {
|
||||
let testDir = createTemporaryDirectory(baseName: "ArchiveTests.archiveURLsEmpty")!
|
||||
defer { try? FileManager.default.removeItem(at: testDir) }
|
||||
|
||||
let archiveURL = testDir.appendingPathComponent("test.tar.gz")
|
||||
let writer = try ArchiveWriter(format: .pax, filter: .gzip, file: archiveURL)
|
||||
#expect(throws: Never.self) {
|
||||
try writer.archive([], base: FilePath(testDir.path))
|
||||
}
|
||||
try writer.finishEncoding()
|
||||
|
||||
var count = 0
|
||||
let reader = try ArchiveReader(file: archiveURL)
|
||||
for _ in reader { count += 1 }
|
||||
#expect(count == 0)
|
||||
}
|
||||
|
||||
@Test func archiveURLsSingle() throws {
|
||||
let testDir = createTemporaryDirectory(baseName: "ArchiveTests.archiveURLsSingle")!
|
||||
defer { try? FileManager.default.removeItem(at: testDir) }
|
||||
|
||||
try "only content".write(to: testDir.appendingPathComponent("only.txt"), atomically: true, encoding: .utf8)
|
||||
|
||||
let archiveURL = testDir.appendingPathComponent("test.tar.gz")
|
||||
let writer = try ArchiveWriter(format: .pax, filter: .gzip, file: archiveURL)
|
||||
try writer.archive([FilePath(testDir.appendingPathComponent("only.txt").path)], base: FilePath(testDir.path))
|
||||
try writer.finishEncoding()
|
||||
|
||||
var entries: [String: String] = [:]
|
||||
let reader = try ArchiveReader(file: archiveURL)
|
||||
for (entry, data) in reader {
|
||||
if let path = entry.path, let content = String(data: data, encoding: .utf8) {
|
||||
entries[path] = content
|
||||
}
|
||||
}
|
||||
#expect(entries.count == 1)
|
||||
#expect(entries["only.txt"] == "only content")
|
||||
}
|
||||
|
||||
@Test func archiveURLsPreservesPermissions() throws {
|
||||
let testDir = createTemporaryDirectory(baseName: "ArchiveTests.archiveURLsPerms")!
|
||||
defer { try? FileManager.default.removeItem(at: testDir) }
|
||||
|
||||
let execFile = testDir.appendingPathComponent("script.sh")
|
||||
try "#!/bin/sh\necho hi".write(to: execFile, atomically: true, encoding: .utf8)
|
||||
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: execFile.path)
|
||||
|
||||
let archiveURL = testDir.appendingPathComponent("test.tar.gz")
|
||||
let writer = try ArchiveWriter(format: .pax, filter: .gzip, file: archiveURL)
|
||||
try writer.archive([FilePath(execFile.path)], base: FilePath(testDir.path))
|
||||
try writer.finishEncoding()
|
||||
|
||||
let extractDir = testDir.appendingPathComponent("extract")
|
||||
let reader = try ArchiveReader(file: archiveURL)
|
||||
let rejected = try reader.extractContents(to: extractDir)
|
||||
|
||||
#expect(rejected.isEmpty)
|
||||
let attrs = try FileManager.default.attributesOfItem(atPath: extractDir.appendingPathComponent("script.sh").path)
|
||||
let perms = (attrs[.posixPermissions] as? NSNumber)?.uint16Value ?? 0
|
||||
#expect((perms & 0o777) == 0o755)
|
||||
}
|
||||
|
||||
@Test func archiveURLsNestedDirectories() throws {
|
||||
let testDir = createTemporaryDirectory(baseName: "ArchiveTests.archiveURLsNested")!
|
||||
defer { try? FileManager.default.removeItem(at: testDir) }
|
||||
|
||||
let sourceDir = testDir.appendingPathComponent("source")
|
||||
try FileManager.default.createDirectory(at: sourceDir.appendingPathComponent("a"), withIntermediateDirectories: true)
|
||||
try FileManager.default.createDirectory(at: sourceDir.appendingPathComponent("b/c"), withIntermediateDirectories: true)
|
||||
try "top content".write(to: sourceDir.appendingPathComponent("top.txt"), atomically: true, encoding: .utf8)
|
||||
try "a content".write(to: sourceDir.appendingPathComponent("a/deep.txt"), atomically: true, encoding: .utf8)
|
||||
try "nested content".write(to: sourceDir.appendingPathComponent("b/c/nested.txt"), atomically: true, encoding: .utf8)
|
||||
|
||||
let files: [FilePath] = [
|
||||
FilePath(sourceDir.appendingPathComponent("top.txt").path),
|
||||
FilePath(sourceDir.appendingPathComponent("a/deep.txt").path),
|
||||
FilePath(sourceDir.appendingPathComponent("b/c/nested.txt").path),
|
||||
]
|
||||
let archiveURL = testDir.appendingPathComponent("test.tar.gz")
|
||||
let writer = try ArchiveWriter(format: .pax, filter: .gzip, file: archiveURL)
|
||||
try writer.archive(files, base: FilePath(sourceDir.path))
|
||||
try writer.finishEncoding()
|
||||
|
||||
let extractDir = testDir.appendingPathComponent("extract")
|
||||
let reader = try ArchiveReader(file: archiveURL)
|
||||
let rejected = try reader.extractContents(to: extractDir)
|
||||
|
||||
#expect(rejected.isEmpty)
|
||||
#expect(try String(contentsOf: extractDir.appendingPathComponent("top.txt"), encoding: .utf8) == "top content")
|
||||
#expect(try String(contentsOf: extractDir.appendingPathComponent("a/deep.txt"), encoding: .utf8) == "a content")
|
||||
#expect(try String(contentsOf: extractDir.appendingPathComponent("b/c/nested.txt"), encoding: .utf8) == "nested content")
|
||||
}
|
||||
|
||||
@Test func archiveURLsWithDirectoryURL() throws {
|
||||
let testDir = createTemporaryDirectory(baseName: "ArchiveTests.archiveURLsWithDir")!
|
||||
defer { try? FileManager.default.removeItem(at: testDir) }
|
||||
|
||||
let sourceDir = testDir.appendingPathComponent("source")
|
||||
try FileManager.default.createDirectory(at: sourceDir, withIntermediateDirectories: true)
|
||||
try "top content".write(to: sourceDir.appendingPathComponent("top.txt"), atomically: true, encoding: .utf8)
|
||||
|
||||
let subDir = sourceDir.appendingPathComponent("subdir")
|
||||
try FileManager.default.createDirectory(at: subDir.appendingPathComponent("nested"), withIntermediateDirectories: true)
|
||||
try "sub content".write(to: subDir.appendingPathComponent("sub.txt"), atomically: true, encoding: .utf8)
|
||||
try "deep content".write(to: subDir.appendingPathComponent("nested/deep.txt"), atomically: true, encoding: .utf8)
|
||||
|
||||
let urls: [FilePath] = [
|
||||
FilePath(sourceDir.appendingPathComponent("top.txt").path),
|
||||
FilePath(subDir.path),
|
||||
]
|
||||
let archiveURL = testDir.appendingPathComponent("test.tar.gz")
|
||||
let writer = try ArchiveWriter(format: .pax, filter: .gzip, file: archiveURL)
|
||||
try writer.archive(urls, base: FilePath(sourceDir.path))
|
||||
try writer.finishEncoding()
|
||||
|
||||
let extractDir = testDir.appendingPathComponent("extract")
|
||||
let reader = try ArchiveReader(file: archiveURL)
|
||||
let rejected = try reader.extractContents(to: extractDir)
|
||||
|
||||
#expect(rejected.isEmpty)
|
||||
#expect(try String(contentsOf: extractDir.appendingPathComponent("top.txt"), encoding: .utf8) == "top content")
|
||||
#expect(try String(contentsOf: extractDir.appendingPathComponent("subdir/sub.txt"), encoding: .utf8) == "sub content")
|
||||
#expect(try String(contentsOf: extractDir.appendingPathComponent("subdir/nested/deep.txt"), encoding: .utf8) == "deep content")
|
||||
}
|
||||
|
||||
@Test func archiveURLsDirectoryPreservesPermissions() throws {
|
||||
let testDir = createTemporaryDirectory(baseName: "ArchiveTests.archiveURLsDirPerms")!
|
||||
defer { try? FileManager.default.removeItem(at: testDir) }
|
||||
|
||||
let sourceDir = testDir.appendingPathComponent("source")
|
||||
let readonlyDir = sourceDir.appendingPathComponent("readonly")
|
||||
try FileManager.default.createDirectory(at: readonlyDir, withIntermediateDirectories: true)
|
||||
try "content".write(to: readonlyDir.appendingPathComponent("file.txt"), atomically: true, encoding: .utf8)
|
||||
try FileManager.default.setAttributes([.posixPermissions: 0o777], ofItemAtPath: readonlyDir.path)
|
||||
defer {
|
||||
try? FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: readonlyDir.path)
|
||||
}
|
||||
|
||||
let archiveURL = testDir.appendingPathComponent("test.tar.gz")
|
||||
let writer = try ArchiveWriter(format: .pax, filter: .gzip, file: archiveURL)
|
||||
try writer.archive([FilePath(readonlyDir.path)], base: FilePath(sourceDir.path))
|
||||
try writer.finishEncoding()
|
||||
|
||||
let extractDir = testDir.appendingPathComponent("extract")
|
||||
let reader = try ArchiveReader(file: archiveURL)
|
||||
let rejected = try reader.extractContents(to: extractDir)
|
||||
|
||||
#expect(rejected.isEmpty)
|
||||
#expect(
|
||||
try String(contentsOf: extractDir.appendingPathComponent("readonly/file.txt"), encoding: .utf8)
|
||||
== "content")
|
||||
let attrs = try FileManager.default.attributesOfItem(atPath: extractDir.appendingPathComponent("readonly").path)
|
||||
let perms = (attrs[.posixPermissions] as? NSNumber)?.uint16Value ?? 0
|
||||
#expect((perms & 0o777) == 0o777, "Read-only directory permissions should be preserved")
|
||||
}
|
||||
|
||||
@Test func archiveURLsSymlinks() throws {
|
||||
let testDir = createTemporaryDirectory(baseName: "ArchiveTests.archiveURLsSymlinks")!
|
||||
defer { try? FileManager.default.removeItem(at: testDir) }
|
||||
|
||||
let sourceDir = testDir.appendingPathComponent("source")
|
||||
try FileManager.default.createDirectory(at: sourceDir, withIntermediateDirectories: true)
|
||||
|
||||
let fileURL = sourceDir.appendingPathComponent("file.txt")
|
||||
try "symlink content".write(to: fileURL, atomically: true, encoding: .utf8)
|
||||
|
||||
try FileManager.default.createSymbolicLink(
|
||||
atPath: sourceDir.appendingPathComponent("absolute").path,
|
||||
withDestinationPath: fileURL.path
|
||||
)
|
||||
try FileManager.default.createSymbolicLink(
|
||||
atPath: sourceDir.appendingPathComponent("relative").path,
|
||||
withDestinationPath: "file.txt"
|
||||
)
|
||||
|
||||
let archiveURL = testDir.appendingPathComponent("test.tar.gz")
|
||||
let writer = try ArchiveWriter(format: .pax, filter: .gzip, file: archiveURL)
|
||||
try writer.archive([FilePath(sourceDir.path)], base: FilePath(testDir.path))
|
||||
try writer.finishEncoding()
|
||||
|
||||
let extractDir = testDir.appendingPathComponent("extract")
|
||||
let reader = try ArchiveReader(file: archiveURL)
|
||||
let rejected = try reader.extractContents(to: extractDir)
|
||||
|
||||
#expect(rejected.isEmpty)
|
||||
|
||||
let extractedSource = extractDir.appendingPathComponent("source")
|
||||
#expect(
|
||||
try String(contentsOf: extractedSource.appendingPathComponent("file.txt"), encoding: .utf8)
|
||||
== "symlink content")
|
||||
|
||||
let relTarget = try FileManager.default.destinationOfSymbolicLink(
|
||||
atPath: extractedSource.appendingPathComponent("relative").path)
|
||||
#expect(relTarget == "file.txt")
|
||||
#expect(
|
||||
try String(contentsOf: extractedSource.appendingPathComponent("relative"), encoding: .utf8)
|
||||
== "symlink content")
|
||||
|
||||
let absTarget = try FileManager.default.destinationOfSymbolicLink(
|
||||
atPath: extractedSource.appendingPathComponent("absolute").path)
|
||||
|
||||
print("absTarget: \(absTarget), fileURL: \(fileURL.path)")
|
||||
#expect(absTarget == fileURL.path)
|
||||
}
|
||||
|
||||
@Test func archiveDirectorySymlinkRelativeSubdir() throws {
|
||||
let testDir = createTemporaryDirectory(baseName: "ArchiveTests.archiveDirSymlinkRelSubdir")!
|
||||
defer { try? FileManager.default.removeItem(at: testDir) }
|
||||
|
||||
Reference in new issue
Block a user