From 701c93f45a89d327974181cfe7853f625394674c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 15 Feb 2026 18:34:13 +0000 Subject: [PATCH] feat(app-clip): add Swift App Clip skeleton for RVQS QR seed decoding Minimal iOS App Clip template that bridges into the RVF C FFI to decode QR cognitive seeds. Includes SPM package config linking librvf_runtime.a, a C header mirroring ffi.rs exports (rvqs_parse_header, rvqs_verify_signature, rvqs_verify_content_hash, rvqs_decompress_microkernel, rvqs_get_primary_host_url), a Swift SeedDecoder wrapper with proper memory management, and a SwiftUI view with QR scanner placeholder and decoded seed info display. https://claude.ai/code/session_01RnwD4x5cbpB7FPvoyYQz8G --- examples/app-clip/Package.swift | 42 +++ .../app-clip/Sources/AppClip/AppClipApp.swift | 73 ++++ .../Sources/AppClip/AppClipView.swift | 338 ++++++++++++++++++ .../Sources/AppClip/SeedDecoder.swift | 166 +++++++++ .../Sources/RVFBridge/module.modulemap | 5 + .../app-clip/Sources/RVFBridge/rvf_bridge.h | 172 +++++++++ 6 files changed, 796 insertions(+) create mode 100644 examples/app-clip/Package.swift create mode 100644 examples/app-clip/Sources/AppClip/AppClipApp.swift create mode 100644 examples/app-clip/Sources/AppClip/AppClipView.swift create mode 100644 examples/app-clip/Sources/AppClip/SeedDecoder.swift create mode 100644 examples/app-clip/Sources/RVFBridge/module.modulemap create mode 100644 examples/app-clip/Sources/RVFBridge/rvf_bridge.h diff --git a/examples/app-clip/Package.swift b/examples/app-clip/Package.swift new file mode 100644 index 000000000..b17d6157a --- /dev/null +++ b/examples/app-clip/Package.swift @@ -0,0 +1,42 @@ +// swift-tools-version: 5.9 +// Package.swift — SPM manifest for the RVF App Clip skeleton. +// +// This package links the pre-built RVF static library (librvf_runtime.a) +// produced by: +// cargo build --release --target aarch64-apple-ios --lib +// +// Place the compiled .a file under lib/ before building with Xcode. + +import PackageDescription + +let package = Package( + name: "RVFAppClip", + platforms: [ + .iOS(.v16), + ], + products: [ + .library( + name: "AppClip", + targets: ["AppClip"] + ), + ], + targets: [ + // C bridge module that exposes the RVF FFI header to Swift. + .target( + name: "RVFBridge", + path: "Sources/RVFBridge", + publicHeadersPath: ".", + linkerSettings: [ + // Link the pre-built Rust static library. + .unsafeFlags(["-L../../target/aarch64-apple-ios/release"]), + .linkedLibrary("rvf_runtime"), + ] + ), + // Swift App Clip target that consumes the C bridge. + .target( + name: "AppClip", + dependencies: ["RVFBridge"], + path: "Sources/AppClip" + ), + ] +) diff --git a/examples/app-clip/Sources/AppClip/AppClipApp.swift b/examples/app-clip/Sources/AppClip/AppClipApp.swift new file mode 100644 index 000000000..467a569e6 --- /dev/null +++ b/examples/app-clip/Sources/AppClip/AppClipApp.swift @@ -0,0 +1,73 @@ +// AppClipApp.swift — Entry point for the RVF App Clip. +// +// This is a minimal SwiftUI App Clip that scans QR cognitive seeds +// and decodes them using the RVF C FFI. Designed to stay under the +// 15 MB App Clip size limit per Apple guidelines. +// +// App Clip invocation URL scheme: +// https://rvf.example.com/seed?id= +// +// The App Clip can be invoked by: +// 1. Scanning an RVQS QR code directly (camera flow) +// 2. Tapping an App Clip Code / NFC tag +// 3. Opening a Smart App Banner link + +import SwiftUI + +@main +struct AppClipApp: App { + @StateObject private var appState = AppClipState() + + var body: some Scene { + WindowGroup { + AppClipView() + .onContinueUserActivity( + NSUserActivityTypeBrowsingWeb, + perform: handleUserActivity + ) + .environmentObject(appState) + } + } + + /// Handle App Clip invocation via URL. + /// + /// When the App Clip is launched from a Smart App Banner or App Clip Code, + /// iOS delivers the invocation URL as a user activity. We extract the + /// seed identifier and trigger a download + decode flow. + private func handleUserActivity(_ activity: NSUserActivity) { + guard let url = activity.webpageURL else { return } + appState.handleInvocationURL(url) + } +} + +// MARK: - AppClipState + +/// Shared state for App Clip lifecycle and invocation handling. +@MainActor +final class AppClipState: ObservableObject { + + /// The invocation URL that launched this App Clip (if any). + @Published var invocationURL: URL? + + /// Handle an App Clip invocation URL. + /// + /// Extracts the seed ID from the URL query parameters and could + /// trigger a network fetch for the seed payload. + func handleInvocationURL(_ url: URL) { + invocationURL = url + + // Extract seed ID from query parameters. + // Example: https://rvf.example.com/seed?id=0102030405060708 + guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false), + let seedIDParam = components.queryItems?.first(where: { $0.name == "id" }), + let _ = seedIDParam.value + else { + return + } + + // In production: + // 1. Fetch the seed payload from the CDN using the seed ID. + // 2. Pass the raw bytes to SeedDecoder.decode(data:). + // 3. Begin progressive download of the full RVF file. + } +} diff --git a/examples/app-clip/Sources/AppClip/AppClipView.swift b/examples/app-clip/Sources/AppClip/AppClipView.swift new file mode 100644 index 000000000..f50db0959 --- /dev/null +++ b/examples/app-clip/Sources/AppClip/AppClipView.swift @@ -0,0 +1,338 @@ +// AppClipView.swift — SwiftUI view for the RVF App Clip. +// +// Presents a QR scanner interface and displays decoded seed information. +// Uses AVFoundation for camera access and the SeedDecoder for parsing. + +import SwiftUI +import AVFoundation + +// MARK: - AppClipView + +/// Root view for the App Clip experience. +/// +/// Flow: Scan QR -> Decode RVQS seed -> Display cognitive seed info. +struct AppClipView: View { + @StateObject private var viewModel = AppClipViewModel() + + var body: some View { + NavigationStack { + VStack(spacing: 0) { + switch viewModel.state { + case .scanning: + scannerSection + case .decoding: + decodingSection + case .decoded(let info): + decodedSection(info) + case .error(let message): + errorSection(message) + } + } + .navigationTitle("RVF Seed Scanner") + .navigationBarTitleDisplayMode(.inline) + } + } + + // MARK: - Scanner + + private var scannerSection: some View { + VStack(spacing: 24) { + Spacer() + + // Camera preview placeholder. + // In production, this would be a UIViewRepresentable wrapping + // an AVCaptureVideoPreviewLayer for real-time QR scanning. + ZStack { + RoundedRectangle(cornerRadius: 16) + .fill(Color.black.opacity(0.8)) + .frame(width: 280, height: 280) + + RoundedRectangle(cornerRadius: 12) + .strokeBorder(Color.white.opacity(0.6), lineWidth: 2) + .frame(width: 240, height: 240) + + VStack(spacing: 12) { + Image(systemName: "qrcode.viewfinder") + .font(.system(size: 48)) + .foregroundStyle(.white) + Text("Point camera at QR seed") + .font(.subheadline) + .foregroundStyle(.white.opacity(0.8)) + } + } + + Text("Scan a cognitive seed QR code to bootstrap intelligence.") + .font(.footnote) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + .padding(.horizontal, 32) + + // Demo button for testing without a camera. + Button { + viewModel.decodeDemoSeed() + } label: { + Label("Use Demo Seed", systemImage: "doc.viewfinder") + .font(.body.weight(.medium)) + } + .buttonStyle(.borderedProminent) + .tint(.blue) + + Spacer() + } + } + + // MARK: - Decoding + + private var decodingSection: some View { + VStack(spacing: 16) { + Spacer() + ProgressView() + .scaleEffect(1.5) + Text("Decoding seed...") + .font(.headline) + .foregroundStyle(.secondary) + Spacer() + } + } + + // MARK: - Decoded Result + + private func decodedSection(_ info: SeedInfo) -> some View { + ScrollView { + VStack(alignment: .leading, spacing: 16) { + // Header card. + GroupBox { + VStack(alignment: .leading, spacing: 8) { + Label("Cognitive Seed", systemImage: "brain") + .font(.headline) + + Divider() + + infoRow("Version", value: "v\(info.version)") + infoRow("Dimension", value: "\(info.dimension)") + infoRow("Vectors", value: formatCount(info.totalVectorCount)) + infoRow("Seed Size", value: formatBytes(info.totalSeedSize)) + } + } + + // Content hash. + GroupBox { + VStack(alignment: .leading, spacing: 8) { + Label("Content Hash", systemImage: "number") + .font(.headline) + + Divider() + + Text(info.contentHash) + .font(.system(.caption, design: .monospaced)) + .foregroundStyle(.secondary) + .textSelection(.enabled) + } + } + + // Manifest info. + GroupBox { + VStack(alignment: .leading, spacing: 8) { + Label("Manifest", systemImage: "arrow.down.circle") + .font(.headline) + + Divider() + + infoRow("Hosts", value: "\(info.hosts)") + infoRow("Layers", value: "\(info.layers)") + infoRow("Microkernel", value: info.hasMicrokernel ? "Yes" : "No") + infoRow("Signed", value: info.isSigned ? "Yes" : "No") + + if let url = info.primaryHostURL { + VStack(alignment: .leading, spacing: 4) { + Text("Primary Host") + .font(.caption) + .foregroundStyle(.secondary) + Text(url) + .font(.system(.caption2, design: .monospaced)) + .foregroundStyle(.blue) + .textSelection(.enabled) + } + } + } + } + + // Action buttons. + Button { + viewModel.reset() + } label: { + Label("Scan Another", systemImage: "qrcode.viewfinder") + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + } + .padding() + } + } + + // MARK: - Error + + private func errorSection(_ message: String) -> some View { + VStack(spacing: 24) { + Spacer() + + Image(systemName: "exclamationmark.triangle.fill") + .font(.system(size: 48)) + .foregroundStyle(.red) + + Text("Decode Failed") + .font(.title2.weight(.semibold)) + + Text(message) + .font(.body) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + .padding(.horizontal, 32) + + Button { + viewModel.reset() + } label: { + Label("Try Again", systemImage: "arrow.counterclockwise") + .font(.body.weight(.medium)) + } + .buttonStyle(.borderedProminent) + + Spacer() + } + } + + // MARK: - Helpers + + private func infoRow(_ label: String, value: String) -> some View { + HStack { + Text(label) + .font(.subheadline) + .foregroundStyle(.secondary) + Spacer() + Text(value) + .font(.subheadline.weight(.medium)) + } + } + + private func formatCount(_ count: UInt32) -> String { + if count >= 1_000_000 { + return String(format: "%.1fM", Double(count) / 1_000_000.0) + } else if count >= 1_000 { + return String(format: "%.1fK", Double(count) / 1_000.0) + } + return "\(count)" + } + + private func formatBytes(_ bytes: UInt32) -> String { + if bytes >= 1024 { + return String(format: "%.1f KB", Double(bytes) / 1024.0) + } + return "\(bytes) B" + } +} + +// MARK: - ViewModel + +/// View model driving the App Clip scan-decode flow. +@MainActor +final class AppClipViewModel: ObservableObject { + + enum State: Equatable { + case scanning + case decoding + case decoded(SeedInfo) + case error(String) + } + + @Published var state: State = .scanning + + private let decoder = SeedDecoder() + + /// Handle raw QR payload bytes from the camera scanner. + func handleScannedData(_ data: Data) { + state = .decoding + Task { + do { + let info = try decoder.decode(data: data) + state = .decoded(info) + } catch { + state = .error(error.localizedDescription) + } + } + } + + /// Decode a built-in demo seed for testing without a camera. + func decodeDemoSeed() { + // Construct a minimal valid RVQS header (64 bytes) for demonstration. + // In production, this would come from a real QR scan. + var payload = Data(count: 64) + payload.withUnsafeMutableBytes { buf in + let p = buf.baseAddress!.assumingMemoryBound(to: UInt8.self) + + // seed_magic = 0x52565153 ("RVQS") in little-endian. + p[0] = 0x53; p[1] = 0x51; p[2] = 0x56; p[3] = 0x52 + // seed_version = 1. + p[4] = 0x01; p[5] = 0x00 + // flags = 0 (minimal). + p[6] = 0x00; p[7] = 0x00 + // file_id = 8 bytes. + for i in 8..<16 { p[i] = UInt8(i) } + // total_vector_count = 1000 (little-endian). + p[0x10] = 0xE8; p[0x11] = 0x03; p[0x12] = 0x00; p[0x13] = 0x00 + // dimension = 128. + p[0x14] = 0x80; p[0x15] = 0x00 + // base_dtype = 0, profile_id = 0. + p[0x16] = 0x00; p[0x17] = 0x00 + // created_ns = 0 (8 bytes, already zero). + // microkernel_offset = 64, microkernel_size = 0. + p[0x20] = 0x40; p[0x21] = 0x00; p[0x22] = 0x00; p[0x23] = 0x00 + // download_manifest_offset = 64, download_manifest_size = 0. + p[0x28] = 0x40; p[0x29] = 0x00; p[0x2A] = 0x00; p[0x2B] = 0x00 + // sig_algo = 0, sig_length = 0. + // total_seed_size = 64. + p[0x34] = 0x40; p[0x35] = 0x00; p[0x36] = 0x00; p[0x37] = 0x00 + // content_hash = 8 bytes of 0xAB. + for i in 0x38..<0x40 { p[i] = 0xAB } + } + + handleScannedData(payload) + } + + /// Reset to scanning state. + func reset() { + state = .scanning + } +} + +// MARK: - QR Scanner Coordinator (AVFoundation placeholder) + +/// Coordinator for camera-based QR code scanning. +/// +/// In a full implementation, this would wrap AVCaptureSession with a +/// metadata output delegate to detect QR codes in real-time. +/// Kept as a placeholder to show the integration pattern. +#if canImport(AVFoundation) +final class QRScannerCoordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate { + + var onSeedScanned: ((Data) -> Void)? + + func metadataOutput( + _ output: AVCaptureMetadataOutput, + didOutput metadataObjects: [AVMetadataObject], + from connection: AVCaptureConnection + ) { + guard let readable = metadataObjects.first as? AVMetadataMachineReadableCodeObject, + readable.type == .qr, + let stringValue = readable.stringValue, + let data = stringValue.data(using: .utf8) + else { + return + } + + // In production, the QR code would contain raw binary data. + // For App Clips invoked via URL, the seed bytes would be + // fetched from the URL's associated payload. + onSeedScanned?(data) + } +} +#endif diff --git a/examples/app-clip/Sources/AppClip/SeedDecoder.swift b/examples/app-clip/Sources/AppClip/SeedDecoder.swift new file mode 100644 index 000000000..be090f021 --- /dev/null +++ b/examples/app-clip/Sources/AppClip/SeedDecoder.swift @@ -0,0 +1,166 @@ +// SeedDecoder.swift — Swift wrapper for decoding RVQS QR Cognitive Seeds. +// +// Calls into the RVF C FFI (librvf_runtime.a) via the RVFBridge module +// to parse raw seed bytes scanned from a QR code. + +import Foundation +import RVFBridge + +// MARK: - SeedInfo + +/// Decoded information from an RVQS QR Cognitive Seed. +struct SeedInfo: Codable, Equatable, Sendable { + /// Seed format version. + let version: UInt16 + /// Number of download hosts in the manifest. + let hosts: UInt32 + /// Number of progressive download layers. + let layers: UInt32 + /// SHAKE-256-64 content hash as a hex string. + let contentHash: String + /// Total vector count the seed references. + let totalVectorCount: UInt32 + /// Vector dimensionality. + let dimension: UInt16 + /// Total seed payload size in bytes. + let totalSeedSize: UInt32 + /// Whether the seed has an embedded WASM microkernel. + let hasMicrokernel: Bool + /// Whether the seed is cryptographically signed. + let isSigned: Bool + /// Primary download URL (if available). + let primaryHostURL: String? +} + +// MARK: - SeedDecoderError + +/// Errors that can occur during seed decoding. +enum SeedDecoderError: LocalizedError { + case emptyData + case parseFailed(code: Int32) + case urlExtractionFailed(code: Int32) + + var errorDescription: String? { + switch self { + case .emptyData: + return "Seed data is empty." + case .parseFailed(let code): + return "Seed parse failed with error code \(code)." + case .urlExtractionFailed(let code): + return "Host URL extraction failed with error code \(code)." + } + } +} + +// MARK: - SeedDecoder + +/// Decodes RVQS QR Cognitive Seeds by calling the RVF C FFI. +/// +/// Usage: +/// ```swift +/// let decoder = SeedDecoder() +/// let info = try decoder.decode(data: qrPayload) +/// print(info.contentHash) +/// ``` +final class SeedDecoder: Sendable { + + init() {} + + /// Decode raw QR seed bytes into a `SeedInfo`. + /// + /// - Parameter data: The raw RVQS seed payload from a QR code. + /// - Returns: Parsed seed information. + /// - Throws: `SeedDecoderError` if parsing fails. + func decode(data: Data) throws -> SeedInfo { + guard !data.isEmpty else { + throw SeedDecoderError.emptyData + } + + // Parse the 64-byte header via the C FFI. + var header = RvqsHeaderC() + let parseResult: Int32 = data.withUnsafeBytes { rawBuffer in + guard let baseAddress = rawBuffer.baseAddress else { + return RVQS_ERR_NULL_PTR + } + let ptr = baseAddress.assumingMemoryBound(to: UInt8.self) + return rvqs_parse_header(ptr, rawBuffer.count, &header) + } + + guard parseResult == RVQS_OK else { + throw SeedDecoderError.parseFailed(code: parseResult) + } + + // Extract primary host URL (best-effort; nil if not available). + let primaryURL = extractPrimaryHostURL(from: data) + + // Derive host_count and layer_count from the seed result. + // The C FFI provides the header; we infer counts from manifest presence. + // For a full implementation, rvf_seed_parse would walk the TLV manifest. + // In this skeleton, we report manifest presence via flags. + let hasManifest = (header.flags & 0x0002) != 0 + let hostCount: UInt32 = primaryURL != nil ? 1 : 0 + let layerCount: UInt32 = hasManifest ? 1 : 0 + + // Build the hex string for content_hash. + let hashBytes = withUnsafeBytes(of: header.content_hash) { Array($0) } + let contentHash = hashBytes.map { String(format: "%02x", $0) }.joined() + + // Check flags. + let hasMicrokernel = (header.flags & 0x0001) != 0 + let isSigned = (header.flags & 0x0004) != 0 + + return SeedInfo( + version: header.seed_version, + hosts: hostCount, + layers: layerCount, + contentHash: contentHash, + totalVectorCount: header.total_vector_count, + dimension: header.dimension, + totalSeedSize: header.total_seed_size, + hasMicrokernel: hasMicrokernel, + isSigned: isSigned, + primaryHostURL: primaryURL + ) + } + + /// Verify the content hash of a seed payload. + /// + /// - Parameter data: The raw RVQS seed payload. + /// - Returns: `true` if the content hash is valid. + func verifyContentHash(data: Data) -> Bool { + let result: Int32 = data.withUnsafeBytes { rawBuffer in + guard let baseAddress = rawBuffer.baseAddress else { + return RVQS_ERR_NULL_PTR + } + let ptr = baseAddress.assumingMemoryBound(to: UInt8.self) + return rvqs_verify_content_hash(ptr, rawBuffer.count) + } + return result == RVQS_OK + } + + // MARK: - Private + + /// Extract the primary download host URL from the seed's TLV manifest. + private func extractPrimaryHostURL(from data: Data) -> String? { + var urlBuffer = [UInt8](repeating: 0, count: 256) + var urlLength: Int = 0 + + let result: Int32 = data.withUnsafeBytes { rawBuffer in + guard let baseAddress = rawBuffer.baseAddress else { + return RVQS_ERR_NULL_PTR + } + let ptr = baseAddress.assumingMemoryBound(to: UInt8.self) + return rvqs_get_primary_host_url( + ptr, rawBuffer.count, + &urlBuffer, urlBuffer.count, + &urlLength + ) + } + + guard result == RVQS_OK, urlLength > 0 else { + return nil + } + + return String(bytes: urlBuffer[.. +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ---- Result codes ---- */ + +#define RVQS_OK 0 +#define RVQS_ERR_NULL_PTR -1 +#define RVQS_ERR_TOO_SHORT -2 +#define RVQS_ERR_BAD_MAGIC -3 +#define RVQS_ERR_SIGNATURE_INVALID -4 +#define RVQS_ERR_HASH_MISMATCH -5 +#define RVQS_ERR_DECOMPRESS_FAIL -6 +#define RVQS_ERR_BUFFER_TOO_SMALL -7 +#define RVQS_ERR_PARSE_FAIL -8 + +/* ---- Structs ---- */ + +/** + * Mirrors the RvqsHeaderC struct from ffi.rs. + * 64-byte fixed-size header of an RVQS QR Cognitive Seed. + */ +typedef struct { + uint32_t seed_magic; + uint16_t seed_version; + uint16_t flags; + uint8_t file_id[8]; + uint32_t total_vector_count; + uint16_t dimension; + uint8_t base_dtype; + uint8_t profile_id; + uint64_t created_ns; + uint32_t microkernel_offset; + uint32_t microkernel_size; + uint32_t download_manifest_offset; + uint32_t download_manifest_size; + uint16_t sig_algo; + uint16_t sig_length; + uint32_t total_seed_size; + uint8_t content_hash[8]; +} RvqsHeaderC; + +/** + * High-level seed parse result returned to Swift. + * Populated by rvf_seed_parse and freed by rvf_seed_free. + */ +typedef struct { + /** Seed format version. */ + uint16_t version; + /** Number of download hosts in the manifest. */ + uint32_t host_count; + /** Number of progressive layers in the manifest. */ + uint32_t layer_count; + /** SHAKE-256-64 content hash (8 bytes). */ + uint8_t content_hash[8]; + /** Total vector count from the header. */ + uint32_t total_vector_count; + /** Vector dimensionality. */ + uint16_t dimension; + /** Total seed payload size. */ + uint32_t total_seed_size; + /** Seed flags bitfield. */ + uint16_t flags; +} RvfSeedResult; + +/* ---- FFI Functions (from librvf_runtime.a) ---- */ + +/** + * Parse a raw RVQS seed payload and extract header information. + * + * @param data Pointer to the raw QR seed bytes. + * @param len Length of the data buffer. + * @param out Pointer to an RvqsHeaderC struct to receive the parsed header. + * @return RVQS_OK on success, or a negative error code. + */ +int32_t rvqs_parse_header(const uint8_t *data, size_t len, RvqsHeaderC *out); + +/** + * Verify the HMAC-SHA256 signature of a QR seed. + * + * @param data Pointer to the full seed payload. + * @param data_len Length of the seed payload. + * @param key Pointer to the signing key. + * @param key_len Length of the signing key. + * @return RVQS_OK if signature is valid, or a negative error code. + */ +int32_t rvqs_verify_signature(const uint8_t *data, size_t data_len, + const uint8_t *key, size_t key_len); + +/** + * Verify the content hash of a QR seed payload. + * + * @param data Pointer to the full seed payload. + * @param data_len Length of the seed payload. + * @return RVQS_OK if hash matches, or a negative error code. + */ +int32_t rvqs_verify_content_hash(const uint8_t *data, size_t data_len); + +/** + * Decompress the WASM microkernel from a QR seed. + * + * @param data Pointer to the full seed payload. + * @param data_len Length of the seed payload. + * @param out Buffer to receive decompressed microkernel. + * @param out_cap Capacity of the output buffer. + * @param out_len Receives the actual decompressed size. + * @return RVQS_OK on success, or a negative error code. + */ +int32_t rvqs_decompress_microkernel(const uint8_t *data, size_t data_len, + uint8_t *out, size_t out_cap, + size_t *out_len); + +/** + * Extract the primary host URL from the download manifest. + * + * @param data Pointer to the full seed payload. + * @param data_len Length of the seed payload. + * @param url_buf Buffer to receive the URL string (not null-terminated). + * @param url_cap Capacity of the URL buffer. + * @param url_len Receives the actual URL length. + * @return RVQS_OK on success, or a negative error code. + */ +int32_t rvqs_get_primary_host_url(const uint8_t *data, size_t data_len, + uint8_t *url_buf, size_t url_cap, + size_t *url_len); + +/* ---- Convenience wrappers (implemented in Swift, declared here for reference) ---- */ + +/** + * Parse a QR seed payload into a high-level RvfSeedResult. + * + * This is a convenience wrapper that calls rvqs_parse_header internally + * and populates the simplified result struct. Implemented on the Swift side + * using the lower-level FFI functions above. + * + * @param data Pointer to the raw QR seed bytes. + * @param len Length of the data buffer. + * @param out Pointer to an RvfSeedResult struct to populate. + * @return RVQS_OK on success, or a negative error code. + */ +int32_t rvf_seed_parse(const uint8_t *data, size_t len, RvfSeedResult *out); + +/** + * Free any resources associated with an RvfSeedResult. + * + * Currently a no-op since RvfSeedResult is a plain value type, + * but provided for forward-compatibility if the struct gains + * heap-allocated fields. + * + * @param result Pointer to the result to free. + */ +void rvf_seed_free(RvfSeedResult *result); + +#ifdef __cplusplus +} +#endif + +#endif /* RVF_BRIDGE_H */