Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 1 addition & 47 deletions Sources/SecureXPC/Codable Types/ArrayOptimizedForXPC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,57 +7,11 @@

import Foundation

/// Wraps an array to optimize how it is sent over an XPC connection.
/// Utilities for easier array mapping to data and vice versa
///
/// Arrays of the following type are automatically supported by this property wrapper: `Bool`, `Double`, `Float`, `UInt`, `UInt8`, `UInt16`, `UInt32`,
/// `UInt64`, `Int`, `Int8`, `Int16`, `Int32`, and `Int64`. You may add support for your own trivial types by having them conform to ``Trivial``.
///
/// Usage of this property wrapper is never required and has no benefit when the array is either the message or reply type for an ``XPCRoute``. When transferring
/// a type which _contains_ an array property it is more efficient both in runtime and memory usage to wrap it using this property wrapper.
@propertyWrapper public struct ArrayOptimizedForXPC<Element: Trivial & Codable> {
// Note: There's no actual need for Element to conform to Codable, but doing so provides consistency between arrays
// which are wrapped with this property wrapper vs those that are not. Also it's easy for most trivial types to
// become Codable conforming simplying by declaring conformance; the compiler will autogenerate the implementation.

public var wrappedValue: [Element]

public init(wrappedValue: [Element]) {
self.wrappedValue = wrappedValue
}
}

// MARK: Codable

extension ArrayOptimizedForXPC: Encodable {
public func encode(to encoder: Encoder) throws {
let xpcEncoder = try XPCEncoderImpl.asXPCEncoderImpl(encoder)
guard let data = encodeArrayAsData(value: self.wrappedValue) else {
let debugDescription = "Unable to encode \(self.wrappedValue.self) to XPC data represetation"
let context = EncodingError.Context(codingPath: encoder.codingPath,
debugDescription: debugDescription,
underlyingError: nil)
throw EncodingError.invalidValue(self.wrappedValue, context)
}

xpcEncoder.xpcSingleValueContainer().setAlreadyEncodedValue(data)
}
}

extension ArrayOptimizedForXPC: Decodable {
public init(from decoder: Decoder) throws {
let xpcDecoder = try XPCDecoderImpl.asXPCDecoderImpl(decoder)
let container = xpcDecoder.xpcSingleValueContainer()
guard let array = decodeDataAsArray(arrayType: [Element].self, arrayAsData: container.value) else {
let debugDescription = "Unable to decode \(container.value.description) to an array of type \(Element.self)"
let context = DecodingError.Context(codingPath: container.codingPath,
debugDescription: debugDescription,
underlyingError: nil)
throw DecodingError.dataCorrupted(context)
}

self.wrappedValue = array
}
}

// MARK: Helper functions

Expand Down
49 changes: 0 additions & 49 deletions Sources/SecureXPC/Codable Types/DataOptimizedForXPC.swift

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// Array+XPCRawCodable.swift
// SecureXPC
//
// Created by Robert Fogash on 27.02.2026.
//

import Foundation

extension Array: XPCRawEncodable where Array.Element: Trivial {

func xpcRawValue() -> xpc_object_t? {
self.withUnsafePointer {
xpc_data_create($0, self.elementCount * type(of: self).elementStride)
}
}
}

extension Array: XPCRawDecodable where Array.Element: Trivial {

init?(xpcRawValue: xpc_object_t) {
guard xpc_get_type(xpcRawValue) == XPC_TYPE_DATA,
let dataPointer = xpc_data_get_bytes_ptr(xpcRawValue)
else {
return nil
}
self = Array(pointer: dataPointer, count: xpc_data_get_length(xpcRawValue) / MemoryLayout<Element>.size)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// File.swift
// SecureXPC
//
// Created by Robert Fogash on 27.02.2026.
//

import Foundation

extension Data: XPCRawEncodable {

func xpcRawValue() -> xpc_object_t? {
withUnsafeBytes { (buffer: UnsafeRawBufferPointer) -> xpc_object_t? in
guard let baseAddress = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self)
else {
return nil
}
return xpc_data_create(baseAddress, buffer.count)
}
}
}

extension Data: XPCRawDecodable {

init?(xpcRawValue: xpc_object_t) {
guard xpc_get_type(xpcRawValue) == XPC_TYPE_DATA,
let dataPointer = xpc_data_get_bytes_ptr(xpcRawValue)
else {
return nil
}
self.init(bytes: dataPointer, count: xpc_data_get_length(xpcRawValue))
}
}
20 changes: 20 additions & 0 deletions Sources/SecureXPC/Codable Types/XPCRawCodable/XPCRawCodable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// File.swift
// SecureXPC
//
// Created by Robert Fogash on 27.02.2026.
//

import Foundation

protocol XPCRawEncodable {

func xpcRawValue() -> xpc_object_t?
}

protocol XPCRawDecodable {

init?(xpcRawValue: xpc_object_t)
}
Comment thread
alek-prykhodko marked this conversation as resolved.

typealias XPCRawCodable = XPCRawDecodable & XPCRawEncodable
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,20 @@ internal class XPCKeyedDecodingContainer<K: CodingKey>: KeyedDecodingContainerPr
}

func decode<T>(_ type: T.Type, forKey key: K) throws -> T where T : Decodable {
return try type.init(from: XPCDecoderImpl(value: value(forKey: key),
codingPath: self.codingPath + [key],
userInfo: self.userInfo))
if let castedType = type as? XPCRawDecodable.Type {
let value = try value(forKey: key)
guard let decodedType = castedType.init(xpcRawValue: value) as? T else {
let context = DecodingError.Context(codingPath: codingPath,
debugDescription: "Unable to decode array",
underlyingError: nil)
throw DecodingError.dataCorrupted(context)
}
return decodedType
} else {
return try type.init(from: XPCDecoderImpl(value: value(forKey: key),
codingPath: self.codingPath + [key],
userInfo: self.userInfo))
}
Comment thread
alek-prykhodko marked this conversation as resolved.
}

func nestedContainer<NestedKey>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,18 @@ private class XPCArrayBackedUnkeyedDecodingContainer: UnkeyedDecodingContainer {
}

func decode<T>(_ type: T.Type) throws -> T where T : Decodable {
if let castedType = type as? XPCRawDecodable.Type {
let xpcObject = try nextElement(type)
guard let nextElement = castedType.init(xpcRawValue: xpcObject) else {
let context = DecodingError.Context(codingPath: codingPath,
debugDescription: "Unable to decode array",
underlyingError: nil)
throw DecodingError.dataCorrupted(context)
}
currentIndex += 1

return nextElement as! T
}
Comment thread
alek-prykhodko marked this conversation as resolved.
let decodedElement = try T(from: XPCDecoderImpl(value: try nextElement(type),
codingPath: self.codingPath,
userInfo: self.userInfo))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,33 @@ internal class XPCKeyedEncodingContainer<K>: KeyedEncodingContainerProtocol, XPC
self.setValue(xpc_uint64_create(value), forKey: key)
}

func encode<T>(_ value: T, forKey key: K) throws where T : Encodable {
let encoder = XPCEncoderImpl(codingPath: self.codingPath + [key])
self.setValue(encoder, forKey: key)
func encode(_ value: XPCRawEncodable, forKey key: K) throws {
guard let encodedValue = value.xpcRawValue() else {
let debugDescription = "Unable to encode \(self.self) to XPC data representation"
let context = EncodingError.Context(codingPath: codingPath,
debugDescription: debugDescription,
underlyingError: nil)
throw EncodingError.invalidValue(self, context)
}
self.setValue(encodedValue, forKey: key)
}
Comment thread
alek-prykhodko marked this conversation as resolved.

try value.encode(to: encoder)
func encode<T>(_ value: T, forKey key: K) throws where T : Encodable {
if let castedType = value as? XPCRawEncodable {
guard let encodedValue = castedType.xpcRawValue() else {
let debugDescription = "Unable to encode \(self.self) to XPC data representation"
let context = EncodingError.Context(codingPath: codingPath,
debugDescription: debugDescription,
underlyingError: nil)
throw EncodingError.invalidValue(self, context)
}
self.setValue(encodedValue, forKey: key)
} else {
let encoder = XPCEncoderImpl(codingPath: self.codingPath + [key])
self.setValue(encoder, forKey: key)

try value.encode(to: encoder)
}
}
Comment thread
alek-prykhodko marked this conversation as resolved.

func nestedContainer<NestedKey>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,21 @@ internal class XPCSingleValueEncodingContainer: SingleValueEncodingContainer, XP
}

func encode<T: Encodable>(_ value: T) throws {
let encoder = XPCEncoderImpl(codingPath: self.codingPath)
self.setValue(encoder)

try value.encode(to: encoder)
if let castedValue = value as? XPCRawEncodable {
guard let encodedValue = castedValue.xpcRawValue() else {
let debugDescription = "Unable to encode \(self.self) to XPC data representation"
let context = EncodingError.Context(codingPath: codingPath,
debugDescription: debugDescription,
underlyingError: nil)
throw EncodingError.invalidValue(self, context)
}
self.setValue(encodedValue)
} else {
let encoder = XPCEncoderImpl(codingPath: self.codingPath)
self.setValue(encoder)

try value.encode(to: encoder)
}
}

// MARK: XPC specific encoding
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,11 +172,24 @@ internal class XPCUnkeyedEncodingContainer : UnkeyedEncodingContainer, XPCContai
}

func encode<T: Encodable>(_ value: T) throws {
self.attemptDataBackedAppend(value)

let encoder = XPCEncoderImpl(codingPath: self.codingPath)
self.append(encoder)
try value.encode(to: encoder)
if let castedValue = value as? XPCRawEncodable {
self.attemptDataBackedAppend(castedValue)
guard let encodedValue = castedValue.xpcRawValue() else {
let debugDescription = "Unable to encode \(self.self) to XPC data representation"
let context = EncodingError.Context(codingPath: codingPath,
debugDescription: debugDescription,
underlyingError: nil)
throw EncodingError.invalidValue(self, context)
}
self.append(encodedValue)
return
} else {
self.attemptDataBackedAppend(value)

let encoder = XPCEncoderImpl(codingPath: self.codingPath)
self.append(encoder)
try value.encode(to: encoder)
}
}

func nestedContainer<NestedKey>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,23 @@ import XCTest

final class XPCArrayRoundTripTests: XCTestCase {

static var bigArray: [UInt8]!
static var bigArrayCount = 2 * 1024 * 1024

var bigArray: [UInt8] { Self.bigArray }

override class func setUp() {
super.setUp()

bigArray = (0..<bigArrayCount).map { _ in UInt8.random(in: 0...UInt8.max) }
}

override class func tearDown() {
super.tearDown()

bigArray = nil
}

// MARK: Empty array
func testRoundTrip_arrayOf_Nothing() throws {
try assertRoundTripEqual([Int]())
Expand Down Expand Up @@ -173,4 +190,30 @@ final class XPCArrayRoundTripTests: XCTestCase {
]
try assertRoundTripEqual(arrayOfDicts)
}

func test_bigArrayAsProperty() throws {

struct StructWithArrayAsProperty: Codable, Equatable {
let values: [UInt8]
let text: String
let number: Int
}

let structure = StructWithArrayAsProperty(values: bigArray, text: "STUB_TEXT", number: 35)

try assertRoundTripEqual(structure)
}

func test_bigArrayAsItemOfArray() throws {

struct StructWithArrayOfArrayAsProperty: Codable, Equatable {
let values: [[UInt8]]
let text: String
let number: Int
}

let structure = StructWithArrayOfArrayAsProperty(values: [bigArray], text: "STUB_TEXT", number: 35)

try assertRoundTripEqual(structure)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import SecureXPC
final class ArrayOptimizedForXPCTests: XCTestCase {
struct Info: Codable {
let description: String
@ArrayOptimizedForXPC var array: [Int]
var array: [Int]
}

func testRoundTrip() async throws {
Expand Down
Loading