[Build] purely-lexical URL operations for resolving symlinks (#31)

`BuildFSSync.walk` relied on an implementation detail to skip traversing
the path ".", relative to the build context dir.

Specifically, it assumed that:
- `URL.parentOf()` would return false when the parent argument was
relative.
- `URL.path(percentEncoded: false)` would preserve the relativity of an
input path.
These assumptions held on earlier macOS releases, so the context
directory silently remained untouched.

In some versions of macOS , `URL.path(percentEncoded: false)` always
returns an absolute path, regardless of how the URL was created. Because
of this change, `URL.parentOf()` now returns true for ".", and
BuildFSSync.walk begins descending into the context directory—something
it was never meant to do.

This solution:
- Remove the hidden dependency on `URL.parentOf()` for context‑directory
detection.
- Add an explicit check for "." inside BuildFSSync.walk and bail out
early.

Replace fragile calls to `URL.path(percentEncoded: false`) with the
stable, documented `URL.relativePath`, which preserves relativity when
the original path was relative.
This commit is contained in:
Sidhartha Mani
2025-06-07 15:32:06 -07:00
committed by GitHub
parent f86dcefb04
commit 7020082723
3 changed files with 80 additions and 70 deletions
+21 -3
View File
@@ -65,7 +65,15 @@ actor BuildFSSync: BuildPipelineHandler {
func read(_ sender: AsyncStream<ClientStream>.Continuation, _ packet: BuildTransfer, _ buildID: String) async throws {
let offset: UInt64 = packet.offset() ?? 0
let size: Int = packet.len() ?? 0
var path: URL = URL(filePath: packet.source.cleanPathComponent)
var path: URL
if packet.source.hasPrefix("/") {
path = URL(fileURLWithPath: packet.source).standardizedFileURL
} else {
path =
contextDir
.appendingPathComponent(packet.source)
.standardizedFileURL
}
if !FileManager.default.fileExists(atPath: path.cleanPath) {
path = URL(filePath: self.contextDir.cleanPath)
path.append(components: packet.source.cleanPathComponent)
@@ -87,8 +95,15 @@ actor BuildFSSync: BuildPipelineHandler {
}
func info(_ sender: AsyncStream<ClientStream>.Continuation, _ packet: BuildTransfer, _ buildID: String) async throws {
var path = self.contextDir
path.append(components: packet.source.cleanPathComponent)
let path: URL
if packet.source.hasPrefix("/") {
path = URL(fileURLWithPath: packet.source).standardizedFileURL
} else {
path =
contextDir
.appendingPathComponent(packet.source)
.standardizedFileURL
}
let transfer = try path.buildTransfer(id: packet.id, contextDir: self.contextDir, complete: true)
var response = ClientStream()
response.buildID = buildID
@@ -123,6 +138,9 @@ actor BuildFSSync: BuildPipelineHandler {
let followPathsWalked = try walk(root: self.contextDir, includePatterns: followPaths)
for url in followPathsWalked {
guard self.contextDir.absoluteURL.cleanPath != url.absoluteURL.cleanPath else {
continue
}
guard self.contextDir.parentOf(url) else {
continue
}
+59 -54
View File
@@ -14,77 +14,82 @@
// limitations under the License.
//===----------------------------------------------------------------------===//
//
import Foundation
extension URL {
func parentOf(_ url: URL) -> Bool {
// if self is a relative path
guard self.cleanPath.hasPrefix("/") else {
return true
}
let pathItems = self.standardizedFileURL.absoluteURL.pathComponents.map { $0.cleanPathComponent }
let urlItems = url.standardizedFileURL.absoluteURL.pathComponents.map { $0.cleanPathComponent }
extension String {
fileprivate var fs_cleaned: String {
var value = self
if pathItems.count > urlItems.count {
return false
if value.hasPrefix("file://") {
value.removeFirst("file://".count)
}
for (index, pathItem) in pathItems.enumerated() {
if urlItems[index] != pathItem {
return false
if value.count > 1 && value.last == "/" {
value.removeLast()
}
return value.removingPercentEncoding ?? value
}
fileprivate var fs_components: [String] {
var parts: [String] = []
for segment in self.split(separator: "/", omittingEmptySubsequences: true) {
switch segment {
case ".":
continue
case "..":
if !parts.isEmpty { parts.removeLast() }
default:
parts.append(String(segment))
}
}
return true
return parts
}
fileprivate var fs_isAbsolute: Bool { first == "/" }
}
extension URL {
var cleanPath: String {
self.path.fs_cleaned
}
func parentOf(_ url: URL) -> Bool {
let parentPath = self.absoluteURL.cleanPath
let childPath = url.absoluteURL.cleanPath
guard parentPath.fs_isAbsolute else {
return true
}
let parentParts = parentPath.fs_components
let childParts = childPath.fs_components
guard parentParts.count <= childParts.count else { return false }
return zip(parentParts, childParts).allSatisfy { $0 == $1 }
}
func relativeChildPath(to context: URL) throws -> String {
if !context.parentOf(self.absoluteURL.standardizedFileURL) {
throw BuildFSSync.Error.pathIsNotChild(self.cleanPath, context.cleanPath)
guard context.parentOf(self) else {
throw BuildFSSync.Error.pathIsNotChild(cleanPath, context.cleanPath)
}
let pathItems = context.standardizedFileURL.pathComponents.map { $0.cleanPathComponent }
let urlItems = self.standardizedFileURL.pathComponents.map { $0.cleanPathComponent }
let ctxParts = context.cleanPath.fs_components
let selfParts = cleanPath.fs_components
return String(urlItems.dropFirst(pathItems.count).joined(separator: "/").trimming { $0 == "/" })
}
var cleanPath: String {
let pathStr = self.path(percentEncoded: false)
if let cleanPath = pathStr.removingPercentEncoding {
return cleanPath
}
return pathStr
return selfParts.dropFirst(ctxParts.count).joined(separator: "/")
}
func relativePathFrom(from base: URL) -> String {
let destComponents = self.standardizedFileURL.pathComponents.map { $0.cleanPathComponent }
let baseComponents = base.standardizedFileURL.pathComponents.map { $0.cleanPathComponent }
let destParts = cleanPath.fs_components
let baseParts = base.cleanPath.fs_components
// Find the last common path between the two
var lastCommon: Int = 0
while lastCommon < baseComponents.count && lastCommon < destComponents.count && baseComponents[lastCommon] == destComponents[lastCommon] {
lastCommon += 1
}
let common = zip(destParts, baseParts).prefix { $0 == $1 }.count
guard common > 0 else { return cleanPath }
if lastCommon == 0 {
return self.path
}
var relPath: [String] = []
// Add "../" for each component that's a directory after the common prefix
for i in lastCommon..<baseComponents.count {
let sub = baseComponents[0...i]
let currentPath = URL(filePath: sub.joined(separator: "/"))
let resourceValues: URLResourceValues? = try? currentPath.resourceValues(forKeys: [.isDirectoryKey])
if case let isDirectory = resourceValues?.isDirectory, isDirectory == true {
relPath.append("..")
}
}
relPath.append(contentsOf: destComponents[lastCommon...])
return relPath.joined(separator: "/")
let ups = Array(repeating: "..", count: baseParts.count - common)
let remainder = destParts.dropFirst(common)
return (ups + remainder).joined(separator: "/")
}
func zeroCopyReader(
@@ -14,8 +14,6 @@
// limitations under the License.
//===----------------------------------------------------------------------===//
//
import Foundation
import Testing
@@ -185,17 +183,6 @@ import Testing
#expect(false == fileURL.parentOf(httpURL))
}
@Test func testParentOfRelativePaths() throws {
let absoluteChildDir = baseTempURL.appendingPathComponent("someDir")
try createDirectory(at: absoluteChildDir)
let relativeSelfURL = URL(fileURLWithPath: "a/relative/path")
#expect(relativeSelfURL.parentOf(absoluteChildDir))
let potentiallyParentRelative = URL(fileURLWithPath: baseTempURL.lastPathComponent)
#expect(potentiallyParentRelative.parentOf(absoluteChildDir))
}
// MARK: - relativeChildPath Tests
@Test func testRelativeChildPathDirectChild() throws {
let parentDir = baseTempURL.appendingPathComponent("dir1")
let childFile = parentDir.appendingPathComponent("dir2").appendingPathComponent("file")