diff --git a/Sources/ContainerBuild/BuildFSSync.swift b/Sources/ContainerBuild/BuildFSSync.swift index c921c5c2..a41c629f 100644 --- a/Sources/ContainerBuild/BuildFSSync.swift +++ b/Sources/ContainerBuild/BuildFSSync.swift @@ -65,7 +65,15 @@ actor BuildFSSync: BuildPipelineHandler { func read(_ sender: AsyncStream.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.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 } diff --git a/Sources/ContainerBuild/URL+Extensions.swift b/Sources/ContainerBuild/URL+Extensions.swift index 1f6ff564..75b736de 100644 --- a/Sources/ContainerBuild/URL+Extensions.swift +++ b/Sources/ContainerBuild/URL+Extensions.swift @@ -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..