@@ -20,21 +20,64 @@ import SystemPackage
2020/// Represents a socket that should be published from container to host.
2121public struct PublishSocket : Sendable , Codable {
2222 /// The path to the socket in the container.
23- public var containerPath : URL
23+ public var containerPath : FilePath
2424
2525 /// The path where the socket should appear on the host.
26- public var hostPath : URL
26+ public var hostPath : FilePath
2727
2828 /// File permissions for the socket on the host.
2929 public var permissions : FilePermissions ?
3030
3131 public init (
32- containerPath: URL ,
33- hostPath: URL ,
32+ containerPath: FilePath ,
33+ hostPath: FilePath ,
3434 permissions: FilePermissions ? = nil
3535 ) {
3636 self . containerPath = containerPath
3737 self . hostPath = hostPath
3838 self . permissions = permissions
3939 }
40+
41+ private enum CodingKeys : String , CodingKey {
42+ case containerPath
43+ case hostPath
44+ case permissions
45+ }
46+
47+ /// Encode paths as plain JSON strings.
48+ ///
49+ /// Previously these fields were `URL`s; `JSONEncoder` special-cases `URL`
50+ /// to encode as `absoluteString` (e.g. `"file:///var/run/docker.sock"`).
51+ /// `FilePath`'s synthesized Codable conformance uses a keyed container
52+ /// (`{"_storage": "..."}`), which would change the on-disk and XPC wire
53+ /// format. We override that here to keep the value a flat JSON string;
54+ /// the decoder accepts both the new clean form (`"/var/run/docker.sock"`)
55+ /// and the legacy `URL`-encoded form (`"file:///..."`).
56+ public func encode( to encoder: any Encoder ) throws {
57+ var container = encoder. container ( keyedBy: CodingKeys . self)
58+ try container. encode ( containerPath. string, forKey: . containerPath)
59+ try container. encode ( hostPath. string, forKey: . hostPath)
60+ try container. encodeIfPresent ( permissions, forKey: . permissions)
61+ }
62+
63+ public init ( from decoder: any Decoder ) throws {
64+ let container = try decoder. container ( keyedBy: CodingKeys . self)
65+ self . containerPath = try Self . decodePath ( from: container, forKey: . containerPath)
66+ self . hostPath = try Self . decodePath ( from: container, forKey: . hostPath)
67+ self . permissions = try container. decodeIfPresent ( FilePermissions . self, forKey: . permissions)
68+ }
69+
70+ /// Decode a `FilePath` from either a plain path string or a legacy
71+ /// `URL.absoluteString` (e.g. `"file:///foo"`) for backward compatibility
72+ /// with container bundles persisted before the migration to `FilePath`.
73+ private static func decodePath(
74+ from container: KeyedDecodingContainer < CodingKeys > ,
75+ forKey key: CodingKeys
76+ ) throws -> FilePath {
77+ let raw = try container. decode ( String . self, forKey: key)
78+ if raw. hasPrefix ( " file:// " ) , let url = URL ( string: raw) , url. isFileURL {
79+ return FilePath ( url. path)
80+ }
81+ return FilePath ( raw)
82+ }
4083}
0 commit comments