use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use whiteout::interfaces::{CascFileSystem, FileSystem, HostCascFileSystem, HostFileSystem};
#[derive(Default)]
struct MemFs {
files: Mutex<HashMap<String, Vec<u8>>>,
reads: AtomicUsize,
}
impl FileSystem for MemFs {
fn read_file(&self, path: &str) -> Option<Vec<u8>> {
self.reads.fetch_add(1, Ordering::Relaxed);
self.files.lock().unwrap().get(path).cloned()
}
fn write_file(&self, path: &str, data: &[u8]) -> bool {
self.files
.lock()
.unwrap()
.insert(path.to_string(), data.to_vec());
true
}
fn file_exists(&self, path: &str) -> bool {
self.files.lock().unwrap().contains_key(path)
}
}
#[test]
fn cpp_calls_back_into_a_rust_filesystem() {
let fs = MemFs::default();
fs.write_file("units/arthas.mdx", b"MDLX-ish bytes");
let host = HostFileSystem::new(fs);
let got = host.read_through_native("units/arthas.mdx");
assert_eq!(got.as_deref(), Some(&b"MDLX-ish bytes"[..]));
assert!(host.exists_through_native("units/arthas.mdx"));
assert!(!host.exists_through_native("nope.mdx"));
}
#[test]
fn a_missing_file_reads_back_as_none() {
let host = HostFileSystem::new(MemFs::default());
assert_eq!(host.read_through_native("absent"), None);
}
#[test]
fn large_buffers_survive_the_round_trip() {
let fs = MemFs::default();
let big: Vec<u8> = (0..64 * 1024).map(|i| (i % 251) as u8).collect();
fs.write_file("big.bin", &big);
let host = HostFileSystem::new(fs);
for _ in 0..64 {
let got = host.read_through_native("big.bin").expect("missing");
assert_eq!(got.len(), big.len());
assert_eq!(got, big);
}
}
#[test]
fn non_ascii_paths_cross_intact() {
let fs = MemFs::default();
fs.write_file("模型/Ünïcødé.mdx", b"ok");
let host = HostFileSystem::new(fs);
assert_eq!(
host.read_through_native("模型/Ünïcødé.mdx").as_deref(),
Some(&b"ok"[..])
);
}
struct PanickingFs {
on_read: bool,
on_exists: bool,
}
impl FileSystem for PanickingFs {
fn read_file(&self, _path: &str) -> Option<Vec<u8>> {
if self.on_read {
panic!("deliberate panic inside read_file");
}
Some(b"fine".to_vec())
}
fn file_exists(&self, _path: &str) -> bool {
if self.on_exists {
panic!("deliberate panic inside file_exists");
}
true
}
}
#[test]
fn a_panic_in_read_file_is_contained_at_the_boundary() {
let host = HostFileSystem::new(PanickingFs {
on_read: true,
on_exists: false,
});
let prev = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {})); let got = host.read_through_native("anything");
std::panic::set_hook(prev);
assert_eq!(got, None, "a contained panic must read back as absent");
assert!(host.exists_through_native("anything"));
}
#[test]
fn a_panic_in_file_exists_is_contained() {
let host = HostFileSystem::new(PanickingFs {
on_read: false,
on_exists: true,
});
let prev = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let exists = host.exists_through_native("anything");
std::panic::set_hook(prev);
assert!(!exists, "a contained panic must report false");
}
#[derive(Default)]
struct MemCasc {
files: Mutex<HashMap<u32, Vec<u8>>>,
}
impl CascFileSystem for MemCasc {
fn read_file(&self, file_id: u32) -> Option<Vec<u8>> {
self.files.lock().unwrap().get(&file_id).cloned()
}
fn reserve_file_id(&self, path: &str) -> Option<u32> {
Some(path.len() as u32)
}
fn write_file(&self, file_id: u32, data: &[u8]) -> bool {
self.files.lock().unwrap().insert(file_id, data.to_vec());
true
}
fn file_exists(&self, file_id: u32) -> bool {
self.files.lock().unwrap().contains_key(&file_id)
}
}
#[test]
fn cpp_calls_back_into_a_rust_casc_filesystem() {
let fs = MemCasc::default();
fs.write_file(1234, b"payload");
let host = HostCascFileSystem::new(fs);
assert_eq!(
host.read_through_native(1234).as_deref(),
Some(&b"payload"[..])
);
assert!(host.exists_through_native(1234));
assert!(!host.exists_through_native(9999));
assert_eq!(host.read_through_native(9999), None);
}
#[test]
fn a_host_filesystem_is_usable_from_many_threads() {
let fs = MemFs::default();
for i in 0..16 {
fs.write_file(&format!("f{i}"), format!("data{i}").as_bytes());
}
let host = Arc::new(HostFileSystem::new(fs));
let mut handles = Vec::new();
for t in 0..8 {
let host = Arc::clone(&host);
handles.push(std::thread::spawn(move || {
for i in 0..16 {
let want = format!("data{i}");
let got = host.read_through_native(&format!("f{i}")).expect("missing");
assert_eq!(got, want.as_bytes(), "thread {t}");
}
}));
}
for h in handles {
h.join().expect("worker thread panicked");
}
}
#[test]
fn dropping_the_host_wrapper_releases_both_sides() {
for _ in 0..256 {
let fs = MemFs::default();
fs.write_file("x", b"y");
let host = HostFileSystem::new(fs);
assert!(host.exists_through_native("x"));
}
}