#![forbid(unsafe_code)]
use std::ffi::CString;
use std::ptr;
use tempfile::TempDir;
use crate::engine::Engine;
use crate::ffi::{
EFS_ABI_VERSION, EFS_ENGINE_CREATE, EFS_ENGINE_OPEN, EFS_INVALID_ARGUMENT, EFS_NOT_FOUND,
EFS_OK,
};
type Handle = *mut Engine;
fn open_create(dir: &std::path::Path) -> Handle {
let c = CString::new(dir.to_str().expect("utf8 path")).expect("cstring");
let mut h: Handle = ptr::null_mut();
let rc = crate::ffi::entropyfs_engine_open(c.as_ptr(), EFS_ENGINE_CREATE, &mut h);
assert_eq!(rc, EFS_OK, "create failed: {}", last_error());
assert!(!h.is_null());
h
}
fn open_existing(dir: &std::path::Path) -> Handle {
let c = CString::new(dir.to_str().expect("utf8 path")).expect("cstring");
let mut h: Handle = ptr::null_mut();
let rc = crate::ffi::entropyfs_engine_open(c.as_ptr(), EFS_ENGINE_OPEN, &mut h);
assert_eq!(rc, EFS_OK, "open failed: {}", last_error());
assert!(!h.is_null());
h
}
fn close(h: Handle) {
let rc = crate::ffi::entropyfs_engine_close(h);
assert_eq!(rc, EFS_OK, "close failed: {}", last_error());
}
fn last_error() -> String {
let mut buf = [0u8; 512];
crate::ffi::entropyfs_last_error(buf.as_mut_ptr() as *mut std::ffi::c_char, buf.len());
let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
String::from_utf8_lossy(&buf[..end]).into_owned()
}
fn put(h: Handle, data: &[u8]) -> [u8; 32] {
let mut id = [0u8; 32];
let rc = crate::ffi::entropyfs_blob_put(
h,
if data.is_empty() {
ptr::null()
} else {
data.as_ptr()
},
data.len(),
id.as_mut_ptr(),
);
assert_eq!(rc, EFS_OK, "put failed: {}", last_error());
id
}
fn get(h: Handle, id: &[u8; 32]) -> Vec<u8> {
let mut buf: *mut u8 = ptr::null_mut();
let mut len = 0usize;
let rc = crate::ffi::entropyfs_blob_get(h, id.as_ptr(), &mut buf, &mut len);
assert_eq!(rc, EFS_OK, "get failed: {}", last_error());
crate::ffi::take_output(buf, len)
}
#[test]
fn abi_version_is_current() {
assert_eq!(crate::ffi::entropyfs_abi_version(), EFS_ABI_VERSION);
}
#[test]
fn create_put_get_range_sync_compact_metrics() {
let tmp = TempDir::new().expect("tmp");
let dir = tmp.path().join("store");
let h = open_create(&dir);
let a = b"entropyfs c-abi blob one: the quick brown fox jumps over the lazy dog".to_vec();
let b = vec![0xABu8; 4096];
let c = b"{\"schema\":\"config\",\"value\":42,\"nested\":{\"a\":[1,2,3]}}".to_vec();
let id_a = put(h, &a);
let id_b = put(h, &b);
let id_c = put(h, &c);
let id_a2 = put(h, &a);
assert_eq!(id_a, id_a2, "equal bytes must dedup to the same id");
assert_ne!(id_a, id_b);
assert_ne!(id_b, id_c);
assert_eq!(get(h, &id_a), a);
assert_eq!(get(h, &id_b), b);
assert_eq!(get(h, &id_c), c);
let mut buf: *mut u8 = ptr::null_mut();
let mut len = 0usize;
let rc = crate::ffi::entropyfs_blob_read_range(h, id_b.as_ptr(), 100, 64, &mut buf, &mut len);
assert_eq!(rc, EFS_OK, "range failed: {}", last_error());
assert_eq!(len, 64);
let want = &b[100..164];
let got = crate::ffi::take_output(buf, len);
assert_eq!(got, want, "range bytes must match");
let mut present = 0;
let rc = crate::ffi::entropyfs_contains(h, id_a.as_ptr(), &mut present);
assert_eq!(rc, EFS_OK);
assert_eq!(present, 1);
let mut absent = 1;
let rc = crate::ffi::entropyfs_contains(h, [0xEE; 32].as_ptr(), &mut absent);
assert_eq!(rc, EFS_OK);
assert_eq!(absent, 0);
let rc = crate::ffi::entropyfs_sync(h);
assert_eq!(rc, EFS_OK, "sync failed: {}", last_error());
let mut reclaimed = u64::MAX;
let mut physical = u64::MAX;
let rc = crate::ffi::entropyfs_compact(h, &mut reclaimed, &mut physical);
assert_eq!(rc, EFS_OK, "compact failed: {}", last_error());
assert!(physical > 0, "a store with 3 blobs has physical bytes");
let mut jbuf: *mut u8 = ptr::null_mut();
let mut jlen = 0usize;
let rc = crate::ffi::entropyfs_metrics_json(h, &mut jbuf, &mut jlen);
assert_eq!(rc, EFS_OK, "metrics failed: {}", last_error());
let json: serde_json::Value =
serde_json::from_slice(&crate::ffi::take_output(jbuf, jlen)).expect("metrics JSON parses");
assert_eq!(json["schema_version"], 1);
assert!(json["accounting"]["physical_used_bytes"].as_u64().unwrap() > 0);
close(h);
}
#[test]
fn error_paths_are_classified() {
let tmp = TempDir::new().expect("tmp");
let dir = tmp.path().join("store");
let h = open_create(&dir);
let mut buf: *mut u8 = ptr::null_mut();
let mut len = 0usize;
let rc = crate::ffi::entropyfs_blob_get(h, [0x11; 32].as_ptr(), &mut buf, &mut len);
assert_eq!(rc, EFS_NOT_FOUND, "missing blob must be NOT_FOUND");
assert!(!last_error().is_empty(), "last_error carries detail");
assert!(buf.is_null(), "no buffer on error");
let rc =
crate::ffi::entropyfs_blob_get(ptr::null_mut(), [0u8; 32].as_ptr(), &mut buf, &mut len);
assert_eq!(rc, EFS_INVALID_ARGUMENT);
let mut id = [0u8; 32];
let rc = crate::ffi::entropyfs_blob_put(h, ptr::null(), 16, id.as_mut_ptr());
assert_eq!(rc, EFS_INVALID_ARGUMENT);
let ghost = CString::new(tmp.path().join("ghost").to_str().unwrap()).unwrap();
let mut h2: Handle = ptr::null_mut();
let rc = crate::ffi::entropyfs_engine_open(ghost.as_ptr(), EFS_ENGINE_OPEN, &mut h2);
assert_ne!(rc, EFS_OK);
assert!(h2.is_null());
let c = CString::new(dir.to_str().unwrap()).unwrap();
let mut h3: Handle = ptr::null_mut();
let rc = crate::ffi::entropyfs_engine_open(c.as_ptr(), EFS_ENGINE_CREATE, &mut h3);
assert_ne!(rc, EFS_OK, "creating over an existing store must fail");
close(h);
}
#[test]
fn lifecycle_open_close_cycles() {
let tmp = TempDir::new().expect("tmp");
let dir = tmp.path().join("store");
for i in 0..3 {
let mode = if i == 0 {
EFS_ENGINE_CREATE
} else {
EFS_ENGINE_OPEN
};
let c = CString::new(dir.to_str().unwrap()).unwrap();
let mut h: Handle = ptr::null_mut();
let rc = crate::ffi::entropyfs_engine_open(c.as_ptr(), mode, &mut h);
assert_eq!(rc, EFS_OK, "cycle {i} open: {}", last_error());
assert!(!h.is_null());
close(h);
}
}
#[test]
fn dedup_across_handles() {
let tmp = TempDir::new().expect("tmp");
let dir = tmp.path().join("store");
let h1 = open_create(&dir);
let id = put(h1, b"shared immutable bytes across handles".as_slice());
close(h1);
let h2 = open_existing(&dir);
let id2 = put(h2, b"shared immutable bytes across handles".as_slice());
assert_eq!(id, id2, "content identity is stable across opens");
assert_eq!(
get(h2, &id),
b"shared immutable bytes across handles".to_vec()
);
close(h2);
}