use std::path::PathBuf;
use crate::native::core::ChoiceValue;
pub trait TestCaseDatabase: Send + Sync {
fn fetch(&self, key: &[u8]) -> Vec<Vec<u8>>;
fn save(&self, key: &[u8], value: &[u8]);
fn delete(&self, key: &[u8], value: &[u8]);
fn move_value(&self, src: &[u8], dst: &[u8], value: &[u8]);
}
pub const METAKEYS_NAME: &[u8] = b".hegel-keys";
const KEY_PREFIX: &[u8] = b"native:";
fn key_hash(key: &[u8]) -> String {
let mut buf = Vec::with_capacity(KEY_PREFIX.len() + key.len());
buf.extend_from_slice(KEY_PREFIX);
buf.extend_from_slice(key);
fnv_hex(&buf)
}
pub struct DirectoryTestCaseDatabase {
db_root: PathBuf,
metakeys_hash: String,
}
impl DirectoryTestCaseDatabase {
pub fn new(db_root: &str) -> Self {
DirectoryTestCaseDatabase {
db_root: PathBuf::from(db_root),
metakeys_hash: key_hash(METAKEYS_NAME),
}
}
pub fn key_path(&self, key: &[u8]) -> PathBuf {
self.db_root.join(key_hash(key))
}
fn value_path(&self, key: &[u8], value: &[u8]) -> PathBuf {
self.key_path(key).join(fnv_hex(value))
}
}
impl TestCaseDatabase for DirectoryTestCaseDatabase {
fn fetch(&self, key: &[u8]) -> Vec<Vec<u8>> {
let dir = self.key_path(key);
let entries = match std::fs::read_dir(&dir) {
Ok(d) => d,
Err(_) => return Vec::new(),
};
let mut out = Vec::new();
for entry in entries.flatten() {
if let Ok(bytes) = std::fs::read(entry.path()) {
out.push(bytes);
}
}
out
}
fn save(&self, key: &[u8], value: &[u8]) {
if key_hash(key) != self.metakeys_hash {
self.save(METAKEYS_NAME, key);
}
let dir = self.key_path(key);
if std::fs::create_dir_all(&dir).is_err() {
return; }
let path = self.value_path(key, value);
if path.exists() {
return;
}
let _ = std::fs::write(&path, value);
}
fn delete(&self, key: &[u8], value: &[u8]) {
if std::fs::remove_file(self.value_path(key, value)).is_err() {
return;
}
if std::fs::remove_dir(self.key_path(key)).is_ok() && key_hash(key) != self.metakeys_hash {
self.delete(METAKEYS_NAME, key);
}
}
fn move_value(&self, src: &[u8], dst: &[u8], value: &[u8]) {
if src == dst {
self.save(src, value);
return;
}
if !self.key_path(dst).exists() {
self.save(METAKEYS_NAME, dst);
}
let dst_dir = self.key_path(dst);
if std::fs::create_dir_all(&dst_dir).is_err() {
self.delete(src, value);
self.save(dst, value);
return;
}
let src_path = self.value_path(src, value);
let dst_path = self.value_path(dst, value);
if std::fs::rename(&src_path, &dst_path).is_err() {
self.delete(src, value);
self.save(dst, value);
return;
}
let _ = std::fs::remove_dir(self.key_path(src));
}
}
pub(super) fn fnv_hex(s: &[u8]) -> String {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
for &byte in s {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
format!("{hash:016x}")
}
pub fn serialize_choices(choices: &[ChoiceValue]) -> Vec<u8> {
let mut buf = Vec::with_capacity(4 + choices.len() * 17);
let count = choices.len() as u32;
buf.extend_from_slice(&count.to_le_bytes());
for choice in choices {
match choice {
ChoiceValue::Integer(v) => {
buf.push(0);
buf.extend_from_slice(&v.to_le_bytes());
}
ChoiceValue::Boolean(v) => {
buf.push(1);
buf.push(*v as u8);
}
ChoiceValue::Float(v) => {
buf.push(2);
buf.extend_from_slice(&v.to_bits().to_le_bytes());
}
}
}
buf
}
pub fn deserialize_choices(bytes: &[u8]) -> Option<Vec<ChoiceValue>> {
if bytes.len() < 4 {
return None;
}
let count = u32::from_le_bytes(bytes[..4].try_into().ok()?) as usize;
let mut choices = Vec::with_capacity(count.min(bytes.len()));
let mut pos = 4;
for _ in 0..count {
if pos >= bytes.len() {
return None;
}
match bytes[pos] {
0 => {
pos += 1;
if pos + 16 > bytes.len() {
return None;
}
let v = i128::from_le_bytes(bytes[pos..pos + 16].try_into().ok()?);
choices.push(ChoiceValue::Integer(v));
pos += 16;
}
1 => {
pos += 1;
if pos >= bytes.len() {
return None;
}
choices.push(ChoiceValue::Boolean(bytes[pos] != 0));
pos += 1;
}
2 => {
pos += 1;
if pos + 8 > bytes.len() {
return None;
}
let bits = u64::from_le_bytes(bytes[pos..pos + 8].try_into().ok()?);
choices.push(ChoiceValue::Float(f64::from_bits(bits)));
pos += 8;
}
_ => return None,
}
}
Some(choices)
}
#[cfg(test)]
#[path = "../../tests/embedded/native/database_tests.rs"]
mod tests;