diff --git a/Sources/Services/ContainerAPIService/Client/Parser.swift b/Sources/Services/ContainerAPIService/Client/Parser.swift index 559796bca..2306da739 100644 --- a/Sources/Services/ContainerAPIService/Client/Parser.swift +++ b/Sources/Services/ContainerAPIService/Client/Parser.swift @@ -318,7 +318,7 @@ public struct Parser { let rlimits = try Parser.rlimits(processFlags.ulimits) return .init( - executable: commandToRun.first!, + executable: commandToRun[0], arguments: [String](commandToRun.dropFirst()), environment: envvars, workingDirectory: workingDir, @@ -340,11 +340,31 @@ public struct Parser { public static let defaultDirectives = ["type": "virtiofs"] public static func tmpfsMounts(_ mounts: [String]) throws -> [Filesystem] { - let mounts = mounts.dedupe() var result: [Filesystem] = [] result.reserveCapacity(mounts.count) + var seenDestinations: Set = [] + for tmpfs in mounts { - let fs = Filesystem.tmpfs(destination: tmpfs, options: []) + let parts = tmpfs.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false) + let destination = String(parts[0]) + let options = parts.count == 2 ? String(parts[1]).split(separator: ",").map(String.init) : [] + + if destination.isEmpty { + throw ContainerizationError(.invalidArgument, message: "mount destination cannot be empty") + } + + let filePath = FilePath(destination) + guard filePath.isAbsolute else { + throw ContainerizationError(.invalidArgument, message: "\(destination) is not an absolute path") + } + + let normalizedDest = filePath.lexicallyNormalized().string + if seenDestinations.contains(normalizedDest) { + continue + } + seenDestinations.insert(normalizedDest) + + let fs = Filesystem.tmpfs(destination: destination, options: options) try validateMount(.filesystem(fs)) result.append(fs) } diff --git a/Tests/ContainerAPIClientTests/ParserTest.swift b/Tests/ContainerAPIClientTests/ParserTest.swift index 7684ff936..dbb3aa3c0 100644 --- a/Tests/ContainerAPIClientTests/ParserTest.swift +++ b/Tests/ContainerAPIClientTests/ParserTest.swift @@ -1523,6 +1523,33 @@ struct ParserTest { #expect(result.count == 20) } + @Test("tmpfsMounts parses mount options and dedupes on destination path") + func testTmpfsMountsWithColons() throws { + let mounts = [ + "/mnt/scratch:rw,exec", + "/mnt/scratch", // Should be deduped based on path + "/mnt/cache:ro", + ] + let result = try Parser.tmpfsMounts(mounts) + #expect(result.count == 2) + #expect(result[0].destination == "/mnt/scratch") + #expect(result[0].options == ["rw", "exec"]) + } + + @Test("tmpfsMounts throws on empty destination") + func testTmpfsMountsEmptyDestination() throws { + #expect(throws: ContainerizationError.self) { + _ = try Parser.tmpfsMounts([""]) + } + } + + @Test("tmpfsMounts throws on non-absolute destination") + func testTmpfsMountsNonAbsoluteDestination() throws { + #expect(throws: ContainerizationError.self) { + _ = try Parser.tmpfsMounts(["relative/path:rw"]) + } + } + @Test("volumes with large input") func testVolumesLargeInput() throws { let volumes = (0..<20).map { "vol\($0):/mnt/vol\($0)" }