Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 17 additions & 13 deletions Sources/Services/ContainerAPIService/Client/Parser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -772,14 +772,15 @@ public struct Parser {

// Parse a single `--publish-socket`` argument into a `PublishSocket`.
public static func publishSocket(_ socketText: String) throws -> PublishSocket {
// Split by colon to two parts: [host_path, container_path]
let parts = socketText.split(separator: ":")

switch parts.count {
case 2:
// The host path may itself contain ':' (e.g. a timestamped filename),
// so split on the LAST ':' rather than requiring exactly one. The
// container path is always a simple absolute path and is validated
// as such below, mirroring the approach used for `container cp` path
// references.
if let colon = socketText.lastIndex(of: ":") {
// Extract host and container paths
let hostPath = String(parts[0])
let containerPath = String(parts[1])
let hostPath = String(socketText[..<colon])
let containerPath = String(socketText[socketText.index(after: colon)...])

if hostPath.isEmpty {
throw ContainerizationError(
Expand All @@ -789,6 +790,10 @@ public struct Parser {
throw ContainerizationError(
.invalidArgument, message: "container socket path cannot be empty")
}
guard FilePath(containerPath).isAbsolute else {
throw ContainerizationError(
.invalidArgument, message: "containerPath must be absolute: \(containerPath)")
}

let absoluteHostPath = FilePathOps.absolutePath(FilePath(hostPath))

Expand Down Expand Up @@ -820,13 +825,12 @@ public struct Parser {
hostPath: absoluteHostPath,
permissions: nil
)

default:
throw ContainerizationError(
.invalidArgument,
message:
"invalid publish-socket format \(socketText). Expected: host_path:container_path")
}

throw ContainerizationError(
.invalidArgument,
message:
"invalid publish-socket format \(socketText). Expected: host_path:container_path")
}

// MARK: Networks
Expand Down
77 changes: 77 additions & 0 deletions Tests/ContainerAPIClientTests/ParserTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,83 @@ struct ParserTest {
}
}

@Test
func testPublishSocketBasic() throws {
let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent("test-publish-socket-\(UUID().uuidString)")
defer { try? FileManager.default.removeItem(at: tempDir) }
let hostPath = tempDir.appendingPathComponent("app.sock").path

let result = try Parser.publishSocket("\(hostPath):/var/run/app.sock")
#expect(result.hostPath.string == hostPath)
#expect(result.containerPath.string == "/var/run/app.sock")
}

@Test
func testPublishSocketHostPathWithColon() throws {
// Regression: a host path containing ':' (e.g. a timestamped filename)
// was rejected because the argument was split on every ':' instead of
// only the last one, mirroring the fix for `container cp` path refs.
let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent("test-publish-socket-\(UUID().uuidString)")
defer { try? FileManager.default.removeItem(at: tempDir) }
let hostPath = tempDir.appendingPathComponent("backup-2026-08-18T10:30:00.sock").path

let result = try Parser.publishSocket("\(hostPath):/var/run/app.sock")
#expect(result.hostPath.string == hostPath)
#expect(result.containerPath.string == "/var/run/app.sock")
}

@Test
func testPublishSocketEmptyHostPath() throws {
#expect {
_ = try Parser.publishSocket(":/var/run/app.sock")
} throws: { error in
guard let error = error as? ContainerizationError else {
return false
}
return error.description.contains("host socket path cannot be empty")
}
}

@Test
func testPublishSocketEmptyContainerPath() throws {
#expect {
_ = try Parser.publishSocket("/tmp/app.sock:")
} throws: { error in
guard let error = error as? ContainerizationError else {
return false
}
return error.description.contains("container socket path cannot be empty")
}
}

@Test
func testPublishSocketNoColon() throws {
#expect {
_ = try Parser.publishSocket("/tmp/app.sock")
} throws: { error in
guard let error = error as? ContainerizationError else {
return false
}
return error.description.contains("invalid publish-socket format")
}
}

@Test
func testPublishSocketNonAbsoluteContainerPath() throws {
let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent("test-publish-socket-\(UUID().uuidString)")
defer { try? FileManager.default.removeItem(at: tempDir) }
let hostPath = tempDir.appendingPathComponent("app.sock").path

#expect {
_ = try Parser.publishSocket("\(hostPath):relative/app.sock")
} throws: { error in
guard let error = error as? ContainerizationError else {
return false
}
return error.description.contains("containerPath must be absolute")
}
}

@Test
func testRelativePaths() throws {
// Test bind mount with relative path "."
Expand Down