Skip to content

Commit 9155dc1

Browse files
committed
Address PR apple#1594 review feedback
- PublishSocket.init now throws and validates absolute paths so objects are correct by construction; adds DocC for parameters and constraints (addresses comment on PublishSocket.swift:33 and :72). - Adds deprecation/migration note on the struct documenting that the decoder accepts both new `FilePath` and legacy `URL` forms for one release (addresses comment on PublishSocket.swift:21). - Wire format pre-1.0 breaking change: encoder now emits the plain absolute path (e.g. "/var/run/docker.sock") instead of the legacy file-URL form ("file:///var/run/docker.sock"). The decoder still accepts both forms so persisted bundles from earlier releases continue to load; that compatibility will be removed in a later release (addresses comment on PublishSocket.swift:58). - Bumps containerization to 0.33.2 and replaces URL(fileURLWithPath:).absoluteURL.path with ContainerizationOS.FilePathOps.absolutePath in Parser.publishSocket, removing the duplicate "must be absolute" check now that init() enforces it (addresses comments on Parser.swift:754 and :762).
1 parent 1e7f495 commit 9155dc1

5 files changed

Lines changed: 148 additions & 125 deletions

File tree

Package.resolved

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import PackageDescription
2323
let releaseVersion = ProcessInfo.processInfo.environment["RELEASE_VERSION"] ?? "0.0.0"
2424
let gitCommit = ProcessInfo.processInfo.environment["GIT_COMMIT"] ?? "unspecified"
2525
let builderShimVersion = "0.12.0"
26-
let scVersion = "0.33.1"
26+
let scVersion = "0.33.2"
2727

2828
let package = Package(
2929
name: "container",

Sources/ContainerResource/Container/PublishSocket.swift

Lines changed: 61 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,52 @@
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.
27+
/// Absolute path to the socket inside the container.
2328
public var containerPath: FilePath
2429

25-
/// The path where the socket should appear on the host.
30+
/// Absolute path where the socket appears on the host.
2631
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(
3247
containerPath: FilePath,
3348
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
@@ -44,41 +71,45 @@ public struct PublishSocket: Sendable, Codable {
4471
case permissions
4572
}
4673

47-
/// Encode paths as file-URL absolute strings (e.g. `"file:///var/run/docker.sock"`).
74+
/// Encodes each path as its plain absolute string (e.g. `"/var/run/docker.sock"`).
4875
///
49-
/// These fields were previously typed `URL`; `JSONEncoder` special-cases
50-
/// `URL` to emit `absoluteString`. `FilePath`'s synthesized `Codable`
51-
/// would instead emit a keyed container (`{"_storage": "..."}`), changing
52-
/// the on-disk and XPC wire format. We therefore encode each path as the
53-
/// equivalent `URL.absoluteString` so the byte form remains compatible
54-
/// with persisted bundles and any readers (e.g. an older service binary)
55-
/// that still decode these fields as `URL`.
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.
5681
public func encode(to encoder: any Encoder) throws {
5782
var container = encoder.container(keyedBy: CodingKeys.self)
58-
try container.encode(Self.encodePath(containerPath), forKey: .containerPath)
59-
try container.encode(Self.encodePath(hostPath), forKey: .hostPath)
83+
try container.encode(containerPath.string, forKey: .containerPath)
84+
try container.encode(hostPath.string, forKey: .hostPath)
6085
try container.encodeIfPresent(permissions, forKey: .permissions)
6186
}
6287

6388
public init(from decoder: any Decoder) throws {
6489
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-
/// Encode a `FilePath` as a file-URL `absoluteString` to match the prior
71-
/// `URL`-typed wire format byte-for-byte.
72-
private static func encodePath(_ path: FilePath) -> String {
73-
URL(filePath: path.string).absoluteString
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+
}
74106
}
75107

76-
/// Decode a `FilePath` from either the canonical file-URL form
77-
/// (e.g. `"file:///foo"`) emitted by `encodePath(_:)` and the legacy
78-
/// `URL`-typed wire format, or a plain absolute path string. Throws
79-
/// `DecodingError.dataCorrupted` on malformed, empty, or non-absolute
80-
/// inputs so corrupt persisted state fails loudly rather than silently
81-
/// producing an invalid socket path.
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 or empty input.
112+
/// Absoluteness is enforced in `init(containerPath:hostPath:permissions:)`.
82113
private static func decodePath(
83114
from container: KeyedDecodingContainer<CodingKeys>,
84115
forKey key: CodingKeys
@@ -106,11 +137,11 @@ public struct PublishSocket: Sendable, Codable {
106137
path = raw
107138
}
108139

109-
guard path.hasPrefix("/") else {
140+
guard !path.isEmpty else {
110141
throw DecodingError.dataCorruptedError(
111142
forKey: key,
112143
in: container,
113-
debugDescription: "socket path must be absolute: \(raw)"
144+
debugDescription: "decoded socket path is empty: \(raw)"
114145
)
115146
}
116147

Sources/Services/ContainerAPIService/Client/Parser.swift

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -740,7 +740,6 @@ public struct Parser {
740740
let hostPath = String(parts[0])
741741
let containerPath = String(parts[1])
742742

743-
// Validate paths are not empty
744743
if hostPath.isEmpty {
745744
throw ContainerizationError(
746745
.invalidArgument, message: "host socket path cannot be empty")
@@ -750,46 +749,34 @@ public struct Parser {
750749
.invalidArgument, message: "container socket path cannot be empty")
751750
}
752751

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

764-
// Check if host socket already exists and might be in use
765-
if FileManager.default.fileExists(atPath: absoluteHostPath) {
754+
if FileManager.default.fileExists(atPath: absoluteHostPath.string) {
766755
do {
767-
let attrs = try FileManager.default.attributesOfItem(atPath: absoluteHostPath)
756+
let attrs = try FileManager.default.attributesOfItem(atPath: absoluteHostPath.string)
768757
if let fileType = attrs[.type] as? FileAttributeType, fileType == .typeSocket {
769758
throw ContainerizationError(
770759
.invalidArgument,
771760
message: "host socket \(absoluteHostPath) already exists and may be in use")
772761
}
773762
// If it exists but is not a socket, we can remove it and create socket
774-
try FileManager.default.removeItem(atPath: absoluteHostPath)
763+
try FileManager.default.removeItem(atPath: absoluteHostPath.string)
775764
} catch let error as ContainerizationError {
776765
throw error
777766
} catch {
778767
// For other file system errors, continue with creation
779768
}
780769
}
781770

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

789-
// Create and return PublishSocket object with validated paths
790-
return PublishSocket(
777+
return try PublishSocket(
791778
containerPath: FilePath(containerPath),
792-
hostPath: FilePath(absoluteHostPath),
779+
hostPath: absoluteHostPath,
793780
permissions: nil
794781
)
795782

0 commit comments

Comments
 (0)