// The native sink's C interface (wai/extensions/native-sink.md §9), wrapped for Swift: one // `WaiSink` is one playback session. Every call passes JSON across the boundary as the // extension's tables list it, and every answer comes back as the JSON the library wrote. import Foundation import WaiFFI /// An error the library returned: its status and `wai_last_error`'s message, which starts with /// the reason for an input that does not read (`native_input_invalid`, …). public struct WaiSinkError: Error, CustomStringConvertible, Equatable { public let status: Int32 public let message: String /// The reason code the message starts with. public var reason: String { String(message.split(separator: ":", maxSplits: 1).first ?? "") } public var description: String { "wai_sink status \(status): \(message)" } } /// One session of the native sink (native-sink §3). public final class WaiSink: @unchecked Sendable { /// What a session requires of what it is given (native-sink §3.1). public struct Config: Equatable { public var channel: String public var owner: String public var presentation: String? public var windowUs: UInt64? public var adClasses: [String] public var contentAddressed: Bool public var requireSigned: Bool public var unseenGraceMs: UInt64? public var idleMs: UInt64? public init(channel: String, owner: String, presentation: String? = nil, windowUs: UInt64? = nil, adClasses: [String] = [], contentAddressed: Bool = false, requireSigned: Bool = false, unseenGraceMs: UInt64? = nil, idleMs: UInt64? = nil) { self.channel = channel self.owner = owner self.presentation = presentation self.windowUs = windowUs self.adClasses = adClasses self.contentAddressed = contentAddressed self.requireSigned = requireSigned self.unseenGraceMs = unseenGraceMs self.idleMs = idleMs } /// The configuration JSON of §3.1. public var json: [String: Any] { var o: [String: Any] = ["channel": channel, "owner": owner, "ad_classes": adClasses, "content_addressed": contentAddressed, "require": requireSigned ? "signed" : "nothing"] if let p = presentation { o["presentation"] = p } if let w = windowUs { o["window_us"] = w } if let g = unseenGraceMs { o["unseen_grace_ms"] = g } if let i = idleMs { o["idle_ms"] = i } return o } } /// How a session ends (stream-audit §8.2). public enum Ended: String, Sendable { case ended, user, hidden, idle, error } private let handle: OpaquePointer /// The ABI version the linked library implements. public static var abiVersion: UInt32 { wai_sink_abi_version() } /// The host's clock, in microseconds since 1970. public static var nowUs: UInt64 { wai_sink_now_us() } /// Open a session from a configuration JSON object. `seed` is for conformance vectors and /// tests only: a viewer's session takes a fresh key (`seed` nil). public init(configJSON: [String: Any], seed: Data? = nil, startUs: UInt64 = WaiSink.nowUs) throws { guard WaiSink.abiVersion == UInt32(WAI_SINK_ABI_VERSION) else { throw WaiSinkError(status: -1, message: "native_abi_mismatch: the linked library implements ABI \(WaiSink.abiVersion)") } let cfg = try WaiSink.text(configJSON) var out: OpaquePointer? let st: Int32 = cfg.withCString { c in if let seed { return seed.withUnsafeBytes { p in wai_sink_open(c, p.bindMemory(to: UInt8.self).baseAddress, UInt(seed.count), startUs, &out) } } return wai_sink_open(c, nil, 0, startUs, &out) } guard st == 0, let h = out else { throw WaiSink.error(st) } handle = h } public convenience init(config: Config, seed: Data? = nil, startUs: UInt64 = WaiSink.nowUs) throws { try self.init(configJSON: config.json, seed: seed, startUs: startUs) } deinit { wai_sink_free(handle) } // MARK: calls /// The session's identity: `channel`, `owner`, `player`, `session`, `start_us`, /// `presentation`, `ended`. public func info() throws -> [String: Any] { try WaiSink.object(try call { wai_sink_info(handle, &$0) }) } /// The receipt file to fetch beside a playlist, relative to it: `mr/.json`. public static func receiptName(_ playlist: Data) -> String { var out = wai_buffer_t(data: nil, len: 0, cap: 0) let st = playlist.withUnsafeBytes { p in wai_sink_receipt_name(p.bindMemory(to: UInt8.self).baseAddress, UInt(playlist.count), &out) } defer { wai_buffer_free(out) } guard st == 0 else { return "" } return String(decoding: WaiSink.bytes(out), as: UTF8.self) } /// A playlist the player fetched, with its receipt where one was served (§4.2). public func playlist(url: String, bytes: Data, receipt: Data?) throws -> [String: Any] { try WaiSink.object(try call { out in url.withCString { u in bytes.withUnsafeBytes { b in let bp = b.bindMemory(to: UInt8.self).baseAddress if let receipt { return receipt.withUnsafeBytes { r in wai_sink_playlist(handle, u, bp, UInt(bytes.count), r.bindMemory(to: UInt8.self).baseAddress ?? UnsafePointer(bitPattern: 1), UInt(receipt.count), &out) } } return wai_sink_playlist(handle, u, bp, UInt(bytes.count), nil, 0, &out) } } }) } /// The parent receipt a verdict named in `need_parent`, or nil when it was not served. public func playlistParent(url: String, name: String, bytes: Data?) throws { let st: Int32 = url.withCString { u in name.withCString { n in if let bytes { return bytes.withUnsafeBytes { b in wai_sink_playlist_parent(handle, u, n, b.bindMemory(to: UInt8.self).baseAddress ?? UnsafePointer(bitPattern: 1), UInt(bytes.count)) } } return wai_sink_playlist_parent(handle, u, n, nil, 0) } } guard st == 0 else { throw WaiSink.error(st) } } /// A response body the player received, its content codings removed (§4.3). public func object(meta: [String: Any], bytes: Data) throws -> [String: Any] { let m = try WaiSink.text(meta) return try WaiSink.object(try call { out in m.withCString { c in bytes.withUnsafeBytes { b in wai_sink_object(handle, c, b.bindMemory(to: UInt8.self).baseAddress, UInt(bytes.count), &out) } } }) } /// A log event (§5.2). public func event(_ name: String, atUs: UInt64) throws { let st = name.withCString { wai_sink_event(handle, $0, atUs) } guard st == 0 else { throw WaiSink.error(st) } } /// An observation (§5.1): the ending the termination rule gives, if any (§6.1). @discardableResult public func observe(_ obs: [String: Any]) throws -> Ended? { let o = try WaiSink.text(obs) let v = try WaiSink.object(try call { out in o.withCString { wai_sink_observe(handle, $0, &out) } }) return (v["end"] as? String).flatMap(Ended.init(rawValue:)) } /// A role's energy entry (§7). public func energy(_ entry: [String: Any]) throws { let e = try WaiSink.text(entry) let st = e.withCString { wai_sink_energy(handle, $0) } guard st == 0 else { throw WaiSink.error(st) } } /// Attach another signed record of the session (stream-audit §8.3). public func attach(name: String, bytes: Data) throws { let st = name.withCString { n in bytes.withUnsafeBytes { wai_sink_attach(handle, n, $0.bindMemory(to: UInt8.self).baseAddress, UInt(bytes.count)) } } guard st == 0 else { throw WaiSink.error(st) } } /// The Event-mode CMCD batch of the log since the last call. public func takeCMCD() throws -> String { String(decoding: try call { wai_sink_take_cmcd(handle, &$0) }, as: UTF8.self) } /// End the session: the bundle (stream-audit §9), the JSON a host posts to the collector. public func end(_ ended: Ended, atUs: UInt64) throws -> Data { try call { out in ended.rawValue.withCString { wai_sink_end(handle, $0, atUs, &out) } } } /// Verify a bundle as the owner's collector does (stream-audit §10): the outcome JSON. public static func verifyBundle(_ bundle: Data, owner: Data, channels: [String]) throws -> [String: Any] { precondition(owner.count == 32, "an owner key is 32 bytes") let ch = try text(channels) var out = wai_buffer_t(data: nil, len: 0, cap: 0) let st: Int32 = ch.withCString { c in bundle.withUnsafeBytes { b in owner.withUnsafeBytes { o in wai_audit_verify_bundle(b.bindMemory(to: UInt8.self).baseAddress, UInt(bundle.count), o.bindMemory(to: UInt8.self).baseAddress, c, &out) } } } defer { wai_buffer_free(out) } guard st == 0 else { throw error(st) } return try object(bytes(out)) } // MARK: plumbing private func call(_ f: (inout wai_buffer_t) -> Int32) throws -> Data { var out = wai_buffer_t(data: nil, len: 0, cap: 0) let st = f(&out) defer { wai_buffer_free(out) } guard st == 0 else { throw WaiSink.error(st) } return WaiSink.bytes(out) } static func bytes(_ b: wai_buffer_t) -> Data { guard let p = b.data, b.len > 0 else { return Data() } return Data(bytes: p, count: Int(b.len)) } static func error(_ st: Int32) -> WaiSinkError { WaiSinkError(status: st, message: wai_last_error().map { String(cString: $0) } ?? "") } static func text(_ v: Any) throws -> String { String(decoding: try JSONSerialization.data(withJSONObject: v, options: [.fragmentsAllowed]), as: UTF8.self) } static func object(_ d: Data) throws -> [String: Any] { guard let o = try JSONSerialization.jsonObject(with: d, options: [.fragmentsAllowed]) as? [String: Any] else { throw WaiSinkError(status: -1, message: "native_answer_unreadable") } return o } }