Skip to content

Commit a25f55e

Browse files
committed
ArchiveWriter: preserve symlink targets verbatim
1 parent e36060c commit a25f55e

2 files changed

Lines changed: 12 additions & 100 deletions

File tree

Sources/ContainerizationArchive/ArchiveWriter.swift

Lines changed: 6 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -209,12 +209,8 @@ extension ArchiveWriter {
209209
extension ArchiveWriter {
210210
/// Archives an explicit, ordered list of host filesystem entries.
211211
public func archiveEntries(_ entries: [ArchiveSourceEntry]) throws {
212-
let archivedPathsByHostPath = entries.reduce(into: [String: [String]]()) { result, entry in
213-
result[entry.pathOnHost.path, default: []].append(entry.pathInArchive)
214-
}
215-
216212
for source in entries {
217-
guard let entry = try Self.makeEntry(from: source, archivedPathsByHostPath: archivedPathsByHostPath) else {
213+
guard let entry = try Self.makeEntry(from: source) else {
218214
throw ArchiveError.failedToCreateArchive("unsupported file type at '\(source.pathOnHost.path)'")
219215
}
220216
try self.writeSourceEntry(entry: entry, sourcePath: source.pathOnHost.path)
@@ -304,12 +300,8 @@ extension ArchiveWriter {
304300
guard resolvedFull.starts(with: dirPath) else {
305301
continue
306302
}
307-
entry.symlinkTarget = Self.rewriteArchivedDirectorySymlinkTarget(
308-
targetPath,
309-
sourceEntryPath: relativePath,
310-
sourceRoot: dirPath,
311-
resolvedTargetPath: resolvedFull
312-
)
303+
// Match Docker build-context semantics and preserve the original target verbatim.
304+
entry.symlinkTarget = targetPath
313305
}
314306

315307
entry.path = relativePath
@@ -405,8 +397,7 @@ extension ArchiveWriter {
405397
}
406398

407399
private static func makeEntry(
408-
from source: ArchiveSourceEntry,
409-
archivedPathsByHostPath: [String: [String]]
400+
from source: ArchiveSourceEntry
410401
) throws -> WriteEntry? {
411402
guard let status = try Self.fileStatus(atPath: source.pathOnHost.path) else {
412403
return nil
@@ -423,11 +414,8 @@ extension ArchiveWriter {
423414
case .symbolicLink:
424415
entry.fileType = .symbolicLink
425416
entry.size = 0
426-
entry.symlinkTarget = Self.rewriteArchivedAbsoluteSymlinkTarget(
427-
status.symlinkTarget ?? "",
428-
sourceEntryPath: source.pathInArchive,
429-
archivedPathsByHostPath: archivedPathsByHostPath
430-
)
417+
// Match Docker build-context semantics and preserve the original target verbatim.
418+
entry.symlinkTarget = status.symlinkTarget
431419
}
432420

433421
entry.path = source.pathInArchive
@@ -537,30 +525,6 @@ extension ArchiveWriter {
537525
#endif
538526
}
539527

540-
private static func rewriteArchivedAbsoluteSymlinkTarget(
541-
_ symlinkTarget: String,
542-
sourceEntryPath: String,
543-
archivedPathsByHostPath: [String: [String]]
544-
) -> String {
545-
guard symlinkTarget.hasPrefix("/") else {
546-
return symlinkTarget
547-
}
548-
549-
let targetPath = URL(fileURLWithPath: symlinkTarget)
550-
.standardizedFileURL
551-
.resolvingSymlinksInPath()
552-
.path
553-
guard let targetArchivePaths = archivedPathsByHostPath[targetPath],
554-
targetArchivePaths.count == 1,
555-
let targetArchivePath = targetArchivePaths.first
556-
else {
557-
return symlinkTarget
558-
}
559-
560-
let sourceDirectory = (sourceEntryPath as NSString).deletingLastPathComponent
561-
return Self.relativeArchivePath(fromDirectory: sourceDirectory, to: targetArchivePath)
562-
}
563-
564528
private static func resolveArchivedDirectorySymlinkTarget(
565529
_ symlinkTarget: String,
566530
symlinkPath: FilePath
@@ -576,56 +540,4 @@ extension ArchiveWriter {
576540
let symlinkParent = symlinkPath.removingLastComponent()
577541
return symlinkParent.appending(symlinkTarget).lexicallyNormalized()
578542
}
579-
580-
private static func rewriteArchivedDirectorySymlinkTarget(
581-
_ symlinkTarget: String,
582-
sourceEntryPath: String,
583-
sourceRoot: FilePath,
584-
resolvedTargetPath: FilePath
585-
) -> String {
586-
guard symlinkTarget.hasPrefix("/"),
587-
let targetArchivePath = Self.relativePath(path: resolvedTargetPath.string, within: sourceRoot.string)
588-
else {
589-
return symlinkTarget
590-
}
591-
592-
let sourceDirectory = (sourceEntryPath as NSString).deletingLastPathComponent
593-
return Self.relativeArchivePath(fromDirectory: sourceDirectory, to: targetArchivePath)
594-
}
595-
596-
private static func relativePath(path: String, within root: String) -> String? {
597-
if path == root {
598-
return ""
599-
}
600-
601-
let rootPrefix = root.hasSuffix("/") ? root : root + "/"
602-
guard path.hasPrefix(rootPrefix) else {
603-
return nil
604-
}
605-
return String(path.dropFirst(rootPrefix.count))
606-
}
607-
608-
private static func relativeArchivePath(fromDirectory: String, to path: String) -> String {
609-
let fromComponents = Self.archivePathComponents(fromDirectory)
610-
let toComponents = Self.archivePathComponents(path)
611-
612-
var commonPrefixCount = 0
613-
while commonPrefixCount < fromComponents.count,
614-
commonPrefixCount < toComponents.count,
615-
fromComponents[commonPrefixCount] == toComponents[commonPrefixCount]
616-
{
617-
commonPrefixCount += 1
618-
}
619-
620-
let upwardTraversal = Array(repeating: "..", count: fromComponents.count - commonPrefixCount)
621-
let remainder = Array(toComponents.dropFirst(commonPrefixCount))
622-
let relativeComponents = upwardTraversal + remainder
623-
return relativeComponents.isEmpty ? "." : relativeComponents.joined(separator: "/")
624-
}
625-
626-
private static func archivePathComponents(_ path: String) -> [String] {
627-
NSString(string: path).pathComponents.filter { component in
628-
component != "/" && component != "."
629-
}
630-
}
631543
}

Tests/ContainerizationArchiveTests/ArchiveTests.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ struct ArchiveTests {
354354
#expect(linkDest == "target.txt")
355355
}
356356

357-
@Test func archiveDirectoryRewritesInternalAbsoluteSymlink() throws {
357+
@Test func archiveDirectoryPreservesInternalAbsoluteSymlink() throws {
358358
let testDir = createTemporaryDirectory(baseName: "ArchiveTests.archiveDirAbsoluteSymlink")!
359359
defer { try? FileManager.default.removeItem(at: testDir) }
360360

@@ -379,7 +379,7 @@ struct ArchiveTests {
379379
#expect(rejected.isEmpty)
380380
let extractedLink = extractDir.appendingPathComponent("link.txt")
381381
let linkDest = try FileManager.default.destinationOfSymbolicLink(atPath: extractedLink.path)
382-
#expect(linkDest == "target.txt")
382+
#expect(linkDest == targetURL.path)
383383
#expect(try String(contentsOf: extractedLink, encoding: .utf8) == "target content")
384384
}
385385

@@ -629,7 +629,7 @@ struct ArchiveTests {
629629
#expect(content == "in a")
630630
}
631631

632-
@Test func archiveEntriesRewritesInternalAbsoluteSymlink() throws {
632+
@Test func archiveEntriesPreservesInternalAbsoluteSymlink() throws {
633633
let testDir = createTemporaryDirectory(baseName: "ArchiveTests.archiveEntriesInternalAbsolute")!
634634
defer { try? FileManager.default.removeItem(at: testDir) }
635635

@@ -655,7 +655,7 @@ struct ArchiveTests {
655655
#expect(rejected.isEmpty)
656656
let extractedLink = extractDir.appendingPathComponent("link.txt")
657657
let linkDest = try FileManager.default.destinationOfSymbolicLink(atPath: extractedLink.path)
658-
#expect(linkDest == "target.txt")
658+
#expect(linkDest == targetURL.path)
659659
#expect(try String(contentsOf: extractedLink, encoding: .utf8) == "hello")
660660
}
661661

@@ -687,7 +687,7 @@ struct ArchiveTests {
687687
#expect(linkDest == externalTargetURL.path)
688688
}
689689

690-
@Test func archiveEntriesCanonicalizesInternalAbsoluteSymlinkThroughAncestorSymlink() throws {
690+
@Test func archiveEntriesPreservesAbsoluteSymlinkThroughAncestorSymlink() throws {
691691
let testDir = createTemporaryDirectory(baseName: "ArchiveTests.archiveEntriesCanonicalAbsolute")!
692692
defer { try? FileManager.default.removeItem(at: testDir) }
693693

@@ -724,7 +724,7 @@ struct ArchiveTests {
724724
#expect(rejected.isEmpty)
725725
let extractedLink = extractDir.appendingPathComponent("link.txt")
726726
let linkDest = try FileManager.default.destinationOfSymbolicLink(atPath: extractedLink.path)
727-
#expect(linkDest == "real/target.txt")
727+
#expect(linkDest == sourceDir.appendingPathComponent("alias/target.txt").path)
728728
#expect(try String(contentsOf: extractedLink, encoding: .utf8) == "hello")
729729
}
730730
}

0 commit comments

Comments
 (0)