use std::sync::{Mutex, OnceLock};
use crate::error::{err_number, VBError, VBResult};
use crate::library::resources::resfile::ResFile;
static RESOURCE_FILE: OnceLock<Mutex<Option<Linked>>> = OnceLock::new();
#[derive(Debug)]
struct Linked {
path: String,
parsed: Option<ResFile>,
}
fn state() -> &'static Mutex<Option<Linked>> {
RESOURCE_FILE.get_or_init(|| Mutex::new(None))
}
pub fn set_file(path: impl Into<String>) {
let mut guard = state().lock().unwrap_or_else(|e| e.into_inner());
*guard = Some(Linked {
path: path.into(),
parsed: None,
});
}
pub fn file_path() -> Option<String> {
let guard = state().lock().unwrap_or_else(|e| e.into_inner());
guard.as_ref().map(|linked| linked.path.clone())
}
pub fn clear() {
let mut guard = state().lock().unwrap_or_else(|e| e.into_inner());
*guard = None;
}
pub fn invalidate_cache() {
let mut guard = state().lock().unwrap_or_else(|e| e.into_inner());
if let Some(linked) = guard.as_mut() {
linked.parsed = None;
}
}
pub fn with_file<T>(f: impl FnOnce(&ResFile) -> VBResult<T>) -> VBResult<T> {
let mut guard = state().lock().unwrap_or_else(|e| e.into_inner());
let linked = guard
.as_mut()
.ok_or_else(|| VBError::new(err_number::RESOURCE_NOT_FOUND))?;
if linked.parsed.is_none() {
linked.parsed = Some(ResFile::load(&linked.path)?);
}
let parsed = linked
.parsed
.as_ref()
.expect("resource file was just loaded");
f(parsed)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::state::file;
fn sample_res() -> Vec<u8> {
let mut bytes = res_record(0, 0, &[]);
bytes.extend(res_record(10, 101, b"payload"));
bytes
}
fn res_record(res_type: u16, name: u16, data: &[u8]) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend_from_slice(&(data.len() as u32).to_le_bytes());
bytes.extend_from_slice(&32u32.to_le_bytes());
bytes.extend_from_slice(&0xFFFFu16.to_le_bytes());
bytes.extend_from_slice(&res_type.to_le_bytes());
bytes.extend_from_slice(&0xFFFFu16.to_le_bytes());
bytes.extend_from_slice(&name.to_le_bytes());
bytes.extend_from_slice(&[0u8; 16]);
bytes.extend_from_slice(data);
bytes.resize(bytes.len().next_multiple_of(4), 0);
bytes
}
fn with_linked_res<T>(f: impl FnOnce() -> T) -> T {
let _guard = crate::state::test_support::lock_test();
let _ = file::close_all_files();
file::set_backend(Box::new(file::memory::MemoryBackend::new()));
file::set_root("/");
file::write_memory_file("/app.res", &sample_res()).unwrap();
clear();
set_file("/app.res");
let result = f();
clear();
let _ = file::close_all_files();
file::reset_backend();
result
}
#[test]
fn unlinked_file_reports_resource_not_found() {
let _guard = crate::state::test_support::lock_test();
clear();
let error = with_file(|_| Ok(())).unwrap_err();
assert_eq!(error.number, err_number::RESOURCE_NOT_FOUND);
}
#[test]
fn set_file_records_the_path() {
let _guard = crate::state::test_support::lock_test();
clear();
set_file("/some/app.res");
assert_eq!(file_path().as_deref(), Some("/some/app.res"));
clear();
assert_eq!(file_path(), None);
}
#[test]
fn with_file_parses_the_linked_file() {
with_linked_res(|| {
let count = with_file(|res| Ok(res.entry_count())).unwrap();
assert_eq!(count, 1);
});
}
#[test]
fn parse_is_cached_across_calls() {
with_linked_res(|| {
with_file(|res| Ok(res.entry_count())).unwrap();
file::write_memory_file("/app.res", &res_record(0, 0, &[])).unwrap();
let cached = with_file(|res| Ok(res.entry_count())).unwrap();
assert_eq!(cached, 1, "cached parse should be reused");
invalidate_cache();
let fresh = with_file(|res| Ok(res.entry_count())).unwrap();
assert_eq!(fresh, 0);
});
}
#[test]
fn missing_linked_file_reports_file_not_found() {
let _guard = crate::state::test_support::lock_test();
let _ = file::close_all_files();
file::set_backend(Box::new(file::memory::MemoryBackend::new()));
file::set_root("/");
clear();
set_file("/absent.res");
let error = with_file(|_| Ok(())).unwrap_err();
assert_eq!(error.number, err_number::FILE_NOT_FOUND);
clear();
let _ = file::close_all_files();
file::reset_backend();
}
}