From 20dc0bcfee931616a138caa57c60800fa0fc9f38 Mon Sep 17 00:00:00 2001 From: Danny Canter Date: Sun, 4 Jan 2026 11:11:09 -0800 Subject: [PATCH] Parser: Support relative paths for --volume (#1013) --- Sources/ContainerClient/Parser.swift | 4 +- Tests/ContainerClientTests/ParserTest.swift | 55 +++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/Sources/ContainerClient/Parser.swift b/Sources/ContainerClient/Parser.swift index 3613b60a..29e94f06 100644 --- a/Sources/ContainerClient/Parser.swift +++ b/Sources/ContainerClient/Parser.swift @@ -487,8 +487,8 @@ public struct Parser { let src = String(parts[0]) let dst = String(parts[1]) - // Check if it's an absolute directory path first - guard src.hasPrefix("/") else { + // Check if it's a filesystem path + guard src.contains("/") else { // Named volume - validate name syntax only guard VolumeStorage.isValidVolumeName(src) else { throw ContainerizationError(.invalidArgument, message: "invalid volume name '\(src)': must match \(VolumeStorage.volumeNamePattern)") diff --git a/Tests/ContainerClientTests/ParserTest.swift b/Tests/ContainerClientTests/ParserTest.swift index 334a2f64..f54527fd 100644 --- a/Tests/ContainerClientTests/ParserTest.swift +++ b/Tests/ContainerClientTests/ParserTest.swift @@ -382,6 +382,61 @@ struct ParserTest { } } + @Test + func testVolumeRelativePath() throws { + let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent("test-volume-rel-\(UUID().uuidString)") + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + defer { + try? FileManager.default.removeItem(at: tempDir) + } + + let originalDir = FileManager.default.currentDirectoryPath + FileManager.default.changeCurrentDirectoryPath(tempDir.path) + defer { + FileManager.default.changeCurrentDirectoryPath(originalDir) + } + + let result = try Parser.volume("./:/foo") + + switch result { + case .filesystem(let fs): + let expectedPath = URL(filePath: ".").absoluteURL.path + // Normalize trailing slashes for comparison + #expect(fs.source.trimmingCharacters(in: CharacterSet(charactersIn: "/")) == expectedPath.trimmingCharacters(in: CharacterSet(charactersIn: "/"))) + #expect(fs.destination == "/foo") + case .volume: + #expect(Bool(false), "Expected filesystem mount, got volume") + } + } + + @Test + func testVolumeRelativePathNested() throws { + let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent("test-volume-rel-nested-\(UUID().uuidString)") + let nestedDir = tempDir.appendingPathComponent("subdir") + try FileManager.default.createDirectory(at: nestedDir, withIntermediateDirectories: true) + defer { + try? FileManager.default.removeItem(at: tempDir) + } + + let originalDir = FileManager.default.currentDirectoryPath + FileManager.default.changeCurrentDirectoryPath(tempDir.path) + defer { + FileManager.default.changeCurrentDirectoryPath(originalDir) + } + + let result = try Parser.volume("./subdir:/foo") + + switch result { + case .filesystem(let fs): + let expectedPath = URL(filePath: "./subdir").absoluteURL.path + // Normalize trailing slashes for comparison + #expect(fs.source.trimmingCharacters(in: CharacterSet(charactersIn: "/")) == expectedPath.trimmingCharacters(in: CharacterSet(charactersIn: "/"))) + #expect(fs.destination == "/foo") + case .volume: + #expect(Bool(false), "Expected filesystem mount, got volume") + } + } + @Test func testIsValidDomainNameOk() throws { let names = [