mirror of
https://github.com/apple/container.git
synced 2026-10-13 17:49:16 +00:00
Fix Globber dropping directory contents under a symlinked ancestor (#2252)
This commit is contained in:
1 parent
4e7dce66ad
commit
09d8e6aeea
2 files changed
+35
-9
No files matched your search
@@ -83,19 +83,17 @@ public class Globber {
|
||||
/// (same as a regular file) so pattern components after it never match —
|
||||
/// mirrors the containment check `BuildFSSync` applies before reading.
|
||||
///
|
||||
/// Children are named by their resolved (physical) path, not by `url`, so
|
||||
/// that `walk(root:includePatterns:)`'s later filter — which is driven by
|
||||
/// `Archiver.compress`'s own physical directory walk — reliably finds a
|
||||
/// matching entry regardless of whether that walk itself follows `url`'s
|
||||
/// symlink. `url` is separately inserted into `results` so the symlink
|
||||
/// entry is still present in the tar for the builder to resolve the
|
||||
/// original path against.
|
||||
/// Children are named by appending each entry's basename onto `dir`
|
||||
/// rather than using `contentsOfDirectory(at:)`'s own URLs, which silently
|
||||
/// re-resolve `dir` (e.g. `/tmp` → `/private/tmp`) even when `dir` itself
|
||||
/// isn't a symlink, breaking the lexical `parentOf` containment checks
|
||||
/// downstream.
|
||||
private func children(of url: URL) -> [URL] {
|
||||
// TODO: modifying object state and returning results is odd, rework
|
||||
guard let dir = self.resolvedDirectory(of: url) else { return [] }
|
||||
if url.isSymlink { self.results.insert(url) }
|
||||
return (try? FileManager.default.contentsOfDirectory(at: dir, includingPropertiesForKeys: nil))
|
||||
?? []
|
||||
let names = (try? FileManager.default.contentsOfDirectory(atPath: dir.path)) ?? []
|
||||
return names.map { dir.appendingPathComponent($0) }
|
||||
}
|
||||
|
||||
/// Recursive form of ``children(of:)``, used once a full pattern (or `**`)
|
||||
|
||||
@@ -256,6 +256,34 @@ import Testing
|
||||
#expect(leaked.isEmpty, "walk() returned non-symlink URLs that physically resolve outside the context: \(leaked)")
|
||||
}
|
||||
|
||||
// MARK: - walk(): plain subdirectory contents under a symlinked ancestor
|
||||
//
|
||||
// Globber.children(of:) used to list directories via
|
||||
// contentsOfDirectory(at:), which silently resolves /tmp to /private/tmp
|
||||
// even for a non-symlink child. That made walk()'s parentOf() check
|
||||
// reject every entry found by descending into a plain subdirectory, so
|
||||
// "COPY foodir /dest" tarred an empty directory while
|
||||
// "COPY foodir/payload /dest/payload" worked fine.
|
||||
|
||||
@Test func testWalkUnderSymlinkedAncestor() async throws {
|
||||
// Root the context directly under /tmp (a symlink to /private/tmp on
|
||||
// macOS) to reproduce the bug precisely.
|
||||
let tmpBase = URL(fileURLWithPath: "/tmp").appendingPathComponent(UUID().uuidString)
|
||||
let ctx = tmpBase.appendingPathComponent("context")
|
||||
try fm.createDirectory(at: ctx, withIntermediateDirectories: true)
|
||||
defer { try? fm.removeItem(at: tmpBase) }
|
||||
|
||||
let subdir = ctx.appendingPathComponent("foodir")
|
||||
try fm.createDirectory(at: subdir, withIntermediateDirectories: true)
|
||||
try write("payload", to: subdir.appendingPathComponent("payload.txt"))
|
||||
|
||||
let fssync = try BuildFSSync(ctx)
|
||||
let infos = try await walkJSON(fssync, followPaths: ["foodir"])
|
||||
|
||||
let payload = infos.first { $0.name == "foodir/payload.txt" }
|
||||
#expect(payload != nil, "expected foodir/payload.txt to be included when COPYing a directory glob, got: \(infos.map { $0.name })")
|
||||
}
|
||||
|
||||
// MARK: - walk(): JSON/FileInfo metadata paths
|
||||
//
|
||||
// walk() has two response formats: a tar stream (the primary data path,
|
||||
|
||||
Reference in new issue
Block a user