Add stat RPC (#614)

Add `Stat` RPC to vminitd.

`Stat` RPC is used to stat file system objects in the VM. It accepts the
path to the object (resolved against VM root) and returns the stat
result.
This commit is contained in:
jwhur
2026-04-27 14:44:33 -07:00
committed by GitHub
parent 59f599eed5
commit b770cd2a02
8 changed files with 826 additions and 0 deletions
@@ -166,6 +166,19 @@ public enum Com_Apple_Containerization_Sandbox_V3_SandboxContext: Sendable {
type: .serverStreaming
)
}
/// Namespace for "Stat" metadata.
public enum Stat: Sendable {
/// Request type for "Stat".
public typealias Input = Com_Apple_Containerization_Sandbox_V3_StatRequest
/// Response type for "Stat".
public typealias Output = Com_Apple_Containerization_Sandbox_V3_StatResponse
/// Descriptor for "Stat".
public static let descriptor = GRPCCore.MethodDescriptor(
service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "com.apple.containerization.sandbox.v3.SandboxContext"),
method: "Stat",
type: .unary
)
}
/// Namespace for "CreateProcess" metadata.
public enum CreateProcess: Sendable {
/// Request type for "CreateProcess".
@@ -412,6 +425,7 @@ public enum Com_Apple_Containerization_Sandbox_V3_SandboxContext: Sendable {
SetupEmulator.descriptor,
WriteFile.descriptor,
Copy.descriptor,
Stat.descriptor,
CreateProcess.descriptor,
DeleteProcess.descriptor,
StartProcess.descriptor,
@@ -641,6 +655,24 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContext {
context: GRPCCore.ServerContext
) async throws -> GRPCCore.StreamingServerResponse<Com_Apple_Containerization_Sandbox_V3_CopyResponse>
/// Handle the "Stat" method.
///
/// > Source IDL Documentation:
/// >
/// > Stat a path in the guest filesystem.
///
/// - Parameters:
/// - request: A streaming request of `Com_Apple_Containerization_Sandbox_V3_StatRequest` messages.
/// - context: Context providing information about the RPC.
/// - Throws: Any error which occurred during the processing of the request. Thrown errors
/// of type `RPCError` are mapped to appropriate statuses. All other errors are converted
/// to an internal error.
/// - Returns: A streaming response of `Com_Apple_Containerization_Sandbox_V3_StatResponse` messages.
func stat(
request: GRPCCore.StreamingServerRequest<Com_Apple_Containerization_Sandbox_V3_StatRequest>,
context: GRPCCore.ServerContext
) async throws -> GRPCCore.StreamingServerResponse<Com_Apple_Containerization_Sandbox_V3_StatResponse>
/// Handle the "CreateProcess" method.
///
/// > Source IDL Documentation:
@@ -1161,6 +1193,24 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContext {
context: GRPCCore.ServerContext
) async throws -> GRPCCore.StreamingServerResponse<Com_Apple_Containerization_Sandbox_V3_CopyResponse>
/// Handle the "Stat" method.
///
/// > Source IDL Documentation:
/// >
/// > Stat a path in the guest filesystem.
///
/// - Parameters:
/// - request: A request containing a single `Com_Apple_Containerization_Sandbox_V3_StatRequest` message.
/// - context: Context providing information about the RPC.
/// - Throws: Any error which occurred during the processing of the request. Thrown errors
/// of type `RPCError` are mapped to appropriate statuses. All other errors are converted
/// to an internal error.
/// - Returns: A response containing a single `Com_Apple_Containerization_Sandbox_V3_StatResponse` message.
func stat(
request: GRPCCore.ServerRequest<Com_Apple_Containerization_Sandbox_V3_StatRequest>,
context: GRPCCore.ServerContext
) async throws -> GRPCCore.ServerResponse<Com_Apple_Containerization_Sandbox_V3_StatResponse>
/// Handle the "CreateProcess" method.
///
/// > Source IDL Documentation:
@@ -1680,6 +1730,24 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContext {
context: GRPCCore.ServerContext
) async throws
/// Handle the "Stat" method.
///
/// > Source IDL Documentation:
/// >
/// > Stat a path in the guest filesystem.
///
/// - Parameters:
/// - request: A `Com_Apple_Containerization_Sandbox_V3_StatRequest` message.
/// - context: Context providing information about the RPC.
/// - Throws: Any error which occurred during the processing of the request. Thrown errors
/// of type `RPCError` are mapped to appropriate statuses. All other errors are converted
/// to an internal error.
/// - Returns: A `Com_Apple_Containerization_Sandbox_V3_StatResponse` to respond with.
func stat(
request: Com_Apple_Containerization_Sandbox_V3_StatRequest,
context: GRPCCore.ServerContext
) async throws -> Com_Apple_Containerization_Sandbox_V3_StatResponse
/// Handle the "CreateProcess" method.
///
/// > Source IDL Documentation:
@@ -2121,6 +2189,17 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContext.StreamingServiceP
)
}
)
router.registerHandler(
forMethod: Com_Apple_Containerization_Sandbox_V3_SandboxContext.Method.Stat.descriptor,
deserializer: GRPCProtobuf.ProtobufDeserializer<Com_Apple_Containerization_Sandbox_V3_StatRequest>(),
serializer: GRPCProtobuf.ProtobufSerializer<Com_Apple_Containerization_Sandbox_V3_StatResponse>(),
handler: { request, context in
try await self.stat(
request: request,
context: context
)
}
)
router.registerHandler(
forMethod: Com_Apple_Containerization_Sandbox_V3_SandboxContext.Method.CreateProcess.descriptor,
deserializer: GRPCProtobuf.ProtobufDeserializer<Com_Apple_Containerization_Sandbox_V3_CreateProcessRequest>(),
@@ -2435,6 +2514,17 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContext.ServiceProtocol {
return response
}
public func stat(
request: GRPCCore.StreamingServerRequest<Com_Apple_Containerization_Sandbox_V3_StatRequest>,
context: GRPCCore.ServerContext
) async throws -> GRPCCore.StreamingServerResponse<Com_Apple_Containerization_Sandbox_V3_StatResponse> {
let response = try await self.stat(
request: GRPCCore.ServerRequest(stream: request),
context: context
)
return GRPCCore.StreamingServerResponse(single: response)
}
public func createProcess(
request: GRPCCore.StreamingServerRequest<Com_Apple_Containerization_Sandbox_V3_CreateProcessRequest>,
context: GRPCCore.ServerContext
@@ -2771,6 +2861,19 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContext.SimpleServiceProt
)
}
public func stat(
request: GRPCCore.ServerRequest<Com_Apple_Containerization_Sandbox_V3_StatRequest>,
context: GRPCCore.ServerContext
) async throws -> GRPCCore.ServerResponse<Com_Apple_Containerization_Sandbox_V3_StatResponse> {
return GRPCCore.ServerResponse<Com_Apple_Containerization_Sandbox_V3_StatResponse>(
message: try await self.stat(
request: request.message,
context: context
),
metadata: [:]
)
}
public func createProcess(
request: GRPCCore.ServerRequest<Com_Apple_Containerization_Sandbox_V3_CreateProcessRequest>,
context: GRPCCore.ServerContext
@@ -3251,6 +3354,29 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContext {
onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse<Com_Apple_Containerization_Sandbox_V3_CopyResponse>) async throws -> Result
) async throws -> Result where Result: Sendable
/// Call the "Stat" method.
///
/// > Source IDL Documentation:
/// >
/// > Stat a path in the guest filesystem.
///
/// - Parameters:
/// - request: A request containing a single `Com_Apple_Containerization_Sandbox_V3_StatRequest` message.
/// - serializer: A serializer for `Com_Apple_Containerization_Sandbox_V3_StatRequest` messages.
/// - deserializer: A deserializer for `Com_Apple_Containerization_Sandbox_V3_StatResponse` messages.
/// - options: Options to apply to this RPC.
/// - handleResponse: A closure which handles the response, the result of which is
/// returned to the caller. Returning from the closure will cancel the RPC if it
/// hasn't already finished.
/// - Returns: The result of `handleResponse`.
func stat<Result>(
request: GRPCCore.ClientRequest<Com_Apple_Containerization_Sandbox_V3_StatRequest>,
serializer: some GRPCCore.MessageSerializer<Com_Apple_Containerization_Sandbox_V3_StatRequest>,
deserializer: some GRPCCore.MessageDeserializer<Com_Apple_Containerization_Sandbox_V3_StatResponse>,
options: GRPCCore.CallOptions,
onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse<Com_Apple_Containerization_Sandbox_V3_StatResponse>) async throws -> Result
) async throws -> Result where Result: Sendable
/// Call the "CreateProcess" method.
///
/// > Source IDL Documentation:
@@ -4027,6 +4153,40 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContext {
)
}
/// Call the "Stat" method.
///
/// > Source IDL Documentation:
/// >
/// > Stat a path in the guest filesystem.
///
/// - Parameters:
/// - request: A request containing a single `Com_Apple_Containerization_Sandbox_V3_StatRequest` message.
/// - serializer: A serializer for `Com_Apple_Containerization_Sandbox_V3_StatRequest` messages.
/// - deserializer: A deserializer for `Com_Apple_Containerization_Sandbox_V3_StatResponse` messages.
/// - options: Options to apply to this RPC.
/// - handleResponse: A closure which handles the response, the result of which is
/// returned to the caller. Returning from the closure will cancel the RPC if it
/// hasn't already finished.
/// - Returns: The result of `handleResponse`.
public func stat<Result>(
request: GRPCCore.ClientRequest<Com_Apple_Containerization_Sandbox_V3_StatRequest>,
serializer: some GRPCCore.MessageSerializer<Com_Apple_Containerization_Sandbox_V3_StatRequest>,
deserializer: some GRPCCore.MessageDeserializer<Com_Apple_Containerization_Sandbox_V3_StatResponse>,
options: GRPCCore.CallOptions = .defaults,
onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse<Com_Apple_Containerization_Sandbox_V3_StatResponse>) async throws -> Result = { response in
try response.message
}
) async throws -> Result where Result: Sendable {
try await self.client.unary(
request: request,
descriptor: Com_Apple_Containerization_Sandbox_V3_SandboxContext.Method.Stat.descriptor,
serializer: serializer,
deserializer: deserializer,
options: options,
onResponse: handleResponse
)
}
/// Call the "CreateProcess" method.
///
/// > Source IDL Documentation:
@@ -4935,6 +5095,35 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContext.ClientProtocol {
)
}
/// Call the "Stat" method.
///
/// > Source IDL Documentation:
/// >
/// > Stat a path in the guest filesystem.
///
/// - Parameters:
/// - request: A request containing a single `Com_Apple_Containerization_Sandbox_V3_StatRequest` message.
/// - options: Options to apply to this RPC.
/// - handleResponse: A closure which handles the response, the result of which is
/// returned to the caller. Returning from the closure will cancel the RPC if it
/// hasn't already finished.
/// - Returns: The result of `handleResponse`.
public func stat<Result>(
request: GRPCCore.ClientRequest<Com_Apple_Containerization_Sandbox_V3_StatRequest>,
options: GRPCCore.CallOptions = .defaults,
onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse<Com_Apple_Containerization_Sandbox_V3_StatResponse>) async throws -> Result = { response in
try response.message
}
) async throws -> Result where Result: Sendable {
try await self.stat(
request: request,
serializer: GRPCProtobuf.ProtobufSerializer<Com_Apple_Containerization_Sandbox_V3_StatRequest>(),
deserializer: GRPCProtobuf.ProtobufDeserializer<Com_Apple_Containerization_Sandbox_V3_StatResponse>(),
options: options,
onResponse: handleResponse
)
}
/// Call the "CreateProcess" method.
///
/// > Source IDL Documentation:
@@ -5792,6 +5981,39 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContext.ClientProtocol {
)
}
/// Call the "Stat" method.
///
/// > Source IDL Documentation:
/// >
/// > Stat a path in the guest filesystem.
///
/// - Parameters:
/// - message: request message to send.
/// - metadata: Additional metadata to send, defaults to empty.
/// - options: Options to apply to this RPC, defaults to `.defaults`.
/// - handleResponse: A closure which handles the response, the result of which is
/// returned to the caller. Returning from the closure will cancel the RPC if it
/// hasn't already finished.
/// - Returns: The result of `handleResponse`.
public func stat<Result>(
_ message: Com_Apple_Containerization_Sandbox_V3_StatRequest,
metadata: GRPCCore.Metadata = [:],
options: GRPCCore.CallOptions = .defaults,
onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse<Com_Apple_Containerization_Sandbox_V3_StatResponse>) async throws -> Result = { response in
try response.message
}
) async throws -> Result where Result: Sendable {
let request = GRPCCore.ClientRequest<Com_Apple_Containerization_Sandbox_V3_StatRequest>(
message: message,
metadata: metadata
)
return try await self.stat(
request: request,
options: options,
onResponse: handleResponse
)
}
/// Call the "CreateProcess" method.
///
/// > Source IDL Documentation:
@@ -941,6 +941,119 @@ 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_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
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_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 {_atime ?? SwiftProtobuf.Google_Protobuf_Timestamp()}
set {_atime = newValue}
}
/// Returns true if `atime` has been explicitly set.
public var hasAtime: Bool {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 {_mtime ?? SwiftProtobuf.Google_Protobuf_Timestamp()}
set {_mtime = newValue}
}
/// Returns true if `mtime` has been explicitly set.
public var hasMtime: Bool {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 {_ctime ?? SwiftProtobuf.Google_Protobuf_Timestamp()}
set {_ctime = newValue}
}
/// Returns true if `ctime` has been explicitly set.
public var hasCtime: Bool {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: @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 {_storage._stat ?? Com_Apple_Containerization_Sandbox_V3_Stat()}
set {_uniqueStorage()._stat = newValue}
}
/// Returns true if `stat` has been explicitly set.
public var hasStat: Bool {_storage._stat != nil}
/// Clears the value of `stat`. Subsequent reads from it will return its default value.
public mutating func clearStat() {_uniqueStorage()._stat = nil}
/// Non-empty if stat failed.
public var error: String {
get {_storage._error}
set {_uniqueStorage()._error = newValue}
}
public var unknownFields = SwiftProtobuf.UnknownStorage()
public init() {}
fileprivate var _storage = _StorageClass.defaultInstance
}
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
@@ -2814,6 +2927,207 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyResponse.Status: SwiftProtob
public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0METADATA\0\u{1}COMPLETE\0")
}
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(bytecode: "\0\u{1}path\0")
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(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<V: SwiftProtobuf.Visitor>(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(bytecode: "\0\u{1}dev\0\u{1}ino\0\u{1}mode\0\u{1}nlink\0\u{1}uid\0\u{1}gid\0\u{1}rdev\0\u{1}size\0\u{1}blksize\0\u{1}blocks\0\u{1}atime\0\u{1}mtime\0\u{1}ctime\0")
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(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.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
}
}
}
public func traverse<V: SwiftProtobuf.Visitor>(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.ino != 0 {
try visitor.visitSingularUInt64Field(value: self.ino, fieldNumber: 2)
}
if self.mode != 0 {
try visitor.visitSingularUInt32Field(value: self.mode, fieldNumber: 3)
}
if self.nlink != 0 {
try visitor.visitSingularUInt64Field(value: self.nlink, fieldNumber: 4)
}
if self.uid != 0 {
try visitor.visitSingularUInt32Field(value: self.uid, fieldNumber: 5)
}
if self.gid != 0 {
try visitor.visitSingularUInt32Field(value: self.gid, fieldNumber: 6)
}
if self.rdev != 0 {
try visitor.visitSingularUInt64Field(value: self.rdev, fieldNumber: 7)
}
if self.size != 0 {
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: 11)
} }()
try { if let v = self._mtime {
try visitor.visitSingularMessageField(value: v, fieldNumber: 12)
} }()
try { if let v = self._ctime {
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}
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(bytecode: "\0\u{1}stat\0\u{1}error\0")
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<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
_ = _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<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
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._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
}
}
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(bytecode: "\0\u{1}interface\0\u{1}up\0\u{1}mtu\0")
@@ -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,29 @@ message CopyResponse {
string error = 4;
}
message StatRequest { string path = 1; }
message Stat {
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 {
Stat stat = 1;
string error = 2; // Non-empty if stat failed.
}
message IpLinkSetRequest {
string interface = 1;
bool up = 2;
+31
View File
@@ -464,6 +464,37 @@ extension Vminitd {
public let totalSize: UInt64
}
/// Stat a path in the guest filesystem and return its metadata.
public func stat(
path: URL
) async throws -> ContainerizationOS.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)")
}
let s = response.stat
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
/// the response stream. Data transfer happens over a separate vsock connection
/// managed by the caller.
+88
View File
@@ -0,0 +1,88 @@
//===----------------------------------------------------------------------===//
// Copyright © 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
}
}
+90
View File
@@ -1635,6 +1635,96 @@ extension IntegrationSuite {
}
}
func testStat() async throws {
let id = "test-stat"
let bs = try await bootstrap(id)
let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in
config.process.arguments = ["sleep", "100"]
config.bootLog = bs.bootLog
}
func assertExec(_ container: LinuxContainer, id: String, cmd: String) async throws {
let exec = try await container.exec(id) { config in
config.arguments = ["sh", "-c", cmd]
}
try await exec.start()
let status = try await exec.wait()
try await exec.delete()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "\(id) failed with exit code \(status.exitCode)")
}
}
do {
try await container.create()
try await container.start()
// regular file: "regular file" is exactly 12 bytes
try await assertExec(container, id: "create-regular-file", cmd: "echo -n 'regular file' > /tmp/regular-file.txt")
// directory
try await assertExec(container, id: "create-dir", cmd: "mkdir /tmp/test-dir")
// relative symlink so stat() resolves the target within the same directory
try await assertExec(container, id: "create-symlink", cmd: "ln -s regular-file.txt /tmp/test-link")
// FIFO
try await assertExec(container, id: "create-fifo", cmd: "mkfifo /tmp/test-fifo")
let vsock = try await container.dialVsock(port: 1024)
let vminitd = try Vminitd(connection: vsock, group: Self.eventLoop)
let root = URL(filePath: container.root)
// --- regular file ---
let regularStat = try await vminitd.stat(path: root.appending(path: "tmp/regular-file.txt"))
guard (regularStat.mode & UInt32(S_IFMT)) == S_IFREG else {
throw IntegrationError.assert(msg: "regular file: expected S_IFREG, got mode 0x\(String(regularStat.mode, radix: 16))")
}
guard regularStat.size == 12 else {
throw IntegrationError.assert(msg: "regular file: expected size 12, got \(regularStat.size)")
}
guard regularStat.ino > 0 else {
throw IntegrationError.assert(msg: "regular file: expected non-zero inode, got \(regularStat.ino)")
}
guard regularStat.nlink >= 1 else {
throw IntegrationError.assert(msg: "regular file: expected nlink >= 1, got \(regularStat.nlink)")
}
// --- directory ---
let dirStat = try await vminitd.stat(path: root.appending(path: "tmp/test-dir"))
guard (dirStat.mode & UInt32(S_IFMT)) == S_IFDIR else {
throw IntegrationError.assert(msg: "directory: expected S_IFDIR, got mode 0x\(String(dirStat.mode, radix: 16))")
}
// A directory always has at least 2 hard links (. and its entry in the parent)
guard dirStat.nlink >= 2 else {
throw IntegrationError.assert(msg: "directory: expected nlink >= 2, got \(dirStat.nlink)")
}
// --- symlink ---
// stat(2) follows symlinks, so the result reflects the target regular file
let symlinkStat = try await vminitd.stat(path: root.appending(path: "tmp/test-link"))
guard (symlinkStat.mode & UInt32(S_IFMT)) == S_IFREG else {
throw IntegrationError.assert(msg: "symlink (followed): expected S_IFREG, got mode 0x\(String(symlinkStat.mode, radix: 16))")
}
guard symlinkStat.size == regularStat.size else {
throw IntegrationError.assert(msg: "symlink (followed): expected size \(regularStat.size), got \(symlinkStat.size)")
}
// --- FIFO ---
let fifoStat = try await vminitd.stat(path: root.appending(path: "tmp/test-fifo"))
guard (fifoStat.mode & UInt32(S_IFMT)) == S_IFIFO else {
throw IntegrationError.assert(msg: "FIFO: expected S_IFIFO, got mode 0x\(String(fifoStat.mode, radix: 16))")
}
try await container.kill(SIGKILL)
try await container.wait()
try await container.stop()
} catch {
try? await container.stop()
throw error
}
}
func testCopyIn() async throws {
let id = "test-copy-in"
+1
View File
@@ -324,6 +324,7 @@ struct IntegrationSuite: AsyncParsableCommand {
Test("container capabilities OCI default", testCapabilitiesOCIDefault),
Test("container capabilities all capabilities", testCapabilitiesAllCapabilities),
Test("container capabilities file ownership", testCapabilitiesFileOwnership),
Test("container stat", testStat),
Test("container copy in", testCopyIn),
Test("container copy out", testCopyOut),
Test("container copy large file", testCopyLargeFile),
+55
View File
@@ -38,12 +38,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<CChar>, UnsafeMutablePointer<Musl.stat>) -> 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<CChar>, UnsafeMutablePointer<Glibc.stat>) -> Int32 = stat
#endif
extension ContainerizationError {
@@ -362,6 +364,59 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContext.SimpleServ
return .init()
}
func stat(
request: Com_Apple_Containerization_Sandbox_V3_StatRequest,
context: GRPCCore.ServerContext,
) 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.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)
}
$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