#![allow(unsafe_code)]
use alloc::boxed::Box;
use alloc::vec::Vec;
use crate::jaccard_estimate;
use crate::reference::canonical_stream;
pub const PITH_OK: i32 = 0;
pub const PITH_E_INVALID: i32 = -1;
pub const PITH_E_REJECTED: i32 = -2;
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pith_text_fingerprint(
data: *const u8,
len: usize,
out: *mut *mut u8,
out_len: *mut usize,
) -> i32 {
if out.is_null() || out_len.is_null() {
return PITH_E_INVALID;
}
let bytes: &[u8] = if len == 0 {
&[]
} else {
if data.is_null() {
return PITH_E_INVALID;
}
unsafe { core::slice::from_raw_parts(data, len) }
};
match fingerprint_stream(bytes) {
Ok(stream) => {
let len = stream.len();
let ptr = Box::into_raw(stream.into_boxed_slice());
unsafe {
*out = ptr.cast::<u8>();
*out_len = len;
}
PITH_OK
}
Err(status) => status,
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pith_text_jaccard(
a: *const u64,
a_words: usize,
b: *const u64,
b_words: usize,
out_bits: *mut u64,
) -> i32 {
if out_bits.is_null() {
return PITH_E_INVALID;
}
let a = match unsafe { read_words(a, a_words) } {
Ok(words) => words,
Err(status) => return status,
};
let b = match unsafe { read_words(b, b_words) } {
Ok(words) => words,
Err(status) => return status,
};
unsafe { *out_bits = jaccard_bits(&a, &b) };
PITH_OK
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pith_text_free(ptr: *mut u8, len: usize) {
if ptr.is_null() {
return;
}
let slice = unsafe { core::slice::from_raw_parts_mut(ptr, len) };
drop(unsafe { Box::from_raw(slice) });
}
fn fingerprint_stream(bytes: &[u8]) -> Result<Vec<u8>, i32> {
let text = core::str::from_utf8(bytes).map_err(|_| PITH_E_REJECTED)?;
Ok(canonical_stream(text))
}
fn jaccard_bits(a: &[u64], b: &[u64]) -> u64 {
jaccard_estimate(a, b).to_bits()
}
unsafe fn read_words(ptr: *const u64, words: usize) -> Result<Vec<u64>, i32> {
if ptr.is_null() {
if words == 0 {
return Ok(Vec::new());
}
return Err(PITH_E_INVALID);
}
let mut out = Vec::new();
for i in 0..words {
out.push(unsafe { core::ptr::read_unaligned(ptr.add(i)) });
}
Ok(out)
}
#[cfg(test)]
mod tests {
use alloc::vec::Vec;
use super::{
PITH_E_INVALID, PITH_E_REJECTED, PITH_OK, fingerprint_stream, jaccard_bits,
pith_text_fingerprint, pith_text_free, pith_text_jaccard, read_words,
};
fn canonical_stream_core(input: &[u8]) -> Vec<u8> {
crate::reference::canonical_stream(core::str::from_utf8(input).expect("utf-8"))
}
fn fixture(name: &str) -> Vec<u8> {
let path = format!("{}/tests/fixtures/{name}.txt", env!("CARGO_MANIFEST_DIR"));
std::fs::read(&path).expect("fixture")
}
fn fixture_words(name: &str) -> Vec<u64> {
let stream = fingerprint_stream(&fixture(name)).expect("utf-8");
let tail = &stream[stream.len() - 1024..];
tail.chunks_exact(8)
.map(|c| u64::from_le_bytes(c.try_into().unwrap()))
.collect()
}
#[test]
fn ffi_fingerprint_roundtrips_the_canonical_stream() {
let input = b"alpha beta gamma";
let expected = canonical_stream_core(input);
assert_eq!(expected.len(), 8 + 17 + 1024);
let mut out: *mut u8 = core::ptr::null_mut();
let mut out_len: usize = 0;
let status =
unsafe { pith_text_fingerprint(input.as_ptr(), input.len(), &mut out, &mut out_len) };
assert_eq!(status, PITH_OK);
assert_eq!(out_len, expected.len());
let handed_back = unsafe { core::slice::from_raw_parts(out, out_len) };
assert_eq!(handed_back, expected.as_slice());
assert_eq!(&handed_back[..8], &[0, 0, 0, 3, 0, 0, 0, 1]);
unsafe { pith_text_free(out, out_len) };
}
#[test]
fn ffi_fingerprint_accepts_the_empty_input() {
let expected = canonical_stream_core(b"");
assert_eq!(expected.len(), 8 + 1 + 1024);
let empty: Vec<u8> = Vec::new();
let mut out: *mut u8 = core::ptr::null_mut();
let mut out_len: usize = 0;
let status =
unsafe { pith_text_fingerprint(empty.as_ptr(), empty.len(), &mut out, &mut out_len) };
assert_eq!(status, PITH_OK);
let handed_back = unsafe { core::slice::from_raw_parts(out, out_len) };
assert_eq!(handed_back, expected.as_slice());
assert_eq!(handed_back[8], b'\n');
assert!(handed_back[9..].iter().all(|&b| b == 0xff));
unsafe { pith_text_free(out, out_len) };
let status = unsafe { pith_text_fingerprint(core::ptr::null(), 0, &mut out, &mut out_len) };
assert_eq!(status, PITH_OK);
unsafe { pith_text_free(out, out_len) };
}
#[test]
fn ffi_fingerprint_refusals() {
let mut out: *mut u8 = core::ptr::null_mut();
let mut out_len: usize = 0;
let input = b"text";
let null_out = unsafe {
pith_text_fingerprint(
input.as_ptr(),
input.len(),
core::ptr::null_mut(),
&mut out_len,
)
};
assert_eq!(null_out, PITH_E_INVALID);
let null_out_len = unsafe {
pith_text_fingerprint(input.as_ptr(), input.len(), &mut out, core::ptr::null_mut())
};
assert_eq!(null_out_len, PITH_E_INVALID);
let null_data = unsafe {
pith_text_fingerprint(core::ptr::null(), input.len(), &mut out, &mut out_len)
};
assert_eq!(null_data, PITH_E_INVALID);
let bad_utf8: &[u8] = &[b'o', b'n', b'e', 0xFF];
let status = unsafe {
pith_text_fingerprint(bad_utf8.as_ptr(), bad_utf8.len(), &mut out, &mut out_len)
};
assert_eq!(status, PITH_E_REJECTED);
unsafe { pith_text_free(core::ptr::null_mut(), 0) };
}
#[test]
fn safe_core_rejects_non_utf8() {
assert_eq!(fingerprint_stream(&[0xFF]), Err(PITH_E_REJECTED));
assert_eq!(fingerprint_stream(b"ok"), Ok(canonical_stream_core(b"ok")));
}
#[test]
fn ffi_jaccard_matches_reference_pins() {
let doc_a = fixture_words("doc_a");
let doc_a_edit = fixture_words("doc_a_edit");
let doc_b = fixture_words("doc_b");
let prose = fixture_words("prose");
let mut bits: u64 = 0;
let status = unsafe {
pith_text_jaccard(
doc_a.as_ptr(),
doc_a.len(),
doc_a_edit.as_ptr(),
doc_a_edit.len(),
&mut bits,
)
};
assert_eq!(status, PITH_OK);
assert_eq!(bits, 0x3fef_8000_0000_0000);
let status = unsafe {
pith_text_jaccard(
doc_a.as_ptr(),
doc_a.len(),
doc_b.as_ptr(),
doc_b.len(),
&mut bits,
)
};
assert_eq!(status, PITH_OK);
assert_eq!(bits, 0x0000_0000_0000_0000);
let status = unsafe {
pith_text_jaccard(
prose.as_ptr(),
prose.len(),
prose.as_ptr(),
prose.len(),
&mut bits,
)
};
assert_eq!(status, PITH_OK);
assert_eq!(bits, 0x3ff0_0000_0000_0000);
let status = unsafe {
pith_text_jaccard(doc_a.as_ptr(), doc_a.len(), core::ptr::null(), 0, &mut bits)
};
assert_eq!(status, PITH_OK);
assert_eq!(bits, 0x3ff0_0000_0000_0000);
let status =
unsafe { pith_text_jaccard(core::ptr::null(), 0, core::ptr::null(), 0, &mut bits) };
assert_eq!(status, PITH_OK);
assert_eq!(bits, 0x3ff0_0000_0000_0000);
}
#[test]
fn ffi_jaccard_refusals() {
let words = [1u64, 2, 3];
let mut bits: u64 = 0;
let null_bits = unsafe {
pith_text_jaccard(
words.as_ptr(),
words.len(),
words.as_ptr(),
words.len(),
core::ptr::null_mut(),
)
};
assert_eq!(null_bits, PITH_E_INVALID);
let null_a = unsafe {
pith_text_jaccard(core::ptr::null(), 1, words.as_ptr(), words.len(), &mut bits)
};
assert_eq!(null_a, PITH_E_INVALID);
let null_b = unsafe {
pith_text_jaccard(words.as_ptr(), words.len(), core::ptr::null(), 2, &mut bits)
};
assert_eq!(null_b, PITH_E_INVALID);
}
#[test]
fn safe_cores_match_the_abi_contract() {
assert_eq!(unsafe { read_words(core::ptr::null(), 0) }, Ok(Vec::new()));
assert_eq!(
unsafe { read_words(core::ptr::null(), 1) },
Err(PITH_E_INVALID)
);
let words = [7u64, 9];
assert_eq!(
unsafe { read_words(words.as_ptr(), words.len()) },
Ok(words.to_vec())
);
assert_eq!(jaccard_bits(&[], &[]), 1.0f64.to_bits());
assert_eq!(jaccard_bits(&[1], &[1]), 1.0f64.to_bits());
assert_eq!(jaccard_bits(&[1], &[2]), 0.0f64.to_bits());
}
}