177 lines
4.9 KiB
Swift
177 lines
4.9 KiB
Swift
import Foundation
|
|
import Security
|
|
|
|
struct PairingDescriptor: Equatable {
|
|
let host: String
|
|
let port: Int
|
|
let pairingID: String
|
|
let secret: Data
|
|
|
|
init(url: URL) throws {
|
|
guard url.scheme == "hololake", url.host == "pair",
|
|
let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
|
|
throw HoloLakeMobileError.invalidPairingLink
|
|
}
|
|
var values: [String: String] = [:]
|
|
for item in components.queryItems ?? [] {
|
|
guard let value = item.value, values[item.name] == nil else {
|
|
throw HoloLakeMobileError.invalidPairingLink
|
|
}
|
|
values[item.name] = value
|
|
}
|
|
guard let host = values["host"], !host.isEmpty,
|
|
let portText = values["port"], let port = Int(portText), (1...65535).contains(port),
|
|
let pairingID = values["id"], UUID(uuidString: pairingID) != nil,
|
|
let secretText = values["secret"],
|
|
let secret = Data(base64URL: secretText), secret.count == 32 else {
|
|
throw HoloLakeMobileError.invalidPairingLink
|
|
}
|
|
self.host = host
|
|
self.port = port
|
|
self.pairingID = pairingID
|
|
self.secret = secret
|
|
}
|
|
|
|
var baseURL: URL {
|
|
URL(string: "http://\(host):\(port)")!
|
|
}
|
|
}
|
|
|
|
struct StoredSession: Codable, Equatable {
|
|
let host: String
|
|
let port: Int
|
|
let deviceID: String
|
|
let sessionKey: Data
|
|
let desktopName: String
|
|
var counter: UInt64
|
|
var cursor: UInt64
|
|
|
|
var baseURL: URL {
|
|
URL(string: "http://\(host):\(port)")!
|
|
}
|
|
}
|
|
|
|
struct PairRequest: Codable {
|
|
let deviceName: String
|
|
let platform: String
|
|
let requestID: String
|
|
}
|
|
|
|
struct PairResponse: Codable {
|
|
let schema: String
|
|
let state: String
|
|
let deviceID: String
|
|
let sessionKey: String
|
|
let desktopName: String
|
|
let rootNodeRole: String
|
|
let requestID: String
|
|
}
|
|
|
|
struct SyncRequest: Codable {
|
|
let counter: UInt64
|
|
let afterCursor: UInt64?
|
|
let capture: MobileCaptureInput?
|
|
}
|
|
|
|
struct MobileCaptureInput: Codable {
|
|
let title: String
|
|
let body: String
|
|
let requestID: String
|
|
}
|
|
|
|
struct MobileSyncSnapshot: Codable {
|
|
let schema: String
|
|
let state: String
|
|
let cursor: UInt64
|
|
let generatedAtUnixMs: UInt64
|
|
let desktopName: String
|
|
let rootNodeOnline: Bool
|
|
let personalChannel: MobileChannelProjection
|
|
let webNovel: MobileWebNovelProjection
|
|
let education: MobileEducationProjection
|
|
let recentCaptures: [MobileCaptureProjection]
|
|
let boundary: MobileBoundaryProjection
|
|
}
|
|
|
|
struct MobileChannelProjection: Codable {
|
|
let home: String
|
|
let growthEventCount: UInt64
|
|
let integrity: String
|
|
}
|
|
|
|
struct MobileWebNovelProjection: Codable {
|
|
let workCount: UInt64
|
|
let volumeCount: UInt64
|
|
let chapterCount: UInt64
|
|
let works: [MobileWebNovelWork]
|
|
}
|
|
|
|
struct MobileWebNovelWork: Codable, Identifiable {
|
|
let workID: String
|
|
let title: String
|
|
let status: String
|
|
let updatedAtUnixMs: UInt64
|
|
var id: String { workID }
|
|
}
|
|
|
|
struct MobileEducationProjection: Codable {
|
|
let activeTableCount: UInt64
|
|
let archivedTableCount: UInt64
|
|
let unassignedTableCount: UInt64
|
|
let sensitiveValuesIncluded: Bool
|
|
}
|
|
|
|
struct MobileCaptureProjection: Codable, Identifiable {
|
|
let cursor: UInt64
|
|
let captureID: String
|
|
let title: String
|
|
let body: String
|
|
let sourceDeviceID: String
|
|
let createdAtUnixMs: UInt64
|
|
var id: String { captureID }
|
|
}
|
|
|
|
struct MobileBoundaryProjection: Codable {
|
|
let mobileRole: String
|
|
let remoteDesktopClone: Bool
|
|
let desktopOfflineExecution: Bool
|
|
let sensitiveEducationValues: String
|
|
let modelAPI: String
|
|
}
|
|
|
|
enum HoloLakeMobileError: LocalizedError {
|
|
case invalidPairingLink
|
|
case invalidResponse
|
|
case rejected(String)
|
|
case missingNonce
|
|
case missingSession
|
|
case keychain(OSStatus)
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .invalidPairingLink: return "配对链接无效或已损坏。"
|
|
case .invalidResponse: return "电脑端返回了无法校验的响应。"
|
|
case .rejected(let code): return "电脑端拒绝了本次请求:\(code)"
|
|
case .missingNonce: return "加密响应缺少一次性随机数。"
|
|
case .missingSession: return "请先与电脑端 HoloLake 配对。"
|
|
case .keychain(let status): return "本机钥匙串写入失败(\(status))。"
|
|
}
|
|
}
|
|
}
|
|
|
|
extension Data {
|
|
init?(base64URL value: String) {
|
|
var text = value.replacingOccurrences(of: "-", with: "+")
|
|
.replacingOccurrences(of: "_", with: "/")
|
|
text += String(repeating: "=", count: (4 - text.count % 4) % 4)
|
|
guard let data = Data(base64Encoded: text) else { return nil }
|
|
self = data
|
|
}
|
|
|
|
var base64URL: String {
|
|
base64EncodedString()
|
|
.replacingOccurrences(of: "+", with: "-")
|
|
.replacingOccurrences(of: "/", with: "_")
|
|
.replacingOccurrences(of: "=", with: "")
|
|
}
|
|
}
|