Skip to content

Commit da8daf3

Browse files
authored
Use FilePath for PublishSocket (apple#1594)
- Closes apple#1593.
1 parent 37595a7 commit da8daf3

4 files changed

Lines changed: 383 additions & 32 deletions

File tree

Sources/ContainerResource/Container/PublishSocket.swift

Lines changed: 128 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,27 +14,148 @@
1414
// limitations under the License.
1515
//===----------------------------------------------------------------------===//
1616

17+
import ContainerizationError
1718
import Foundation
1819
import SystemPackage
1920

2021
/// Represents a socket that should be published from container to host.
22+
///
23+
/// - Deprecated: New for 1.0.0, path types changed from `URL` to `FilePath`.
24+
/// - Note: Decoder handles `FilePath` and `URL` for persistent data compatibility;
25+
/// this compatibility will be removed in a later release.
2126
public struct PublishSocket: Sendable, Codable {
22-
/// The path to the socket in the container.
23-
public var containerPath: URL
27+
/// Absolute path to the socket inside the container.
28+
public var containerPath: FilePath
2429

25-
/// The path where the socket should appear on the host.
26-
public var hostPath: URL
30+
/// Absolute path where the socket appears on the host.
31+
public var hostPath: FilePath
2732

2833
/// File permissions for the socket on the host.
2934
public var permissions: FilePermissions?
3035

36+
/// Creates a `PublishSocket` with validated absolute paths.
37+
///
38+
/// - Parameters:
39+
/// - containerPath: Absolute path to the socket inside the container.
40+
/// Must begin with `/`.
41+
/// - hostPath: Absolute path where the socket appears on the host.
42+
/// Must begin with `/`.
43+
/// - permissions: File permissions applied to the socket on the host.
44+
/// - Throws: `ContainerizationError` with code `.invalidArgument` if
45+
/// either path is not absolute.
3146
public init(
32-
containerPath: URL,
33-
hostPath: URL,
47+
containerPath: FilePath,
48+
hostPath: FilePath,
3449
permissions: FilePermissions? = nil
35-
) {
50+
) throws {
51+
guard containerPath.isAbsolute else {
52+
throw ContainerizationError(
53+
.invalidArgument,
54+
message: "containerPath must be absolute: \(containerPath)"
55+
)
56+
}
57+
guard hostPath.isAbsolute else {
58+
throw ContainerizationError(
59+
.invalidArgument,
60+
message: "hostPath must be absolute: \(hostPath)"
61+
)
62+
}
3663
self.containerPath = containerPath
3764
self.hostPath = hostPath
3865
self.permissions = permissions
3966
}
67+
68+
private enum CodingKeys: String, CodingKey {
69+
case containerPath
70+
case hostPath
71+
case permissions
72+
}
73+
74+
/// Encodes each path as its plain absolute string (e.g. `"/var/run/docker.sock"`).
75+
///
76+
/// Pre-1.0 wire-format change from the prior `URL`-typed encoding which
77+
/// emitted `URL.absoluteString` (`"file:///var/run/docker.sock"`). The
78+
/// decoder accepts both forms for compatibility with persisted bundles
79+
/// from earlier releases; that compatibility will be removed in a later
80+
/// release.
81+
public func encode(to encoder: any Encoder) throws {
82+
var container = encoder.container(keyedBy: CodingKeys.self)
83+
try container.encode(containerPath.string, forKey: .containerPath)
84+
try container.encode(hostPath.string, forKey: .hostPath)
85+
try container.encodeIfPresent(permissions, forKey: .permissions)
86+
}
87+
88+
public init(from decoder: any Decoder) throws {
89+
let container = try decoder.container(keyedBy: CodingKeys.self)
90+
let containerPath = try Self.decodePath(from: container, forKey: .containerPath)
91+
let hostPath = try Self.decodePath(from: container, forKey: .hostPath)
92+
let permissions = try container.decodeIfPresent(FilePermissions.self, forKey: .permissions)
93+
do {
94+
try self.init(
95+
containerPath: containerPath,
96+
hostPath: hostPath,
97+
permissions: permissions
98+
)
99+
} catch let error as ContainerizationError {
100+
throw DecodingError.dataCorruptedError(
101+
forKey: .containerPath,
102+
in: container,
103+
debugDescription: String(describing: error)
104+
)
105+
}
106+
}
107+
108+
/// Decodes a `FilePath` accepting either the new plain-path form
109+
/// (`"/var/run/docker.sock"`) or the legacy file-URL form emitted by
110+
/// older releases (`"file:///var/run/docker.sock"`). Throws
111+
/// `DecodingError.dataCorrupted` on a malformed file URL, empty input, or a
112+
/// non-absolute path — validating decoded paths here guards against
113+
/// manually edited or corrupt persisted configs, complementing the
114+
/// by-construction check in `init(containerPath:hostPath:permissions:)`.
115+
private static func decodePath(
116+
from container: KeyedDecodingContainer<CodingKeys>,
117+
forKey key: CodingKeys
118+
) throws -> FilePath {
119+
let raw = try container.decode(String.self, forKey: key)
120+
121+
let path: String
122+
if raw.hasPrefix("file:") {
123+
guard let url = URL(string: raw), url.isFileURL else {
124+
throw DecodingError.dataCorruptedError(
125+
forKey: key,
126+
in: container,
127+
debugDescription: "malformed file URL: \(raw)"
128+
)
129+
}
130+
if let host = url.host(), !host.isEmpty, host != "localhost" {
131+
throw DecodingError.dataCorruptedError(
132+
forKey: key,
133+
in: container,
134+
debugDescription: "file URL host must be empty or 'localhost': \(raw)"
135+
)
136+
}
137+
path = url.path(percentEncoded: false)
138+
} else {
139+
path = raw
140+
}
141+
142+
guard !path.isEmpty else {
143+
throw DecodingError.dataCorruptedError(
144+
forKey: key,
145+
in: container,
146+
debugDescription: "decoded socket path is empty: \(raw)"
147+
)
148+
}
149+
150+
let filePath = FilePath(path)
151+
guard filePath.isAbsolute else {
152+
throw DecodingError.dataCorruptedError(
153+
forKey: key,
154+
in: container,
155+
debugDescription: "decoded socket path must be absolute: \(raw)"
156+
)
157+
}
158+
159+
return filePath
160+
}
40161
}

Sources/Services/ContainerAPIService/Client/Parser.swift

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import ContainerizationExtras
2222
import ContainerizationOCI
2323
import ContainerizationOS
2424
import Foundation
25+
import SystemPackage
2526

2627
/// A parsed volume specification from user input
2728
public struct ParsedVolume {
@@ -739,7 +740,6 @@ public struct Parser {
739740
let hostPath = String(parts[0])
740741
let containerPath = String(parts[1])
741742

742-
// Validate paths are not empty
743743
if hostPath.isEmpty {
744744
throw ContainerizationError(
745745
.invalidArgument, message: "host socket path cannot be empty")
@@ -749,46 +749,34 @@ public struct Parser {
749749
.invalidArgument, message: "container socket path cannot be empty")
750750
}
751751

752-
// Ensure container path must start with /
753-
if !containerPath.hasPrefix("/") {
754-
throw ContainerizationError(
755-
.invalidArgument,
756-
message: "container socket path must be absolute: \(containerPath)")
757-
}
758-
759-
// Convert host path to absolute path for consistency
760-
let hostURL = URL(fileURLWithPath: hostPath)
761-
let absoluteHostPath = hostURL.absoluteURL.path
752+
let absoluteHostPath = FilePathOps.absolutePath(FilePath(hostPath))
762753

763-
// Check if host socket already exists and might be in use
764-
if FileManager.default.fileExists(atPath: absoluteHostPath) {
754+
if FileManager.default.fileExists(atPath: absoluteHostPath.string) {
765755
do {
766-
let attrs = try FileManager.default.attributesOfItem(atPath: absoluteHostPath)
756+
let attrs = try FileManager.default.attributesOfItem(atPath: absoluteHostPath.string)
767757
if let fileType = attrs[.type] as? FileAttributeType, fileType == .typeSocket {
768758
throw ContainerizationError(
769759
.invalidArgument,
770760
message: "host socket \(absoluteHostPath) already exists and may be in use")
771761
}
772762
// If it exists but is not a socket, we can remove it and create socket
773-
try FileManager.default.removeItem(atPath: absoluteHostPath)
763+
try FileManager.default.removeItem(atPath: absoluteHostPath.string)
774764
} catch let error as ContainerizationError {
775765
throw error
776766
} catch {
777767
// For other file system errors, continue with creation
778768
}
779769
}
780770

781-
// Create host directory if it doesn't exist
782-
let hostDir = hostURL.deletingLastPathComponent()
783-
if !FileManager.default.fileExists(atPath: hostDir.path) {
771+
let hostDir = absoluteHostPath.removingLastComponent()
772+
if !FileManager.default.fileExists(atPath: hostDir.string) {
784773
try FileManager.default.createDirectory(
785-
at: hostDir, withIntermediateDirectories: true)
774+
atPath: hostDir.string, withIntermediateDirectories: true)
786775
}
787776

788-
// Create and return PublishSocket object with validated paths
789-
return PublishSocket(
790-
containerPath: URL(fileURLWithPath: containerPath),
791-
hostPath: URL(fileURLWithPath: absoluteHostPath),
777+
return try PublishSocket(
778+
containerPath: FilePath(containerPath),
779+
hostPath: absoluteHostPath,
792780
permissions: nil
793781
)
794782

Sources/Services/RuntimeLinux/Server/RuntimeService.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,9 +1000,10 @@ public actor RuntimeService {
10001000
}
10011001

10021002
for publishedSocket in config.publishedSockets {
1003+
// UnixSocketConfiguration (Containerization) takes URL; convert from FilePath at the boundary.
10031004
let socketConfig = UnixSocketConfiguration(
1004-
source: publishedSocket.containerPath,
1005-
destination: publishedSocket.hostPath,
1005+
source: URL(filePath: publishedSocket.containerPath.string),
1006+
destination: URL(filePath: publishedSocket.hostPath.string),
10061007
permissions: publishedSocket.permissions,
10071008
direction: .outOf
10081009
)

0 commit comments

Comments
 (0)