use std::io::{self, BufRead, Cursor};
use std::sync::Arc;
use super::fs::glob_ci;
use super::{BdDir, BdFile, ReadSeek, SearchOption, extension_of_name};
#[derive(Debug, Clone)]
pub struct MemFile {
name: String,
full_name: String,
extension: String,
data: Arc<[u8]>,
}
impl MemFile {
#[must_use]
pub fn new(dir: &str, name: &str, data: Vec<u8>) -> Self {
Self {
full_name: format!("{dir}/{name}"),
extension: extension_of_name(name),
name: name.to_owned(),
data: Arc::from(data),
}
}
}
impl BdFile for MemFile {
fn name(&self) -> &str {
&self.name
}
fn full_name(&self) -> &str {
&self.full_name
}
fn extension(&self) -> &str {
&self.extension
}
fn length(&self) -> u64 {
u64::try_from(self.data.len()).unwrap_or(u64::MAX)
}
fn is_dir(&self) -> bool {
false
}
fn open_read(&self) -> io::Result<Box<dyn ReadSeek>> {
Ok(Box::new(Cursor::new(Arc::clone(&self.data))))
}
fn open_text(&self) -> io::Result<Box<dyn BufRead>> {
Ok(Box::new(Cursor::new(Arc::clone(&self.data))))
}
}
#[derive(Debug, Clone)]
pub struct MemDir {
name: String,
full_name: String,
dirs: Vec<Self>,
files: Vec<MemFile>,
}
impl MemDir {
#[must_use]
pub fn new(name: &str) -> Self {
Self {
name: name.to_owned(),
full_name: name.to_owned(),
dirs: Vec::new(),
files: Vec::new(),
}
}
pub fn add_file(&mut self, dirs: &[&str], name: &str, data: Vec<u8>) {
let mut node = self;
for &dir in dirs {
let idx = if let Some(found) = node.dirs.iter().position(|d| d.name == dir) {
found
} else {
let full_name = format!("{}/{dir}", node.full_name);
node.dirs.push(Self {
name: dir.to_owned(),
full_name,
dirs: Vec::new(),
files: Vec::new(),
});
node.dirs.len().saturating_sub(1)
};
#[expect(
clippy::indexing_slicing,
reason = "idx is freshly found-or-pushed in node.dirs, always in bounds"
)]
let next = &mut node.dirs[idx];
node = next;
}
let file = MemFile::new(&node.full_name, name, data);
node.files.push(file);
}
fn collect_pattern(&self, pattern: &str, recurse: bool, out: &mut Vec<Box<dyn BdFile>>) {
for file in &self.files {
if glob_ci(pattern.as_bytes(), file.name.as_bytes()) {
out.push(Box::new(file.clone()));
}
}
if recurse {
for dir in &self.dirs {
dir.collect_pattern(pattern, recurse, out);
}
}
}
}
impl BdDir for MemDir {
fn name(&self) -> &str {
&self.name
}
fn full_name(&self) -> &str {
&self.full_name
}
fn parent(&self) -> Option<Box<dyn BdDir>> {
None
}
fn get_files_pattern_option(
&self,
pattern: &str,
option: SearchOption,
) -> io::Result<Vec<Box<dyn BdFile>>> {
let mut out = Vec::new();
self.collect_pattern(pattern, option == SearchOption::AllDirectories, &mut out);
Ok(out)
}
fn get_directories(&self) -> io::Result<Vec<Box<dyn BdDir>>> {
Ok(self.dirs.iter().map(|d| -> Box<dyn BdDir> { Box::new(d.clone()) }).collect())
}
}
pub const SECTION_COUNT: usize = 6;
#[must_use]
pub fn split_sections(data: &[u8], max: usize) -> Vec<Vec<u8>> {
let mut sections: Vec<Vec<u8>> = Vec::new();
let mut rest = data;
while sections.len() < max {
let Some((len_bytes, tail)) = rest.split_first_chunk::<4>() else { break };
let want = usize::try_from(u32::from_be_bytes(*len_bytes)).unwrap_or(usize::MAX);
let take = want.min(tail.len());
let (head, next) = tail.split_at(take);
sections.push(head.to_vec());
rest = next;
}
sections
}
#[must_use]
pub fn build_tree(root_name: &str, sections: Vec<Vec<u8>>) -> MemDir {
let mut next = sections.into_iter();
let mut take = || next.next().unwrap_or_default();
let mut root = MemDir::new(root_name);
root.add_file(&["BDMV"], "index.bdmv", take());
root.add_file(&["BDMV"], "MovieObject.bdmv", take());
root.add_file(&["BDMV", "PLAYLIST"], "00000.mpls", take());
root.add_file(&["BDMV", "CLIPINF"], "00000.clpi", take());
root.add_file(&["BDMV", "STREAM"], "00000.m2ts", take());
root.add_file(&["BDMV", "META", "DL"], "bdmt_eng.xml", take());
root
}
#[cfg(test)]
mod tests {
use std::io::{Read, Seek, SeekFrom};
use proptest::prelude::{any, prop, prop_assert, prop_assert_eq, proptest};
use super::{MemDir, MemFile, SECTION_COUNT, build_tree, split_sections};
use crate::vfs::{BdDir, BdFile, SearchOption};
fn all_files(dir: &MemDir) -> Vec<(String, Vec<u8>)> {
dir.get_files_pattern_option("*", SearchOption::AllDirectories)
.expect("in-memory walks cannot fail")
.iter()
.map(|file| {
let mut bytes = Vec::new();
file.open_read().expect("open").read_to_end(&mut bytes).expect("read");
(file.full_name().to_owned(), bytes)
})
.collect()
}
#[test]
fn mem_file_exposes_metadata_and_reads_as_bytes_and_text() {
let file = MemFile::new("DISC/BDMV", "index.bdmv", b"hello".to_vec());
assert_eq!(file.name(), "index.bdmv");
assert_eq!(file.full_name(), "DISC/BDMV/index.bdmv");
assert_eq!(file.extension(), ".bdmv");
assert_eq!(file.length(), 5);
assert!(!file.is_dir());
let mut reader = file.open_read().expect("open_read");
assert_eq!(reader.seek(SeekFrom::Start(1)).expect("seek"), 1);
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).expect("read bytes");
assert_eq!(bytes, b"ello");
let mut text = String::new();
file.open_text().expect("open_text").read_to_string(&mut text).expect("read text");
assert_eq!(text, "hello");
assert_eq!(MemFile::new("DISC", "NOEXT", Vec::new()).extension(), "");
}
#[test]
fn mem_types_render_debug() {
let mut root = MemDir::new("DISC");
root.add_file(&["BDMV"], "index.bdmv", vec![1]);
let rendered = format!("{root:?}");
assert!(rendered.contains("BDMV"));
assert!(rendered.contains("index.bdmv"));
}
#[test]
fn dir_walk_matches_patterns_with_and_without_recursion() {
let mut root = MemDir::new("DISC");
root.add_file(&["BDMV"], "index.bdmv", vec![0_u8; 2]);
root.add_file(&["BDMV", "STREAM"], "00000.m2ts", vec![0_u8; 4]);
let bdmv = root.get_directories().expect("dirs").into_iter().next().expect("BDMV");
assert_eq!(bdmv.name(), "BDMV");
assert_eq!(bdmv.full_name(), "DISC/BDMV");
assert!(bdmv.parent().is_none());
assert_eq!(bdmv.get_files().expect("files").len(), 1);
assert_eq!(bdmv.get_directories().expect("dirs").len(), 1);
assert_eq!(bdmv.get_files_pattern("*.m2ts").expect("shallow").len(), 0);
assert_eq!(
bdmv.get_files_pattern_option("*.M2TS", SearchOption::AllDirectories)
.expect("deep")
.len(),
1
);
assert_eq!(bdmv.get_files_pattern("*.bdmv").expect("top match").len(), 1);
}
#[test]
fn add_file_reuses_directories_and_derives_full_names() {
let mut root = MemDir::new("DISC");
root.add_file(&["BDMV", "PLAYLIST"], "00000.mpls", vec![b'a']);
root.add_file(&["BDMV", "PLAYLIST"], "00001.mpls", vec![b'b']);
root.add_file(&["BDMV", "CLIPINF"], "00000.clpi", vec![b'c']);
root.add_file(&[], "readme.txt", vec![b'd']);
assert_eq!(root.dirs.len(), 1);
assert_eq!(root.files.iter().map(|f| f.name.as_str()).collect::<Vec<_>>(), ["readme.txt"]);
let bdmv = root.dirs.first().expect("BDMV");
assert_eq!(bdmv.full_name, "DISC/BDMV");
assert_eq!(
bdmv.dirs.iter().map(|d| d.name.as_str()).collect::<Vec<_>>(),
["PLAYLIST", "CLIPINF"]
);
let playlist = bdmv.dirs.first().expect("PLAYLIST");
assert_eq!(
playlist.files.iter().map(|f| f.full_name.as_str()).collect::<Vec<_>>(),
["DISC/BDMV/PLAYLIST/00000.mpls", "DISC/BDMV/PLAYLIST/00001.mpls"]
);
}
#[test]
fn split_sections_frames_prefixed_sections_up_to_max() {
assert_eq!(
split_sections(&[0, 0, 0, 1, b'A', 0, 0, 0, 1, b'B'], 6),
[vec![b'A'], vec![b'B']]
);
assert_eq!(split_sections(&[0, 0, 0, 4, b'X', b'Y'], 6), [vec![b'X', b'Y']]);
assert!(split_sections(&[0, 0], 6).is_empty());
let many: Vec<u8> = std::iter::repeat_n(0_u8, 32).collect();
assert_eq!(split_sections(&many, 6).len(), 6);
assert_eq!(split_sections(&many, 7).len(), 7);
assert!(split_sections(&many, 0).is_empty());
}
#[test]
fn build_tree_lays_the_sections_onto_the_skeleton_in_order() {
let sections: Vec<Vec<u8>> = (1_u8..=6).map(|i| vec![i]).collect();
let root = build_tree("WASMDISC", sections);
assert_eq!(root.name(), "WASMDISC");
assert_eq!(
all_files(&root),
[
("WASMDISC/BDMV/index.bdmv".to_owned(), vec![1]),
("WASMDISC/BDMV/MovieObject.bdmv".to_owned(), vec![2]),
("WASMDISC/BDMV/PLAYLIST/00000.mpls".to_owned(), vec![3]),
("WASMDISC/BDMV/CLIPINF/00000.clpi".to_owned(), vec![4]),
("WASMDISC/BDMV/STREAM/00000.m2ts".to_owned(), vec![5]),
("WASMDISC/BDMV/META/DL/bdmt_eng.xml".to_owned(), vec![6]),
]
);
}
#[test]
fn build_tree_leaves_missing_sections_empty_and_ignores_extras() {
let partial = build_tree("FUZZDISC", vec![vec![9], vec![8]]);
let files = all_files(&partial);
assert_eq!(files.len(), SECTION_COUNT);
assert_eq!(
files.iter().map(|(_, bytes)| bytes.len()).collect::<Vec<_>>(),
[1, 1, 0, 0, 0, 0]
);
assert!(files.iter().all(|(full, _)| full.starts_with("FUZZDISC/BDMV")));
let overfull = build_tree("DISC", vec![vec![7]; 8]);
assert_eq!(all_files(&overfull).len(), SECTION_COUNT);
}
#[test]
fn a_built_tree_is_extendable_with_an_ssif_entry() {
let mut root = build_tree("FUZZDISC", Vec::new());
root.add_file(&["BDMV", "STREAM", "SSIF"], "00000.ssif", vec![b's']);
let files = all_files(&root);
assert_eq!(files.len(), SECTION_COUNT.saturating_add(1));
assert!(
files.iter().any(
|(full, bytes)| full == "FUZZDISC/BDMV/STREAM/SSIF/00000.ssif" && bytes == b"s"
)
);
}
proptest! {
#[test]
fn split_sections_never_panics_and_bounds_hold(
data in any::<Vec<u8>>(),
max in 0_usize..10,
) {
let sections = split_sections(&data, max);
prop_assert!(sections.len() <= max);
let payload: usize = sections.iter().map(Vec::len).sum();
prop_assert!(payload <= data.len());
}
#[test]
fn split_sections_round_trips_whole_sections(
sections in prop::collection::vec(prop::collection::vec(any::<u8>(), 0..64), 0..8),
) {
let mut data = Vec::new();
for section in §ions {
let len = u32::try_from(section.len()).expect("sections are < 2^32 bytes");
data.extend_from_slice(&len.to_be_bytes());
data.extend_from_slice(section);
}
prop_assert_eq!(split_sections(&data, sections.len()), sections);
}
}
}