Skip to content

Commit 7f7ae0e

Browse files
committed
Canonicalize archived absolute symlink targets
1 parent f543353 commit 7f7ae0e

2 files changed

Lines changed: 52 additions & 1 deletion

File tree

Sources/Services/ContainerAPIService/Client/Archiver.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,9 @@ public final class Archiver: Sendable {
353353
return symlinkTarget
354354
}
355355

356-
let targetPath = URL(fileURLWithPath: symlinkTarget).standardizedFileURL
356+
let targetPath = URL(fileURLWithPath: symlinkTarget)
357+
.standardizedFileURL
358+
.resolvingSymlinksInPath()
357359
guard let targetArchivePaths = archivedPathsByHostPath[targetPath], targetArchivePaths.count == 1, let targetArchivePath = targetArchivePaths.first else {
358360
return symlinkTarget
359361
}

Tests/ContainerAPIClientTests/ArchiverTests.swift

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,55 @@ struct ArchiverTests {
139139
#expect(try String(contentsOf: extractedLinkURL, encoding: .utf8) == "hello")
140140
}
141141

142+
@Test
143+
func testCompressAndUncompressRewritesArchivedAbsoluteSymbolicLinkTargetThroughSymlinkedAncestor() throws {
144+
let fileManager = FileManager.default
145+
let tempURL = try fileManager.url(
146+
for: .itemReplacementDirectory,
147+
in: .userDomainMask,
148+
appropriateFor: .temporaryDirectory,
149+
create: true
150+
)
151+
defer { try? fileManager.removeItem(at: tempURL) }
152+
153+
let sourceURL = tempURL.appendingPathComponent("source")
154+
let archiveURL = tempURL.appendingPathComponent("archive.tar.gz")
155+
let destinationURL = tempURL.appendingPathComponent("destination")
156+
try fileManager.createDirectory(at: sourceURL, withIntermediateDirectories: true)
157+
158+
let realDirectoryURL = sourceURL.appendingPathComponent("real")
159+
try fileManager.createDirectory(at: realDirectoryURL, withIntermediateDirectories: true)
160+
let targetURL = realDirectoryURL.appendingPathComponent("target.txt")
161+
try #require("hello".data(using: .utf8)).write(to: targetURL)
162+
163+
let aliasURL = sourceURL.appendingPathComponent("alias")
164+
try fileManager.createSymbolicLink(atPath: aliasURL.path, withDestinationPath: "real")
165+
166+
let linkURL = sourceURL.appendingPathComponent("link.txt")
167+
try fileManager.createSymbolicLink(
168+
atPath: linkURL.path,
169+
withDestinationPath: sourceURL.appendingPathComponent("alias/target.txt").path
170+
)
171+
172+
_ = try Archiver.compress(source: sourceURL, destination: archiveURL) { url in
173+
let sourcePath = sourceURL.standardizedFileURL.path
174+
let path = url.standardizedFileURL.path
175+
let relativePath = String(path.dropFirst(sourcePath.count + 1))
176+
return Archiver.ArchiveEntryInfo(
177+
pathOnHost: url,
178+
pathInArchive: URL(fileURLWithPath: relativePath)
179+
)
180+
}
181+
182+
try Archiver.uncompress(source: archiveURL, destination: destinationURL)
183+
184+
let extractedLinkURL = destinationURL.appendingPathComponent("link.txt")
185+
let values = try extractedLinkURL.resourceValues(forKeys: [.isSymbolicLinkKey])
186+
#expect(values.isSymbolicLink == true)
187+
#expect(try fileManager.destinationOfSymbolicLink(atPath: extractedLinkURL.path) == "real/target.txt")
188+
#expect(try String(contentsOf: extractedLinkURL, encoding: .utf8) == "hello")
189+
}
190+
142191
@Test
143192
func testCompressDigestChangesWhenSymlinkTargetChanges() throws {
144193
let fileManager = FileManager.default

0 commit comments

Comments
 (0)