mirror of
https://github.com/apple/container.git
synced 2026-08-24 10:05:43 -05:00
[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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user