Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
382
vendor/ruvector/crates/rvf/rvf-runtime/src/ffi.rs
vendored
Normal file
382
vendor/ruvector/crates/rvf/rvf-runtime/src/ffi.rs
vendored
Normal file
@@ -0,0 +1,382 @@
|
||||
//! C FFI for App Clip / mobile integration.
|
||||
//!
|
||||
//! These `extern "C"` functions can be compiled into a static library
|
||||
//! (.a / .xcframework) and called directly from Swift or Kotlin.
|
||||
//!
|
||||
//! Build for iOS:
|
||||
//! cargo build --release --target aarch64-apple-ios --lib
|
||||
//! cargo build --release --target aarch64-apple-ios-sim --lib
|
||||
//!
|
||||
//! Build for Android:
|
||||
//! cargo build --release --target aarch64-linux-android --lib
|
||||
//!
|
||||
//! The App Clip contains ~50 KB of this library. Combined with the QR
|
||||
//! seed payload, the user experience is: Scan → Boot → Intelligence.
|
||||
|
||||
use crate::compress;
|
||||
use crate::qr_seed::ParsedSeed;
|
||||
use crate::seed_crypto;
|
||||
use rvf_types::qr_seed::{SeedHeader, SEED_HEADER_SIZE};
|
||||
|
||||
/// Result codes for FFI functions.
|
||||
pub const RVQS_OK: i32 = 0;
|
||||
pub const RVQS_ERR_NULL_PTR: i32 = -1;
|
||||
pub const RVQS_ERR_TOO_SHORT: i32 = -2;
|
||||
pub const RVQS_ERR_BAD_MAGIC: i32 = -3;
|
||||
pub const RVQS_ERR_SIGNATURE_INVALID: i32 = -4;
|
||||
pub const RVQS_ERR_HASH_MISMATCH: i32 = -5;
|
||||
pub const RVQS_ERR_DECOMPRESS_FAIL: i32 = -6;
|
||||
pub const RVQS_ERR_BUFFER_TOO_SMALL: i32 = -7;
|
||||
pub const RVQS_ERR_PARSE_FAIL: i32 = -8;
|
||||
|
||||
/// Opaque header struct for C interop (mirrors SeedHeader layout).
|
||||
#[repr(C)]
|
||||
pub struct RvqsHeaderC {
|
||||
pub seed_magic: u32,
|
||||
pub seed_version: u16,
|
||||
pub flags: u16,
|
||||
pub file_id: [u8; 8],
|
||||
pub total_vector_count: u32,
|
||||
pub dimension: u16,
|
||||
pub base_dtype: u8,
|
||||
pub profile_id: u8,
|
||||
pub created_ns: u64,
|
||||
pub microkernel_offset: u32,
|
||||
pub microkernel_size: u32,
|
||||
pub download_manifest_offset: u32,
|
||||
pub download_manifest_size: u32,
|
||||
pub sig_algo: u16,
|
||||
pub sig_length: u16,
|
||||
pub total_seed_size: u32,
|
||||
pub content_hash: [u8; 8],
|
||||
}
|
||||
|
||||
const _: () = assert!(core::mem::size_of::<RvqsHeaderC>() == SEED_HEADER_SIZE);
|
||||
|
||||
impl From<SeedHeader> for RvqsHeaderC {
|
||||
fn from(h: SeedHeader) -> Self {
|
||||
Self {
|
||||
seed_magic: h.seed_magic,
|
||||
seed_version: h.seed_version,
|
||||
flags: h.flags,
|
||||
file_id: h.file_id,
|
||||
total_vector_count: h.total_vector_count,
|
||||
dimension: h.dimension,
|
||||
base_dtype: h.base_dtype,
|
||||
profile_id: h.profile_id,
|
||||
created_ns: h.created_ns,
|
||||
microkernel_offset: h.microkernel_offset,
|
||||
microkernel_size: h.microkernel_size,
|
||||
download_manifest_offset: h.download_manifest_offset,
|
||||
download_manifest_size: h.download_manifest_size,
|
||||
sig_algo: h.sig_algo,
|
||||
sig_length: h.sig_length,
|
||||
total_seed_size: h.total_seed_size,
|
||||
content_hash: h.content_hash,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a QR seed payload and extract the header.
|
||||
///
|
||||
/// # Safety
|
||||
/// `data` must point to `data_len` valid bytes. `out` must point to a valid `RvqsHeaderC`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rvqs_parse_header(
|
||||
data: *const u8,
|
||||
data_len: usize,
|
||||
out: *mut RvqsHeaderC,
|
||||
) -> i32 {
|
||||
if data.is_null() || out.is_null() {
|
||||
return RVQS_ERR_NULL_PTR;
|
||||
}
|
||||
if data_len < SEED_HEADER_SIZE {
|
||||
return RVQS_ERR_TOO_SHORT;
|
||||
}
|
||||
|
||||
let slice = core::slice::from_raw_parts(data, data_len);
|
||||
match SeedHeader::from_bytes(slice) {
|
||||
Ok(header) => {
|
||||
*out = header.into();
|
||||
RVQS_OK
|
||||
}
|
||||
Err(_) => RVQS_ERR_BAD_MAGIC,
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify the HMAC-SHA256 signature of a QR seed.
|
||||
///
|
||||
/// # Safety
|
||||
/// All pointers must be valid for their respective lengths.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rvqs_verify_signature(
|
||||
data: *const u8,
|
||||
data_len: usize,
|
||||
key: *const u8,
|
||||
key_len: usize,
|
||||
) -> i32 {
|
||||
if data.is_null() || key.is_null() {
|
||||
return RVQS_ERR_NULL_PTR;
|
||||
}
|
||||
if data_len < SEED_HEADER_SIZE {
|
||||
return RVQS_ERR_TOO_SHORT;
|
||||
}
|
||||
|
||||
let slice = core::slice::from_raw_parts(data, data_len);
|
||||
let key_slice = core::slice::from_raw_parts(key, key_len);
|
||||
|
||||
let parsed = match ParsedSeed::parse(slice) {
|
||||
Ok(p) => p,
|
||||
Err(_) => return RVQS_ERR_PARSE_FAIL,
|
||||
};
|
||||
|
||||
let signature = match parsed.signature {
|
||||
Some(s) => s,
|
||||
None => return RVQS_ERR_SIGNATURE_INVALID,
|
||||
};
|
||||
|
||||
let signed_payload = match parsed.signed_payload(slice) {
|
||||
Some(p) => p,
|
||||
None => return RVQS_ERR_SIGNATURE_INVALID,
|
||||
};
|
||||
|
||||
if seed_crypto::verify_seed(key_slice, signed_payload, signature) {
|
||||
RVQS_OK
|
||||
} else {
|
||||
RVQS_ERR_SIGNATURE_INVALID
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify the content hash of a QR seed payload.
|
||||
///
|
||||
/// # Safety
|
||||
/// `data` must point to `data_len` valid bytes.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rvqs_verify_content_hash(data: *const u8, data_len: usize) -> i32 {
|
||||
if data.is_null() {
|
||||
return RVQS_ERR_NULL_PTR;
|
||||
}
|
||||
if data_len < SEED_HEADER_SIZE {
|
||||
return RVQS_ERR_TOO_SHORT;
|
||||
}
|
||||
|
||||
let slice = core::slice::from_raw_parts(data, data_len);
|
||||
let parsed = match ParsedSeed::parse(slice) {
|
||||
Ok(p) => p,
|
||||
Err(_) => return RVQS_ERR_PARSE_FAIL,
|
||||
};
|
||||
|
||||
let microkernel = parsed.microkernel.unwrap_or(&[]);
|
||||
let manifest = parsed.manifest_bytes.unwrap_or(&[]);
|
||||
|
||||
let mut hash_input = Vec::with_capacity(microkernel.len() + manifest.len());
|
||||
hash_input.extend_from_slice(microkernel);
|
||||
hash_input.extend_from_slice(manifest);
|
||||
|
||||
if seed_crypto::verify_content_hash(&parsed.header.content_hash, &hash_input) {
|
||||
RVQS_OK
|
||||
} else {
|
||||
RVQS_ERR_HASH_MISMATCH
|
||||
}
|
||||
}
|
||||
|
||||
/// Decompress the microkernel from a QR seed.
|
||||
///
|
||||
/// # Safety
|
||||
/// `data` must point to `data_len` valid bytes. `out` must point to `out_cap` bytes.
|
||||
/// `out_len` will receive the actual decompressed size.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rvqs_decompress_microkernel(
|
||||
data: *const u8,
|
||||
data_len: usize,
|
||||
out: *mut u8,
|
||||
out_cap: usize,
|
||||
out_len: *mut usize,
|
||||
) -> i32 {
|
||||
if data.is_null() || out.is_null() || out_len.is_null() {
|
||||
return RVQS_ERR_NULL_PTR;
|
||||
}
|
||||
|
||||
let slice = core::slice::from_raw_parts(data, data_len);
|
||||
let parsed = match ParsedSeed::parse(slice) {
|
||||
Ok(p) => p,
|
||||
Err(_) => return RVQS_ERR_PARSE_FAIL,
|
||||
};
|
||||
|
||||
let compressed = match parsed.microkernel {
|
||||
Some(m) => m,
|
||||
None => {
|
||||
*out_len = 0;
|
||||
return RVQS_OK;
|
||||
}
|
||||
};
|
||||
|
||||
let decompressed = match compress::decompress(compressed) {
|
||||
Ok(d) => d,
|
||||
Err(_) => return RVQS_ERR_DECOMPRESS_FAIL,
|
||||
};
|
||||
|
||||
if decompressed.len() > out_cap {
|
||||
return RVQS_ERR_BUFFER_TOO_SMALL;
|
||||
}
|
||||
|
||||
let out_slice = core::slice::from_raw_parts_mut(out, out_cap);
|
||||
out_slice[..decompressed.len()].copy_from_slice(&decompressed);
|
||||
*out_len = decompressed.len();
|
||||
|
||||
RVQS_OK
|
||||
}
|
||||
|
||||
/// Get the download manifest URL from a parsed seed.
|
||||
///
|
||||
/// # Safety
|
||||
/// All pointers must be valid. `url_buf` must have `url_cap` bytes available.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rvqs_get_primary_host_url(
|
||||
data: *const u8,
|
||||
data_len: usize,
|
||||
url_buf: *mut u8,
|
||||
url_cap: usize,
|
||||
url_len: *mut usize,
|
||||
) -> i32 {
|
||||
if data.is_null() || url_buf.is_null() || url_len.is_null() {
|
||||
return RVQS_ERR_NULL_PTR;
|
||||
}
|
||||
|
||||
let slice = core::slice::from_raw_parts(data, data_len);
|
||||
let parsed = match ParsedSeed::parse(slice) {
|
||||
Ok(p) => p,
|
||||
Err(_) => return RVQS_ERR_PARSE_FAIL,
|
||||
};
|
||||
|
||||
let manifest = match parsed.parse_manifest() {
|
||||
Ok(m) => m,
|
||||
Err(_) => return RVQS_ERR_PARSE_FAIL,
|
||||
};
|
||||
|
||||
let host = match manifest.hosts.first() {
|
||||
Some(h) => h,
|
||||
None => {
|
||||
*url_len = 0;
|
||||
return RVQS_OK;
|
||||
}
|
||||
};
|
||||
|
||||
let url_bytes = &host.url[..host.url_length as usize];
|
||||
if url_bytes.len() > url_cap {
|
||||
return RVQS_ERR_BUFFER_TOO_SMALL;
|
||||
}
|
||||
|
||||
let out_slice = core::slice::from_raw_parts_mut(url_buf, url_cap);
|
||||
out_slice[..url_bytes.len()].copy_from_slice(url_bytes);
|
||||
*url_len = url_bytes.len();
|
||||
|
||||
RVQS_OK
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::qr_seed::{make_host_entry, SeedBuilder};
|
||||
use rvf_types::qr_seed::*;
|
||||
|
||||
fn build_signed_seed() -> Vec<u8> {
|
||||
let key = b"test-key-for-ffi-unit-testing-ok";
|
||||
let mk = crate::compress::compress(&[0xCA; 2000]);
|
||||
let host = make_host_entry("https://cdn.test.com/brain.rvf", 0, 1, [0xAA; 16]).unwrap();
|
||||
|
||||
let builder = SeedBuilder::new([0x01; 8], 128, 1000)
|
||||
.with_microkernel(mk)
|
||||
.add_host(host);
|
||||
let (payload, _header) = builder.build_and_sign(key).unwrap();
|
||||
payload
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ffi_parse_header() {
|
||||
let payload = build_signed_seed();
|
||||
let mut header = core::mem::MaybeUninit::<RvqsHeaderC>::uninit();
|
||||
let rc = unsafe { rvqs_parse_header(payload.as_ptr(), payload.len(), header.as_mut_ptr()) };
|
||||
assert_eq!(rc, RVQS_OK);
|
||||
let header = unsafe { header.assume_init() };
|
||||
assert_eq!(header.seed_magic, SEED_MAGIC);
|
||||
assert_eq!(header.dimension, 128);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ffi_verify_signature() {
|
||||
let key = b"test-key-for-ffi-unit-testing-ok";
|
||||
let payload = build_signed_seed();
|
||||
let rc = unsafe {
|
||||
rvqs_verify_signature(payload.as_ptr(), payload.len(), key.as_ptr(), key.len())
|
||||
};
|
||||
assert_eq!(rc, RVQS_OK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ffi_verify_signature_wrong_key() {
|
||||
let payload = build_signed_seed();
|
||||
let bad_key = b"wrong-key-should-fail-verificatn";
|
||||
let rc = unsafe {
|
||||
rvqs_verify_signature(
|
||||
payload.as_ptr(),
|
||||
payload.len(),
|
||||
bad_key.as_ptr(),
|
||||
bad_key.len(),
|
||||
)
|
||||
};
|
||||
assert_eq!(rc, RVQS_ERR_SIGNATURE_INVALID);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ffi_verify_content_hash() {
|
||||
let payload = build_signed_seed();
|
||||
let rc = unsafe { rvqs_verify_content_hash(payload.as_ptr(), payload.len()) };
|
||||
assert_eq!(rc, RVQS_OK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ffi_decompress_microkernel() {
|
||||
let payload = build_signed_seed();
|
||||
let mut out = vec![0u8; 8192];
|
||||
let mut out_len: usize = 0;
|
||||
let rc = unsafe {
|
||||
rvqs_decompress_microkernel(
|
||||
payload.as_ptr(),
|
||||
payload.len(),
|
||||
out.as_mut_ptr(),
|
||||
out.len(),
|
||||
&mut out_len,
|
||||
)
|
||||
};
|
||||
assert_eq!(rc, RVQS_OK);
|
||||
assert_eq!(out_len, 2000);
|
||||
assert_eq!(&out[..out_len], &[0xCA; 2000]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ffi_get_primary_host_url() {
|
||||
let payload = build_signed_seed();
|
||||
let mut url_buf = vec![0u8; 256];
|
||||
let mut url_len: usize = 0;
|
||||
let rc = unsafe {
|
||||
rvqs_get_primary_host_url(
|
||||
payload.as_ptr(),
|
||||
payload.len(),
|
||||
url_buf.as_mut_ptr(),
|
||||
url_buf.len(),
|
||||
&mut url_len,
|
||||
)
|
||||
};
|
||||
assert_eq!(rc, RVQS_OK);
|
||||
let url = core::str::from_utf8(&url_buf[..url_len]).unwrap();
|
||||
assert_eq!(url, "https://cdn.test.com/brain.rvf");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ffi_null_ptr_returns_error() {
|
||||
let mut header = core::mem::MaybeUninit::<RvqsHeaderC>::uninit();
|
||||
let rc = unsafe { rvqs_parse_header(core::ptr::null(), 0, header.as_mut_ptr()) };
|
||||
assert_eq!(rc, RVQS_ERR_NULL_PTR);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user