Skip to content

Commit

Permalink
Merge pull request #88 from trilemma-dev/fd-codable
Browse files Browse the repository at this point in the history
Adds `XPCFileDescriptorContainer' which wraps a file descriptor/handler
  • Loading branch information
jakaplan authored Apr 12, 2022
2 parents 318bb69 + da71e44 commit f193c0d
Show file tree
Hide file tree
Showing 9 changed files with 389 additions and 13 deletions.
1 change: 1 addition & 0 deletions Sources/SecureXPC/SecureXPC.docc/SecureXPC.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,5 @@ See ``XPCClient`` for more on how to retrieve a client and send requests.
- ``SequentialResultProvider``

### Other
- ``XPCFileDescriptorContainer``
- ``XPCRequestContext``
8 changes: 7 additions & 1 deletion Sources/SecureXPC/XPC Coders/XPCDecoder/XPCDecoderImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,15 @@ internal class XPCDecoderImpl: Decoder {
userInfo: self.userInfo)
}

func singleValueContainer() throws -> SingleValueDecodingContainer {
// Internal implementation so that XPC-specific functions of the container can be accessed
func xpcSingleValueContainer() -> XPCSingleValueDecodingContainer {
XPCSingleValueDecodingContainer(value: self.value,
codingPath: self.codingPath,
userInfo: self.userInfo)
}


func singleValueContainer() throws -> SingleValueDecodingContainer {
xpcSingleValueContainer()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,10 @@ internal class XPCSingleValueDecodingContainer: SingleValueDecodingContainer {
codingPath: self.codingPath,
userInfo: self.userInfo))
}

// MARK: XPC specific decoding

func accessAsEncodedValue(xpcType: xpc_type_t) throws -> xpc_object_t {
return try decode(xpcType: xpcType, transform: {$0})
}
}
15 changes: 10 additions & 5 deletions Sources/SecureXPC/XPC Coders/XPCEncoder/XPCEncoderImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,18 @@ internal class XPCEncoderImpl: Encoder, XPCContainer {

return container
}

// Internal implementation so that XPC-specific functions of the container can be accessed
func xpcSingleValueContainer() -> XPCSingleValueEncodingContainer {
let container = XPCSingleValueEncodingContainer(codingPath: self.codingPath)
self.container = container

func singleValueContainer() -> SingleValueEncodingContainer {
let container = XPCSingleValueEncodingContainer(codingPath: self.codingPath)
self.container = container
return container
}

return container
}
func singleValueContainer() -> SingleValueEncodingContainer {
return xpcSingleValueContainer()
}

internal func encodedValue() throws -> xpc_object_t? {
return try container?.encodedValue()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,10 @@ internal class XPCSingleValueEncodingContainer: SingleValueEncodingContainer, XP

try value.encode(to: encoder)
}

// MARK: XPC specific encoding

func setAlreadyEncodedValue(_ value: xpc_object_t) {
self.setValue(value)
}
}
9 changes: 9 additions & 0 deletions Sources/SecureXPC/XPCCommon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ func const(_ input: UnsafePointer<CChar>!) -> UnsafePointer<CChar>! {
return UnsafePointer(mutableCopy) // The result should never actually be mutated
}

/// Thrown by a "fake" `Codable` instance such as ``XPCServerEndpoint`` or ``XPCFileDescriptorContainer`` which are only capable of being
/// encoded or decoded by the XPC coders, not an arbitrary coder.
///
/// This error is intentionally internal to the framework as we don't want API users to be trying to explicitly handle this specific case.
enum XPCCoderError: Error {
case onlyDecodableBySecureXPCFramework
case onlyEncodableBySecureXPCFramework
}

/// Determines the `SecCode` corresponding to an XPC connection and/or message.
///
/// Uses undocumented functionality prior to macOS 11.
Expand Down
162 changes: 162 additions & 0 deletions Sources/SecureXPC/XPCFileDescriptorContainer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
//
// XPCFileDescriptorContainer.swift
// SecureXPC
//
// Created by Josh Kaplan on 2022-04-12
//

import Foundation
import System

/// A container for a file descriptor which can be sent over an XPC connection.
///
/// Any initializer may be used in combination with any `duplicate` function. For example, it is valid to create a container using
/// ``init(descriptor:closeDescriptor:)-5b3wo``, send it over an XPC connection, and then on the receiving side call
/// ``duplicateAsFileHandle()``.
///
/// > Warning: While ``XPCFileDescriptorContainer`` conforms to `Codable` it can only be encoded and decoded by the `SecureXPC` framework.
///
/// ## Topics
/// ### Creating a Container
/// - ``init(descriptor:closeDescriptor:)-5b3wo``
/// - ``init(handle:closeHandle:)``
/// - ``init(descriptor:closeDescriptor:)-8kiiv``
///
/// ### Duplicating
/// - ``duplicateAsNativeDescriptor()``
/// - ``duplicateAsFileHandle()``
/// - ``duplicateAsFileDescriptor()``
public struct XPCFileDescriptorContainer {

/// Errors related to boxing or duplicating file descriptors.
public enum XPCFileDescriptorContainerError: Error {
/// The provided value is not a valid file descriptor, including because it has already been closed.
case invalidFileDescriptor
}

private let xpcEncodedForm: xpc_object_t

/// Boxes the provided native file descriptor.
///
/// - Parameters:
/// - descriptor: The descriptor to be boxed.
/// - closeDescriptor: If set to `true`, `descriptor` will be closed if this initializer succesfully completes.
public init(descriptor: Int32, closeDescriptor: Bool) throws {
guard let xpcEncodedForm = xpc_fd_create(descriptor) else {
throw XPCFileDescriptorContainerError.invalidFileDescriptor
}
self.xpcEncodedForm = xpcEncodedForm

if closeDescriptor {
close(descriptor)
}
}

/// Boxes the provided [`FileHandle`](https://developer.apple.com/documentation/foundation/filehandle).
///
/// - Parameters:
/// - handle: The file handle to be boxed.
/// - closeHandle: If set to `true`, `handler` will be closed if this initializer succesfully completes.
public init(handle: FileHandle, closeHandle: Bool) throws {
guard let xpcEncodedForm = xpc_fd_create(handle.fileDescriptor) else {
throw XPCFileDescriptorContainerError.invalidFileDescriptor
}
self.xpcEncodedForm = xpcEncodedForm

if closeHandle {
if #available(macOS 10.15, *) {
try handle.close()
} else {
handle.closeFile()
}
}
}

/// Boxes the provided [`FileDescriptor`](https://developer.apple.com/documentation/system/filedescriptor).
///
/// - Parameters:
/// - descriptor: The descriptor to be boxed.
/// - closeDescriptor: If set to `true`, `descriptor` will be closed if this initializer succesfully completes.
@available(macOS 11.0, *)
public init(descriptor: FileDescriptor, closeDescriptor: Bool) throws {
guard let xpcEncodedForm = xpc_fd_create(descriptor.rawValue) else {
throw XPCFileDescriptorContainerError.invalidFileDescriptor
}
self.xpcEncodedForm = xpcEncodedForm

if closeDescriptor {
try descriptor.close()
}
}

/// Returns a file descriptor equivalent to the one originally boxed.
///
/// This function may be called multiple times, but each time it will return a different file descriptor. The returned descriptors will be equivalent, as though they
/// had been created by
/// [dup(2)](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/dup2.2.html).
///
/// > Important: The caller is responsible for calling
/// [close(2)](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/close.2.html#//apple_ref/doc/man/2/close)
/// on the returned descriptor.
public func duplicateAsNativeDescriptor() throws -> Int32 {
let fd = xpc_fd_dup(self.xpcEncodedForm)
// From xpc_fd_dup documentation:
// If the descriptor could not be created or if the given object was not an XPC file descriptor, -1 is
// returned.
if fd == -1 {
throw XPCFileDescriptorContainerError.invalidFileDescriptor
}

return fd
}

/// Returns a [`FileHandle`](https://developer.apple.com/documentation/foundation/filehandle) equivalent to the one originally boxed.
///
/// This function may be called multiple times, but each time it will return a different file handle that is equivalent.
///
/// > Important: The caller is responsible for calling
/// [`close()`](https://developer.apple.com/documentation/foundation/filehandle/3172525-close) or
/// [`closeFile()`](https://developer.apple.com/documentation/foundation/filehandle/1413393-closefile).
public func duplicateAsFileHandle() throws -> FileHandle {
FileHandle(fileDescriptor: try duplicateAsNativeDescriptor())
}

/// Returns a [`FileDescriptor`](https://developer.apple.com/documentation/system/filedescriptor) equivalent to the one originally
/// boxed.
///
/// This function may be called multiple times, but each time it will return a different file descriptor. The returned descriptors will be equivalent, as though they
/// had been created by
/// [`duplicate(as:retryoninterrupt:)`](https://developer.apple.com/documentation/system/filedescriptor/duplicate(as:retryoninterrupt:)).
///
/// > Important: The caller is responsible for calling
/// [`close()`](https://developer.apple.com/documentation/system/filedescriptor/close()) or
/// [`closeAfter(_:)`](https://developer.apple.com/documentation/system/filedescriptor/closeafter(_:)) on the returned descriptor.
@available(macOS 11.0, *)
public func duplicateAsFileDescriptor() throws -> FileDescriptor {
FileDescriptor(rawValue: try duplicateAsNativeDescriptor())
}
}

// MARK: Codable

extension XPCFileDescriptorContainer: Encodable {
public func encode(to encoder: Encoder) throws {
guard let xpcEncoder = encoder as? XPCEncoderImpl else {
throw XPCCoderError.onlyEncodableBySecureXPCFramework
}

let container = xpcEncoder.xpcSingleValueContainer()
container.setAlreadyEncodedValue(self.xpcEncodedForm)
}
}

extension XPCFileDescriptorContainer: Decodable {
public init(from decoder: Decoder) throws {
guard let xpcDecoder = decoder as? XPCDecoderImpl else {
throw XPCCoderError.onlyDecodableBySecureXPCFramework
}

let container = xpcDecoder.xpcSingleValueContainer()
self.xpcEncodedForm = try container.accessAsEncodedValue(xpcType: XPC_TYPE_FD)
}
}
9 changes: 2 additions & 7 deletions Sources/SecureXPC/XPCServerEndpoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ private enum CodingKeys: String, CodingKey {
extension XPCServerEndpoint: Encodable {
public func encode(to encoder: Encoder) throws {
guard let xpcEncoder = encoder as? XPCEncoderImpl else {
throw XPCServerEndpointError.onlyEncodableBySecureXPCFramework
throw XPCCoderError.onlyEncodableBySecureXPCFramework
}

let container = xpcEncoder.xpcContainer(keyedBy: CodingKeys.self)
Expand All @@ -60,16 +60,11 @@ extension XPCServerEndpoint: Encodable {
extension XPCServerEndpoint: Decodable {
public init(from decoder: Decoder) throws {
guard let xpcDecoder = decoder as? XPCDecoderImpl else {
throw XPCServerEndpointError.onlyDecodableBySecureXPCFramework
throw XPCCoderError.onlyDecodableBySecureXPCFramework
}

let container = try xpcDecoder.xpcContainer(keyedBy: CodingKeys.self)
self.endpoint = try container.decodeEndpoint(forKey: CodingKeys.endpoint)
self.serviceDescriptor = try container.decode(XPCServiceDescriptor.self, forKey: CodingKeys.serviceDescriptor)
}
}

private enum XPCServerEndpointError: Error {
case onlyDecodableBySecureXPCFramework
case onlyEncodableBySecureXPCFramework
}
Loading

0 comments on commit f193c0d

Please sign in to comment.