71 lines
2.8 KiB
Swift
71 lines
2.8 KiB
Swift
import CloudKit
|
|
import CryptoKit
|
|
import Foundation
|
|
|
|
/// CloudKit transport mechanics shared by the independently owned sync coordinators.
|
|
nonisolated enum CloudSyncRecordCoding {
|
|
static let inlinePayloadLimit = 700_000
|
|
|
|
static func makeRecord(type: String, id: CKRecord.ID, systemFields: Data?) throws -> CKRecord {
|
|
if let systemFields {
|
|
let unarchiver = try NSKeyedUnarchiver(forReadingFrom: systemFields)
|
|
unarchiver.requiresSecureCoding = true
|
|
defer { unarchiver.finishDecoding() }
|
|
if let restored = CKRecord(coder: unarchiver) { return restored }
|
|
}
|
|
return CKRecord(recordType: type, recordID: id)
|
|
}
|
|
|
|
static func setPayload(
|
|
_ payload: Data, on record: CKRecord, assetURL: @autoclosure () throws -> URL
|
|
) throws {
|
|
record["payloadHash"] = hash(payload) as NSString
|
|
if payload.count > inlinePayloadLimit {
|
|
let url = try assetURL()
|
|
try payload.write(to: url, options: .atomic)
|
|
record["payload"] = nil
|
|
record["payloadAsset"] = CKAsset(fileURL: url)
|
|
} else {
|
|
record["payload"] = payload as NSData
|
|
record["payloadAsset"] = nil
|
|
}
|
|
}
|
|
|
|
static func validatedPayloadData(
|
|
from record: CKRecord, invalidEnvelope: @autoclosure () -> any Error
|
|
) throws -> Data {
|
|
let data = try payloadData(from: record, invalidEnvelope: invalidEnvelope())
|
|
guard record["payloadHash"] as? String == hash(data) else { throw invalidEnvelope() }
|
|
return data
|
|
}
|
|
|
|
/// Raw bytes remain available to quarantine even when the payload hash is invalid.
|
|
static func payloadData(
|
|
from record: CKRecord, invalidEnvelope: @autoclosure () -> any Error
|
|
) throws -> Data {
|
|
if let inline = record["payload"] as? Data { return inline }
|
|
if let asset = record["payloadAsset"] as? CKAsset, let url = asset.fileURL {
|
|
return try Data(contentsOf: url)
|
|
}
|
|
throw invalidEnvelope()
|
|
}
|
|
|
|
static func systemFields(of record: CKRecord) throws -> Data {
|
|
let archiver = NSKeyedArchiver(requiringSecureCoding: true)
|
|
record.encodeSystemFields(with: archiver)
|
|
archiver.finishEncoding()
|
|
return archiver.encodedData
|
|
}
|
|
|
|
static func assetURL(recordName: String, directoryName: String) throws -> URL {
|
|
let directory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
|
.appending(path: directoryName, directoryHint: .isDirectory)
|
|
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
|
return directory.appending(path: recordName.replacingOccurrences(of: "/", with: "-"))
|
|
}
|
|
|
|
private static func hash(_ data: Data) -> String {
|
|
SHA256.hash(data: data).map { String(format: "%02x", $0) }.joined()
|
|
}
|
|
}
|