From ae54fed2e3fd03395191e01dec748748c3bc4f7b Mon Sep 17 00:00:00 2001 From: Jaewon Date: Mon, 30 Mar 2026 17:33:12 -0700 Subject: [PATCH 1/3] Add stat RPC --- .../SandboxContext/SandboxContext.grpc.swift | 100 +++++++ .../SandboxContext/SandboxContext.pb.swift | 265 ++++++++++++++++++ .../SandboxContext/SandboxContext.proto | 22 ++ Sources/Containerization/Vminitd.swift | 15 + vminitd/Sources/vminitd/Server+GRPC.swift | 52 ++++ 5 files changed, 454 insertions(+) diff --git a/Sources/Containerization/SandboxContext/SandboxContext.grpc.swift b/Sources/Containerization/SandboxContext/SandboxContext.grpc.swift index 1600090f..6a3bc13f 100644 --- a/Sources/Containerization/SandboxContext/SandboxContext.grpc.swift +++ b/Sources/Containerization/SandboxContext/SandboxContext.grpc.swift @@ -85,6 +85,11 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextClientProtoc handler: @escaping (Com_Apple_Containerization_Sandbox_V3_CopyResponse) -> Void ) -> ServerStreamingCall + func stat( + _ request: Com_Apple_Containerization_Sandbox_V3_StatRequest, + callOptions: CallOptions? + ) -> UnaryCall + func createProcess( _ request: Com_Apple_Containerization_Sandbox_V3_CreateProcessRequest, callOptions: CallOptions? @@ -366,6 +371,24 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContextClientProtocol { ) } + /// Stat a path in the guest filesystem. + /// + /// - Parameters: + /// - request: Request to send to Stat. + /// - callOptions: Call options. + /// - Returns: A `UnaryCall` with futures for the metadata, status and response. + public func stat( + _ request: Com_Apple_Containerization_Sandbox_V3_StatRequest, + callOptions: CallOptions? = nil + ) -> UnaryCall { + return self.makeUnaryCall( + path: Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.stat.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeStatInterceptors() ?? [] + ) + } + /// Create a new process inside the container. /// /// - Parameters: @@ -805,6 +828,11 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncClientP callOptions: CallOptions? ) -> GRPCAsyncServerStreamingCall + func makeStatCall( + _ request: Com_Apple_Containerization_Sandbox_V3_StatRequest, + callOptions: CallOptions? + ) -> GRPCAsyncUnaryCall + func makeCreateProcessCall( _ request: Com_Apple_Containerization_Sandbox_V3_CreateProcessRequest, callOptions: CallOptions? @@ -1026,6 +1054,18 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncClientProtoco ) } + public func makeStatCall( + _ request: Com_Apple_Containerization_Sandbox_V3_StatRequest, + callOptions: CallOptions? = nil + ) -> GRPCAsyncUnaryCall { + return self.makeAsyncUnaryCall( + path: Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.stat.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeStatInterceptors() ?? [] + ) + } + public func makeCreateProcessCall( _ request: Com_Apple_Containerization_Sandbox_V3_CreateProcessRequest, callOptions: CallOptions? = nil @@ -1365,6 +1405,18 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncClientProtoco ) } + public func stat( + _ request: Com_Apple_Containerization_Sandbox_V3_StatRequest, + callOptions: CallOptions? = nil + ) async throws -> Com_Apple_Containerization_Sandbox_V3_StatResponse { + return try await self.performAsyncUnaryCall( + path: Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.stat.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeStatInterceptors() ?? [] + ) + } + public func createProcess( _ request: Com_Apple_Containerization_Sandbox_V3_CreateProcessRequest, callOptions: CallOptions? = nil @@ -1631,6 +1683,9 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextClientInterc /// - Returns: Interceptors to use when invoking 'copy'. func makeCopyInterceptors() -> [ClientInterceptor] + /// - Returns: Interceptors to use when invoking 'stat'. + func makeStatInterceptors() -> [ClientInterceptor] + /// - Returns: Interceptors to use when invoking 'createProcess'. func makeCreateProcessInterceptors() -> [ClientInterceptor] @@ -1701,6 +1756,7 @@ public enum Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata { Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.setupEmulator, Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.writeFile, Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.copy, + Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.stat, Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.createProcess, Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.deleteProcess, Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.startProcess, @@ -1783,6 +1839,12 @@ public enum Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata { type: GRPCCallType.serverStreaming ) + public static let stat = GRPCMethodDescriptor( + name: "Stat", + path: "/com.apple.containerization.sandbox.v3.SandboxContext/Stat", + type: GRPCCallType.unary + ) + public static let createProcess = GRPCMethodDescriptor( name: "CreateProcess", path: "/com.apple.containerization.sandbox.v3.SandboxContext/CreateProcess", @@ -1931,6 +1993,9 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextProvider: Ca /// the gRPC stream is used only for control/metadata. func copy(request: Com_Apple_Containerization_Sandbox_V3_CopyRequest, context: StreamingResponseCallContext) -> EventLoopFuture + /// Stat a path in the guest filesystem. + func stat(request: Com_Apple_Containerization_Sandbox_V3_StatRequest, context: StatusOnlyCallContext) -> EventLoopFuture + /// Create a new process inside the container. func createProcess(request: Com_Apple_Containerization_Sandbox_V3_CreateProcessRequest, context: StatusOnlyCallContext) -> EventLoopFuture @@ -2089,6 +2154,15 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContextProvider { userFunction: self.copy(request:context:) ) + case "Stat": + return UnaryServerHandler( + context: context, + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), + interceptors: self.interceptors?.makeStatInterceptors() ?? [], + userFunction: self.stat(request:context:) + ) + case "CreateProcess": return UnaryServerHandler( context: context, @@ -2328,6 +2402,12 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncProvide context: GRPCAsyncServerCallContext ) async throws + /// Stat a path in the guest filesystem. + func stat( + request: Com_Apple_Containerization_Sandbox_V3_StatRequest, + context: GRPCAsyncServerCallContext + ) async throws -> Com_Apple_Containerization_Sandbox_V3_StatResponse + /// Create a new process inside the container. func createProcess( request: Com_Apple_Containerization_Sandbox_V3_CreateProcessRequest, @@ -2547,6 +2627,15 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncProvider { wrapping: { try await self.copy(request: $0, responseStream: $1, context: $2) } ) + case "Stat": + return GRPCAsyncServerHandler( + context: context, + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), + interceptors: self.interceptors?.makeStatInterceptors() ?? [], + wrapping: { try await self.stat(request: $0, context: $1) } + ) + case "CreateProcess": return GRPCAsyncServerHandler( context: context, @@ -2757,6 +2846,10 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextServerInterc /// Defaults to calling `self.makeInterceptors()`. func makeCopyInterceptors() -> [ServerInterceptor] + /// - Returns: Interceptors to use when handling 'stat'. + /// Defaults to calling `self.makeInterceptors()`. + func makeStatInterceptors() -> [ServerInterceptor] + /// - Returns: Interceptors to use when handling 'createProcess'. /// Defaults to calling `self.makeInterceptors()`. func makeCreateProcessInterceptors() -> [ServerInterceptor] @@ -2845,6 +2938,7 @@ public enum Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata { Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.setupEmulator, Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.writeFile, Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.copy, + Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.stat, Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.createProcess, Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.deleteProcess, Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.startProcess, @@ -2927,6 +3021,12 @@ public enum Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata { type: GRPCCallType.serverStreaming ) + public static let stat = GRPCMethodDescriptor( + name: "Stat", + path: "/com.apple.containerization.sandbox.v3.SandboxContext/Stat", + type: GRPCCallType.unary + ) + public static let createProcess = GRPCMethodDescriptor( name: "CreateProcess", path: "/com.apple.containerization.sandbox.v3.SandboxContext/CreateProcess", diff --git a/Sources/Containerization/SandboxContext/SandboxContext.pb.swift b/Sources/Containerization/SandboxContext/SandboxContext.pb.swift index 87d22643..aa47aa1b 100644 --- a/Sources/Containerization/SandboxContext/SandboxContext.pb.swift +++ b/Sources/Containerization/SandboxContext/SandboxContext.pb.swift @@ -937,6 +937,107 @@ public struct Com_Apple_Containerization_Sandbox_V3_CopyResponse: Sendable { public init() {} } +public struct Com_Apple_Containerization_Sandbox_V3_StatRequest: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var path: String = String() + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +public struct Com_Apple_Containerization_Sandbox_V3_Stat: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// st_dev: ID of device containing file + public var dev: UInt64 = 0 + + /// st_mode: file type and mode (permissions) + public var mode: UInt32 = 0 + + /// st_nlink: number of hard links + public var nlink: UInt64 = 0 + + /// st_uid: user ID of owner + public var uid: UInt32 = 0 + + /// st_gid: group ID of owner + public var gid: UInt32 = 0 + + /// st_rdev: device ID (if special file) + public var rdev: UInt64 = 0 + + /// st_size: total size in bytes + public var size: Int64 = 0 + + /// st_atim: time of last access + public var atime: SwiftProtobuf.Google_Protobuf_Timestamp { + get {return _atime ?? SwiftProtobuf.Google_Protobuf_Timestamp()} + set {_atime = newValue} + } + /// Returns true if `atime` has been explicitly set. + public var hasAtime: Bool {return self._atime != nil} + /// Clears the value of `atime`. Subsequent reads from it will return its default value. + public mutating func clearAtime() {self._atime = nil} + + /// st_mtim: time of last modification + public var mtime: SwiftProtobuf.Google_Protobuf_Timestamp { + get {return _mtime ?? SwiftProtobuf.Google_Protobuf_Timestamp()} + set {_mtime = newValue} + } + /// Returns true if `mtime` has been explicitly set. + public var hasMtime: Bool {return self._mtime != nil} + /// Clears the value of `mtime`. Subsequent reads from it will return its default value. + public mutating func clearMtime() {self._mtime = nil} + + /// st_ctim: time of last status change + public var ctime: SwiftProtobuf.Google_Protobuf_Timestamp { + get {return _ctime ?? SwiftProtobuf.Google_Protobuf_Timestamp()} + set {_ctime = newValue} + } + /// Returns true if `ctime` has been explicitly set. + public var hasCtime: Bool {return self._ctime != nil} + /// Clears the value of `ctime`. Subsequent reads from it will return its default value. + public mutating func clearCtime() {self._ctime = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _atime: SwiftProtobuf.Google_Protobuf_Timestamp? = nil + fileprivate var _mtime: SwiftProtobuf.Google_Protobuf_Timestamp? = nil + fileprivate var _ctime: SwiftProtobuf.Google_Protobuf_Timestamp? = nil +} + +public struct Com_Apple_Containerization_Sandbox_V3_StatResponse: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var stat: Com_Apple_Containerization_Sandbox_V3_Stat { + get {return _stat ?? Com_Apple_Containerization_Sandbox_V3_Stat()} + set {_stat = newValue} + } + /// Returns true if `stat` has been explicitly set. + public var hasStat: Bool {return self._stat != nil} + /// Clears the value of `stat`. Subsequent reads from it will return its default value. + public mutating func clearStat() {self._stat = nil} + + /// Non-empty if stat failed. + public var error: String = String() + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _stat: Com_Apple_Containerization_Sandbox_V3_Stat? = nil +} + public struct Com_Apple_Containerization_Sandbox_V3_IpLinkSetRequest: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for @@ -2929,6 +3030,170 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyResponse.Status: SwiftProtob ] } +extension Com_Apple_Containerization_Sandbox_V3_StatRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".StatRequest" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "path"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.path) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if !self.path.isEmpty { + try visitor.visitSingularStringField(value: self.path, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_StatRequest, rhs: Com_Apple_Containerization_Sandbox_V3_StatRequest) -> Bool { + if lhs.path != rhs.path {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Com_Apple_Containerization_Sandbox_V3_Stat: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".Stat" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "dev"), + 2: .same(proto: "mode"), + 3: .same(proto: "nlink"), + 4: .same(proto: "uid"), + 5: .same(proto: "gid"), + 6: .same(proto: "rdev"), + 7: .same(proto: "size"), + 8: .same(proto: "atime"), + 9: .same(proto: "mtime"), + 10: .same(proto: "ctime"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularUInt64Field(value: &self.dev) }() + case 2: try { try decoder.decodeSingularUInt32Field(value: &self.mode) }() + case 3: try { try decoder.decodeSingularUInt64Field(value: &self.nlink) }() + case 4: try { try decoder.decodeSingularUInt32Field(value: &self.uid) }() + case 5: try { try decoder.decodeSingularUInt32Field(value: &self.gid) }() + case 6: try { try decoder.decodeSingularUInt64Field(value: &self.rdev) }() + case 7: try { try decoder.decodeSingularInt64Field(value: &self.size) }() + case 8: try { try decoder.decodeSingularMessageField(value: &self._atime) }() + case 9: try { try decoder.decodeSingularMessageField(value: &self._mtime) }() + case 10: try { try decoder.decodeSingularMessageField(value: &self._ctime) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if self.dev != 0 { + try visitor.visitSingularUInt64Field(value: self.dev, fieldNumber: 1) + } + if self.mode != 0 { + try visitor.visitSingularUInt32Field(value: self.mode, fieldNumber: 2) + } + if self.nlink != 0 { + try visitor.visitSingularUInt64Field(value: self.nlink, fieldNumber: 3) + } + if self.uid != 0 { + try visitor.visitSingularUInt32Field(value: self.uid, fieldNumber: 4) + } + if self.gid != 0 { + try visitor.visitSingularUInt32Field(value: self.gid, fieldNumber: 5) + } + if self.rdev != 0 { + try visitor.visitSingularUInt64Field(value: self.rdev, fieldNumber: 6) + } + if self.size != 0 { + try visitor.visitSingularInt64Field(value: self.size, fieldNumber: 7) + } + try { if let v = self._atime { + try visitor.visitSingularMessageField(value: v, fieldNumber: 8) + } }() + try { if let v = self._mtime { + try visitor.visitSingularMessageField(value: v, fieldNumber: 9) + } }() + try { if let v = self._ctime { + try visitor.visitSingularMessageField(value: v, fieldNumber: 10) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_Stat, rhs: Com_Apple_Containerization_Sandbox_V3_Stat) -> Bool { + if lhs.dev != rhs.dev {return false} + if lhs.mode != rhs.mode {return false} + if lhs.nlink != rhs.nlink {return false} + if lhs.uid != rhs.uid {return false} + if lhs.gid != rhs.gid {return false} + if lhs.rdev != rhs.rdev {return false} + if lhs.size != rhs.size {return false} + if lhs._atime != rhs._atime {return false} + if lhs._mtime != rhs._mtime {return false} + if lhs._ctime != rhs._ctime {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Com_Apple_Containerization_Sandbox_V3_StatResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".StatResponse" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "stat"), + 2: .same(proto: "error"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._stat) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.error) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._stat { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + if !self.error.isEmpty { + try visitor.visitSingularStringField(value: self.error, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_StatResponse, rhs: Com_Apple_Containerization_Sandbox_V3_StatResponse) -> Bool { + if lhs._stat != rhs._stat {return false} + if lhs.error != rhs.error {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + extension Com_Apple_Containerization_Sandbox_V3_IpLinkSetRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".IpLinkSetRequest" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ diff --git a/Sources/Containerization/SandboxContext/SandboxContext.proto b/Sources/Containerization/SandboxContext/SandboxContext.proto index 5250ced7..f9e9e3fc 100644 --- a/Sources/Containerization/SandboxContext/SandboxContext.proto +++ b/Sources/Containerization/SandboxContext/SandboxContext.proto @@ -28,6 +28,8 @@ service SandboxContext { // Data transfer happens over a dedicated vsock connection; // the gRPC stream is used only for control/metadata. rpc Copy(CopyRequest) returns (stream CopyResponse); + // Stat a path in the guest filesystem. + rpc Stat(StatRequest) returns (StatResponse); // Create a new process inside the container. rpc CreateProcess(CreateProcessRequest) returns (CreateProcessResponse); @@ -267,6 +269,26 @@ message CopyResponse { string error = 4; } +message StatRequest { string path = 1; } + +message Stat { + uint64 dev = 1; // st_dev: ID of device containing file + uint32 mode = 2; // st_mode: file type and mode (permissions) + uint64 nlink = 3; // st_nlink: number of hard links + uint32 uid = 4; // st_uid: user ID of owner + uint32 gid = 5; // st_gid: group ID of owner + uint64 rdev = 6; // st_rdev: device ID (if special file) + int64 size = 7; // st_size: total size in bytes + google.protobuf.Timestamp atime = 8; // st_atim: time of last access + google.protobuf.Timestamp mtime = 9; // st_mtim: time of last modification + google.protobuf.Timestamp ctime = 10; // st_ctim: time of last status change +} + +message StatResponse { + Stat stat = 1; + string error = 2; // Non-empty if stat failed. +} + message IpLinkSetRequest { string interface = 1; bool up = 2; diff --git a/Sources/Containerization/Vminitd.swift b/Sources/Containerization/Vminitd.swift index 363a2131..a1c8d714 100644 --- a/Sources/Containerization/Vminitd.swift +++ b/Sources/Containerization/Vminitd.swift @@ -448,6 +448,21 @@ extension Vminitd { public let totalSize: UInt64 } + public func stat( + path: URL + ) async throws -> Com_Apple_Containerization_Sandbox_V3_Stat { + let request = Com_Apple_Containerization_Sandbox_V3_StatRequest.with { + $0.path = path.path + } + + let response = try await client.stat(request) + guard response.error.isEmpty else { + throw ContainerizationError(.internalError, message: "stat: \(response.error)") + } + + return response.stat + } + /// Unified copy control plane. Sends a CopyRequest over gRPC and processes /// the response stream. Data transfer happens over a separate vsock connection /// managed by the caller. diff --git a/vminitd/Sources/vminitd/Server+GRPC.swift b/vminitd/Sources/vminitd/Server+GRPC.swift index 405ae42b..0f4c8921 100644 --- a/vminitd/Sources/vminitd/Server+GRPC.swift +++ b/vminitd/Sources/vminitd/Server+GRPC.swift @@ -37,12 +37,14 @@ private let _mount = Musl.mount private let _umount = Musl.umount2 private let _kill = Musl.kill private let _sync = Musl.sync +private let _stat: @Sendable (UnsafePointer, UnsafeMutablePointer) -> Int32 = stat #elseif canImport(Glibc) import Glibc private let _mount = Glibc.mount private let _umount = Glibc.umount2 private let _kill = Glibc.kill private let _sync = Glibc.sync +private let _stat: @Sendable (UnsafePointer, UnsafeMutablePointer) -> Int32 = stat #endif extension ContainerizationError { @@ -355,6 +357,56 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncProvid return .init() } + func stat( + request: Com_Apple_Containerization_Sandbox_V3_StatRequest, + context: GRPCAsyncServerCallContext, + ) async throws -> Com_Apple_Containerization_Sandbox_V3_StatResponse { + log.debug( + "stat", + metadata: [ + "path": "\(request.path)" + ] + ) + + #if os(Linux) + #if canImport(Musl) + var s = Musl.stat() + #elseif canImport(Glibc) + var s = Glibc.stat() + #endif + let result = _stat(request.path, &s) + if result == -1 { + let error = swiftErrno("stat") + return .with { $0.error = "\(error)" } + } + return .with { + $0.stat = .with { + $0.dev = UInt64(s.st_dev) + $0.mode = s.st_mode + $0.nlink = UInt64(s.st_nlink) + $0.uid = s.st_uid + $0.gid = s.st_gid + $0.rdev = UInt64(s.st_rdev) + $0.size = Int64(s.st_size) + $0.atime = .with { + $0.seconds = Int64(s.st_atim.tv_sec) + $0.nanos = Int32(s.st_atim.tv_nsec) + } + $0.mtime = .with { + $0.seconds = Int64(s.st_mtim.tv_sec) + $0.nanos = Int32(s.st_mtim.tv_nsec) + } + $0.ctime = .with { + $0.seconds = Int64(s.st_ctim.tv_sec) + $0.nanos = Int32(s.st_ctim.tv_nsec) + } + } + } + #else + fatalError("stat not supported on platform") + #endif + } + // Chunk size for streaming file transfers (1MB). private static let copyChunkSize = 1024 * 1024 From 59c7b240b6e7e2774a2c2b24ad12a20b9c025226 Mon Sep 17 00:00:00 2001 From: Jaewon Date: Wed, 1 Apr 2026 12:27:48 -0700 Subject: [PATCH 2/3] Use Foundation.stat for return type --- .../SandboxContext/SandboxContext.pb.swift | 188 ++++++++++++------ .../SandboxContext/SandboxContext.proto | 23 ++- Sources/Containerization/Vminitd.swift | 19 +- vminitd/Sources/vminitd/Server+GRPC.swift | 3 + 4 files changed, 161 insertions(+), 72 deletions(-) diff --git a/Sources/Containerization/SandboxContext/SandboxContext.pb.swift b/Sources/Containerization/SandboxContext/SandboxContext.pb.swift index aa47aa1b..f10feb8a 100644 --- a/Sources/Containerization/SandboxContext/SandboxContext.pb.swift +++ b/Sources/Containerization/SandboxContext/SandboxContext.pb.swift @@ -954,27 +954,36 @@ public struct Com_Apple_Containerization_Sandbox_V3_Stat: Sendable { // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. - /// st_dev: ID of device containing file + /// st_dev: ID of device containing file public var dev: UInt64 = 0 - /// st_mode: file type and mode (permissions) + /// st_ino: inode number + public var ino: UInt64 = 0 + + /// st_mode: file type and mode (permissions) public var mode: UInt32 = 0 - /// st_nlink: number of hard links + /// st_nlink: number of hard links public var nlink: UInt64 = 0 - /// st_uid: user ID of owner + /// st_uid: user ID of owner public var uid: UInt32 = 0 - /// st_gid: group ID of owner + /// st_gid: group ID of owner public var gid: UInt32 = 0 - /// st_rdev: device ID (if special file) + /// st_rdev: device ID (if special file) public var rdev: UInt64 = 0 - /// st_size: total size in bytes + /// st_size: total size in bytes public var size: Int64 = 0 + /// st_blksize: preferred block size for filesystem I/O + public var blksize: Int64 = 0 + + /// st_blocks: number of 512-byte blocks allocated + public var blocks: Int64 = 0 + /// st_atim: time of last access public var atime: SwiftProtobuf.Google_Protobuf_Timestamp { get {return _atime ?? SwiftProtobuf.Google_Protobuf_Timestamp()} @@ -1014,28 +1023,31 @@ public struct Com_Apple_Containerization_Sandbox_V3_Stat: Sendable { fileprivate var _ctime: SwiftProtobuf.Google_Protobuf_Timestamp? = nil } -public struct Com_Apple_Containerization_Sandbox_V3_StatResponse: Sendable { +public struct Com_Apple_Containerization_Sandbox_V3_StatResponse: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. public var stat: Com_Apple_Containerization_Sandbox_V3_Stat { - get {return _stat ?? Com_Apple_Containerization_Sandbox_V3_Stat()} - set {_stat = newValue} + get {return _storage._stat ?? Com_Apple_Containerization_Sandbox_V3_Stat()} + set {_uniqueStorage()._stat = newValue} } /// Returns true if `stat` has been explicitly set. - public var hasStat: Bool {return self._stat != nil} + public var hasStat: Bool {return _storage._stat != nil} /// Clears the value of `stat`. Subsequent reads from it will return its default value. - public mutating func clearStat() {self._stat = nil} + public mutating func clearStat() {_uniqueStorage()._stat = nil} /// Non-empty if stat failed. - public var error: String = String() + public var error: String { + get {return _storage._error} + set {_uniqueStorage()._error = newValue} + } public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} - fileprivate var _stat: Com_Apple_Containerization_Sandbox_V3_Stat? = nil + fileprivate var _storage = _StorageClass.defaultInstance } public struct Com_Apple_Containerization_Sandbox_V3_IpLinkSetRequest: Sendable { @@ -3066,15 +3078,18 @@ extension Com_Apple_Containerization_Sandbox_V3_Stat: SwiftProtobuf.Message, Swi public static let protoMessageName: String = _protobuf_package + ".Stat" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .same(proto: "dev"), - 2: .same(proto: "mode"), - 3: .same(proto: "nlink"), - 4: .same(proto: "uid"), - 5: .same(proto: "gid"), - 6: .same(proto: "rdev"), - 7: .same(proto: "size"), - 8: .same(proto: "atime"), - 9: .same(proto: "mtime"), - 10: .same(proto: "ctime"), + 2: .same(proto: "ino"), + 3: .same(proto: "mode"), + 4: .same(proto: "nlink"), + 5: .same(proto: "uid"), + 6: .same(proto: "gid"), + 7: .same(proto: "rdev"), + 8: .same(proto: "size"), + 9: .same(proto: "blksize"), + 10: .same(proto: "blocks"), + 11: .same(proto: "atime"), + 12: .same(proto: "mtime"), + 13: .same(proto: "ctime"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -3084,15 +3099,18 @@ extension Com_Apple_Containerization_Sandbox_V3_Stat: SwiftProtobuf.Message, Swi // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { case 1: try { try decoder.decodeSingularUInt64Field(value: &self.dev) }() - case 2: try { try decoder.decodeSingularUInt32Field(value: &self.mode) }() - case 3: try { try decoder.decodeSingularUInt64Field(value: &self.nlink) }() - case 4: try { try decoder.decodeSingularUInt32Field(value: &self.uid) }() - case 5: try { try decoder.decodeSingularUInt32Field(value: &self.gid) }() - case 6: try { try decoder.decodeSingularUInt64Field(value: &self.rdev) }() - case 7: try { try decoder.decodeSingularInt64Field(value: &self.size) }() - case 8: try { try decoder.decodeSingularMessageField(value: &self._atime) }() - case 9: try { try decoder.decodeSingularMessageField(value: &self._mtime) }() - case 10: try { try decoder.decodeSingularMessageField(value: &self._ctime) }() + case 2: try { try decoder.decodeSingularUInt64Field(value: &self.ino) }() + case 3: try { try decoder.decodeSingularUInt32Field(value: &self.mode) }() + case 4: try { try decoder.decodeSingularUInt64Field(value: &self.nlink) }() + case 5: try { try decoder.decodeSingularUInt32Field(value: &self.uid) }() + case 6: try { try decoder.decodeSingularUInt32Field(value: &self.gid) }() + case 7: try { try decoder.decodeSingularUInt64Field(value: &self.rdev) }() + case 8: try { try decoder.decodeSingularInt64Field(value: &self.size) }() + case 9: try { try decoder.decodeSingularInt64Field(value: &self.blksize) }() + case 10: try { try decoder.decodeSingularInt64Field(value: &self.blocks) }() + case 11: try { try decoder.decodeSingularMessageField(value: &self._atime) }() + case 12: try { try decoder.decodeSingularMessageField(value: &self._mtime) }() + case 13: try { try decoder.decodeSingularMessageField(value: &self._ctime) }() default: break } } @@ -3106,44 +3124,56 @@ extension Com_Apple_Containerization_Sandbox_V3_Stat: SwiftProtobuf.Message, Swi if self.dev != 0 { try visitor.visitSingularUInt64Field(value: self.dev, fieldNumber: 1) } + if self.ino != 0 { + try visitor.visitSingularUInt64Field(value: self.ino, fieldNumber: 2) + } if self.mode != 0 { - try visitor.visitSingularUInt32Field(value: self.mode, fieldNumber: 2) + try visitor.visitSingularUInt32Field(value: self.mode, fieldNumber: 3) } if self.nlink != 0 { - try visitor.visitSingularUInt64Field(value: self.nlink, fieldNumber: 3) + try visitor.visitSingularUInt64Field(value: self.nlink, fieldNumber: 4) } if self.uid != 0 { - try visitor.visitSingularUInt32Field(value: self.uid, fieldNumber: 4) + try visitor.visitSingularUInt32Field(value: self.uid, fieldNumber: 5) } if self.gid != 0 { - try visitor.visitSingularUInt32Field(value: self.gid, fieldNumber: 5) + try visitor.visitSingularUInt32Field(value: self.gid, fieldNumber: 6) } if self.rdev != 0 { - try visitor.visitSingularUInt64Field(value: self.rdev, fieldNumber: 6) + try visitor.visitSingularUInt64Field(value: self.rdev, fieldNumber: 7) } if self.size != 0 { - try visitor.visitSingularInt64Field(value: self.size, fieldNumber: 7) + try visitor.visitSingularInt64Field(value: self.size, fieldNumber: 8) + } + if self.blksize != 0 { + try visitor.visitSingularInt64Field(value: self.blksize, fieldNumber: 9) + } + if self.blocks != 0 { + try visitor.visitSingularInt64Field(value: self.blocks, fieldNumber: 10) } try { if let v = self._atime { - try visitor.visitSingularMessageField(value: v, fieldNumber: 8) + try visitor.visitSingularMessageField(value: v, fieldNumber: 11) } }() try { if let v = self._mtime { - try visitor.visitSingularMessageField(value: v, fieldNumber: 9) + try visitor.visitSingularMessageField(value: v, fieldNumber: 12) } }() try { if let v = self._ctime { - try visitor.visitSingularMessageField(value: v, fieldNumber: 10) + try visitor.visitSingularMessageField(value: v, fieldNumber: 13) } }() try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_Stat, rhs: Com_Apple_Containerization_Sandbox_V3_Stat) -> Bool { if lhs.dev != rhs.dev {return false} + if lhs.ino != rhs.ino {return false} if lhs.mode != rhs.mode {return false} if lhs.nlink != rhs.nlink {return false} if lhs.uid != rhs.uid {return false} if lhs.gid != rhs.gid {return false} if lhs.rdev != rhs.rdev {return false} if lhs.size != rhs.size {return false} + if lhs.blksize != rhs.blksize {return false} + if lhs.blocks != rhs.blocks {return false} if lhs._atime != rhs._atime {return false} if lhs._mtime != rhs._mtime {return false} if lhs._ctime != rhs._ctime {return false} @@ -3159,36 +3189,74 @@ extension Com_Apple_Containerization_Sandbox_V3_StatResponse: SwiftProtobuf.Mess 2: .same(proto: "error"), ] + fileprivate class _StorageClass { + var _stat: Com_Apple_Containerization_Sandbox_V3_Stat? = nil + var _error: String = String() + + // This property is used as the initial default value for new instances of the type. + // The type itself is protecting the reference to its storage via CoW semantics. + // This will force a copy to be made of this reference when the first mutation occurs; + // hence, it is safe to mark this as `nonisolated(unsafe)`. + static nonisolated(unsafe) let defaultInstance = _StorageClass() + + private init() {} + + init(copying source: _StorageClass) { + _stat = source._stat + _error = source._error + } + } + + fileprivate mutating func _uniqueStorage() -> _StorageClass { + if !isKnownUniquelyReferenced(&_storage) { + _storage = _StorageClass(copying: _storage) + } + return _storage + } + public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._stat) }() - case 2: try { try decoder.decodeSingularStringField(value: &self.error) }() - default: break + _ = _uniqueStorage() + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &_storage._stat) }() + case 2: try { try decoder.decodeSingularStringField(value: &_storage._error) }() + default: break + } } } } public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._stat { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - if !self.error.isEmpty { - try visitor.visitSingularStringField(value: self.error, fieldNumber: 2) + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = _storage._stat { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + if !_storage._error.isEmpty { + try visitor.visitSingularStringField(value: _storage._error, fieldNumber: 2) + } } try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_StatResponse, rhs: Com_Apple_Containerization_Sandbox_V3_StatResponse) -> Bool { - if lhs._stat != rhs._stat {return false} - if lhs.error != rhs.error {return false} + if lhs._storage !== rhs._storage { + let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in + let _storage = _args.0 + let rhs_storage = _args.1 + if _storage._stat != rhs_storage._stat {return false} + if _storage._error != rhs_storage._error {return false} + return true + } + if !storagesAreEqual {return false} + } if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Sources/Containerization/SandboxContext/SandboxContext.proto b/Sources/Containerization/SandboxContext/SandboxContext.proto index f9e9e3fc..e30edbe9 100644 --- a/Sources/Containerization/SandboxContext/SandboxContext.proto +++ b/Sources/Containerization/SandboxContext/SandboxContext.proto @@ -272,16 +272,19 @@ message CopyResponse { message StatRequest { string path = 1; } message Stat { - uint64 dev = 1; // st_dev: ID of device containing file - uint32 mode = 2; // st_mode: file type and mode (permissions) - uint64 nlink = 3; // st_nlink: number of hard links - uint32 uid = 4; // st_uid: user ID of owner - uint32 gid = 5; // st_gid: group ID of owner - uint64 rdev = 6; // st_rdev: device ID (if special file) - int64 size = 7; // st_size: total size in bytes - google.protobuf.Timestamp atime = 8; // st_atim: time of last access - google.protobuf.Timestamp mtime = 9; // st_mtim: time of last modification - google.protobuf.Timestamp ctime = 10; // st_ctim: time of last status change + uint64 dev = 1; // st_dev: ID of device containing file + uint64 ino = 2; // st_ino: inode number + uint32 mode = 3; // st_mode: file type and mode (permissions) + uint64 nlink = 4; // st_nlink: number of hard links + uint32 uid = 5; // st_uid: user ID of owner + uint32 gid = 6; // st_gid: group ID of owner + uint64 rdev = 7; // st_rdev: device ID (if special file) + int64 size = 8; // st_size: total size in bytes + int64 blksize = 9; // st_blksize: preferred block size for filesystem I/O + int64 blocks = 10; // st_blocks: number of 512-byte blocks allocated + google.protobuf.Timestamp atime = 11; // st_atim: time of last access + google.protobuf.Timestamp mtime = 12; // st_mtim: time of last modification + google.protobuf.Timestamp ctime = 13; // st_ctim: time of last status change } message StatResponse { diff --git a/Sources/Containerization/Vminitd.swift b/Sources/Containerization/Vminitd.swift index a1c8d714..cbb020e2 100644 --- a/Sources/Containerization/Vminitd.swift +++ b/Sources/Containerization/Vminitd.swift @@ -450,7 +450,7 @@ extension Vminitd { public func stat( path: URL - ) async throws -> Com_Apple_Containerization_Sandbox_V3_Stat { + ) async throws -> Foundation.stat { let request = Com_Apple_Containerization_Sandbox_V3_StatRequest.with { $0.path = path.path } @@ -460,7 +460,22 @@ extension Vminitd { throw ContainerizationError(.internalError, message: "stat: \(response.error)") } - return response.stat + let s = response.stat + var result = Foundation.stat() + result.st_dev = dev_t(s.dev) + result.st_mode = mode_t(s.mode) + result.st_nlink = nlink_t(s.nlink) + result.st_ino = ino_t(s.ino) + result.st_uid = s.uid + result.st_gid = s.gid + result.st_rdev = dev_t(s.rdev) + // result.st_atimespec = timespec(tv_sec: Int(s.atime.seconds), tv_nsec: Int(s.atime.nanos)) + // result.st_mtimespec = timespec(tv_sec: Int(s.mtime.seconds), tv_nsec: Int(s.mtime.nanos)) + // result.st_ctimespec = timespec(tv_sec: Int(s.ctime.seconds), tv_nsec: Int(s.ctime.nanos)) + result.st_size = off_t(s.size) + result.st_blocks = blkcnt_t(s.blocks) + result.st_blksize = Int32(s.blksize) + return result } /// Unified copy control plane. Sends a CopyRequest over gRPC and processes diff --git a/vminitd/Sources/vminitd/Server+GRPC.swift b/vminitd/Sources/vminitd/Server+GRPC.swift index 0f4c8921..40df37bb 100644 --- a/vminitd/Sources/vminitd/Server+GRPC.swift +++ b/vminitd/Sources/vminitd/Server+GRPC.swift @@ -382,12 +382,15 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncProvid return .with { $0.stat = .with { $0.dev = UInt64(s.st_dev) + $0.ino = UInt64(s.st_ino) $0.mode = s.st_mode $0.nlink = UInt64(s.st_nlink) $0.uid = s.st_uid $0.gid = s.st_gid $0.rdev = UInt64(s.st_rdev) $0.size = Int64(s.st_size) + $0.blksize = Int64(s.st_blksize) + $0.blocks = Int64(s.st_blocks) $0.atime = .with { $0.seconds = Int64(s.st_atim.tv_sec) $0.nanos = Int32(s.st_atim.tv_nsec) From 3e2d430ba8e6ce661b133a95dc58acf24f60f240 Mon Sep 17 00:00:00 2001 From: Jaewon Date: Tue, 7 Apr 2026 11:13:24 -0700 Subject: [PATCH 3/3] Add public struct Stat --- Sources/Containerization/Vminitd.swift | 33 +++++----- Sources/ContainerizationOS/Stat.swift | 88 ++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 16 deletions(-) create mode 100644 Sources/ContainerizationOS/Stat.swift diff --git a/Sources/Containerization/Vminitd.swift b/Sources/Containerization/Vminitd.swift index cbb020e2..7dcb2f52 100644 --- a/Sources/Containerization/Vminitd.swift +++ b/Sources/Containerization/Vminitd.swift @@ -448,9 +448,10 @@ extension Vminitd { public let totalSize: UInt64 } + /// Stat a path in the guest filesystem and return its metadata. public func stat( path: URL - ) async throws -> Foundation.stat { + ) async throws -> ContainerizationOS.Stat { let request = Com_Apple_Containerization_Sandbox_V3_StatRequest.with { $0.path = path.path } @@ -461,21 +462,21 @@ extension Vminitd { } let s = response.stat - var result = Foundation.stat() - result.st_dev = dev_t(s.dev) - result.st_mode = mode_t(s.mode) - result.st_nlink = nlink_t(s.nlink) - result.st_ino = ino_t(s.ino) - result.st_uid = s.uid - result.st_gid = s.gid - result.st_rdev = dev_t(s.rdev) - // result.st_atimespec = timespec(tv_sec: Int(s.atime.seconds), tv_nsec: Int(s.atime.nanos)) - // result.st_mtimespec = timespec(tv_sec: Int(s.mtime.seconds), tv_nsec: Int(s.mtime.nanos)) - // result.st_ctimespec = timespec(tv_sec: Int(s.ctime.seconds), tv_nsec: Int(s.ctime.nanos)) - result.st_size = off_t(s.size) - result.st_blocks = blkcnt_t(s.blocks) - result.st_blksize = Int32(s.blksize) - return result + return ContainerizationOS.Stat( + dev: s.dev, + ino: s.ino, + mode: s.mode, + nlink: s.nlink, + uid: s.uid, + gid: s.gid, + rdev: s.rdev, + size: s.size, + blksize: s.blksize, + blocks: s.blocks, + atime: TimeSpec(seconds: s.atime.seconds, nanoseconds: s.atime.nanos), + mtime: TimeSpec(seconds: s.mtime.seconds, nanoseconds: s.mtime.nanos), + ctime: TimeSpec(seconds: s.ctime.seconds, nanoseconds: s.ctime.nanos) + ) } /// Unified copy control plane. Sends a CopyRequest over gRPC and processes diff --git a/Sources/ContainerizationOS/Stat.swift b/Sources/ContainerizationOS/Stat.swift new file mode 100644 index 00000000..92dbeecd --- /dev/null +++ b/Sources/ContainerizationOS/Stat.swift @@ -0,0 +1,88 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2025-2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +/// A timestamp with second and nanosecond precision. +public struct TimeSpec: Sendable, Hashable { + /// Seconds since the Unix epoch. + public var seconds: Int64 + /// Nanoseconds past the second. + public var nanoseconds: Int32 + + public init(seconds: Int64, nanoseconds: Int32) { + self.seconds = seconds + self.nanoseconds = nanoseconds + } +} + +/// File metadata returned by a `stat` call. +public struct Stat: Sendable, Hashable { + /// ID of device containing file (`st_dev`). + public var dev: UInt64 + /// Inode number (`st_ino`). + public var ino: UInt64 + /// File type and mode (`st_mode`). + public var mode: UInt32 + /// Number of hard links (`st_nlink`). + public var nlink: UInt64 + /// User ID of owner (`st_uid`). + public var uid: UInt32 + /// Group ID of owner (`st_gid`). + public var gid: UInt32 + /// Device ID, if special file (`st_rdev`). + public var rdev: UInt64 + /// Total size in bytes (`st_size`). + public var size: Int64 + /// Preferred I/O block size (`st_blksize`). + public var blksize: Int64 + /// Number of 512-byte blocks allocated (`st_blocks`). + public var blocks: Int64 + /// Time of last access (`st_atim`). + public var atime: TimeSpec + /// Time of last modification (`st_mtim`). + public var mtime: TimeSpec + /// Time of last status change (`st_ctim`). + public var ctime: TimeSpec + + public init( + dev: UInt64, + ino: UInt64, + mode: UInt32, + nlink: UInt64, + uid: UInt32, + gid: UInt32, + rdev: UInt64, + size: Int64, + blksize: Int64, + blocks: Int64, + atime: TimeSpec, + mtime: TimeSpec, + ctime: TimeSpec + ) { + self.dev = dev + self.ino = ino + self.mode = mode + self.nlink = nlink + self.uid = uid + self.gid = gid + self.rdev = rdev + self.size = size + self.blksize = blksize + self.blocks = blocks + self.atime = atime + self.mtime = mtime + self.ctime = ctime + } +}