diff --git a/Sources/Containerization/LinuxContainer.swift b/Sources/Containerization/LinuxContainer.swift index c51cb622..a7f3607b 100644 --- a/Sources/Containerization/LinuxContainer.swift +++ b/Sources/Containerization/LinuxContainer.swift @@ -18,6 +18,7 @@ import ContainerizationArchive import ContainerizationError import ContainerizationExtras import ContainerizationOCI +import ContainerizationOS import Foundation import Logging import Synchronization @@ -1065,7 +1066,19 @@ extension LinuxContainer { } let isArchive = isDirectory.boolValue - let guestPath = URL(filePath: self.root).appending(path: destination.path) + let guestPath: URL = try await state.vm.withAgent { agent in + guard let vminitd = agent as? Vminitd else { + throw ContainerizationError(.unsupported, message: "copyIn requires Vminitd agent") + } + + return try await self.resolveCopyInGuestPath( + from: source, + to: destination, + sourceIsDirectory: isArchive, + using: vminitd + ) + } + let port = self.hostVsockPorts.wrappingAdd(1, ordering: .relaxed).oldValue let listener = try state.vm.listen(port) @@ -1150,6 +1163,46 @@ extension LinuxContainer { } } + private func resolveCopyInGuestPath( + from source: URL, + to destination: URL, + sourceIsDirectory: Bool, + using vminitd: Vminitd + ) async throws -> URL { + let guestDestination = URL(filePath: self.root).appending(path: destination.path) + + let stat: ContainerizationOS.Stat? + do { + stat = try await vminitd.stat(path: guestDestination) + } catch let error as ContainerizationError where error.code == .notFound { + stat = nil + } + // Any other error propagates so transport and permission failures are visible. + + guard let stat else { + if destination.hasDirectoryPath && !sourceIsDirectory { + throw ContainerizationError( + .invalidArgument, + message: "destination directory does not exist: \(destination.path)" + ) + } + return guestDestination + } + + let destinationIsDirectory = (stat.mode & UInt32(S_IFMT)) == UInt32(S_IFDIR) + guard destinationIsDirectory else { + if sourceIsDirectory { + throw ContainerizationError( + .invalidArgument, + message: "cannot copy directory over existing file: \(destination.path)" + ) + } + return guestDestination + } + + return guestDestination.appendingPathComponent(source.lastPathComponent) + } + /// Copy a file or directory from the container to the host. /// /// Data transfer happens over a dedicated vsock connection. For directories, diff --git a/Sources/Containerization/Vminitd.swift b/Sources/Containerization/Vminitd.swift index bf62a034..d23eff53 100644 --- a/Sources/Containerization/Vminitd.swift +++ b/Sources/Containerization/Vminitd.swift @@ -472,7 +472,12 @@ extension Vminitd { $0.path = path.path } - let response = try await client.stat(request) + let response: Com_Apple_Containerization_Sandbox_V3_StatResponse + do { + response = try await client.stat(request) + } catch let error as RPCError where error.code == .notFound { + throw ContainerizationError(.notFound, message: "stat: path not found '\(path.path)'", cause: error) + } guard response.error.isEmpty else { throw ContainerizationError(.internalError, message: "stat: \(response.error)") } diff --git a/vminitd/Sources/VminitdCore/Server+GRPC.swift b/vminitd/Sources/VminitdCore/Server+GRPC.swift index 23cca9f8..c035623d 100644 --- a/vminitd/Sources/VminitdCore/Server+GRPC.swift +++ b/vminitd/Sources/VminitdCore/Server+GRPC.swift @@ -384,6 +384,13 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContext.SimpleServ let result = _stat(request.path, &s) if result == -1 { let error = swiftErrno("stat") + if error.code == .ENOENT { + throw RPCError( + code: .notFound, + message: "stat: path not found '\(request.path)'", + cause: error + ) + } return .with { $0.error = "\(error)" } } return .with {