use super::super::io::{self, Write};
use alloc::{collections::BTreeMap, collections::VecDeque, string::String, sync::Arc, vec::Vec};
use hadris_common::types::endian::EndianType;
use super::super::io::LogicalSector;
use super::super::path::PathTableEntryHeader;
use super::super::read::PathSeparator;
use crate::file::EntryType;
#[cfg(test)]
use crate::file::{convert_joliet3, convert_l1, convert_l2, convert_l3};
use super::super::directory::DirectoryRef;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct DirectoryId {
indices: Vec<usize>,
}
impl DirectoryId {
pub fn push(&mut self, index: usize) {
self.indices.push(index);
}
pub fn pop(&mut self) -> usize {
self.indices.pop().expect("directory underflow")
}
}
#[derive(Debug)]
pub struct WrittenFiles {
root: WrittenDirectory,
}
impl Default for WrittenFiles {
fn default() -> Self {
Self::new()
}
}
impl WrittenFiles {
pub fn new() -> Self {
Self {
root: WrittenDirectory::new(Arc::new(String::new())),
}
}
pub fn find_file(&self, name: &str, _sep: PathSeparator) -> Option<DirectoryRef> {
let mut current_dir = DirectoryId {
indices: Vec::new(),
};
let mut parts: Vec<&str> = name.split(['/', '\\']).filter(|s| !s.is_empty()).collect();
let filename = parts.pop()?;
'outer: for part in parts {
let dir = self.get(¤t_dir);
for (idx, dir) in dir.dirs.iter().enumerate() {
if dir.name.as_str() == part {
current_dir.push(idx);
continue 'outer;
}
}
return None;
}
let dir = self.get(¤t_dir);
dir.files
.iter()
.find(|f| f.name.as_str() == filename)
.map(|f| f.entry)
}
pub fn root_dir(&self) -> DirectoryId {
DirectoryId {
indices: Vec::new(),
}
}
pub fn root_refs(&self) -> &BTreeMap<EntryType, DirectoryRef> {
&self.root.entries
}
pub fn get(&self, id: &DirectoryId) -> &WrittenDirectory {
let mut dir = &self.root;
for index in &id.indices {
dir = &dir.dirs[*index];
}
dir
}
pub fn get_mut(&mut self, id: &DirectoryId) -> &mut WrittenDirectory {
let mut dir = &mut self.root;
for index in &id.indices {
dir = &mut dir.dirs[*index];
}
dir
}
}
#[derive(Debug)]
pub struct WrittenDirectory {
pub name: Arc<String>,
pub entries: BTreeMap<EntryType, DirectoryRef>,
pub dirs: Vec<WrittenDirectory>,
pub files: Vec<WrittenFile>,
}
impl WrittenDirectory {
pub fn new(name: Arc<String>) -> Self {
Self {
name,
entries: BTreeMap::new(),
dirs: Vec::new(),
files: Vec::new(),
}
}
pub fn push_dir(&mut self, name: Arc<String>) -> usize {
self.dirs.push(Self::new(name));
self.dirs.len() - 1
}
}
#[derive(Debug)]
pub struct WrittenFile {
pub name: Arc<String>,
pub entry: DirectoryRef,
}
pub(crate) struct PathTableWriter<'a> {
pub written_files: &'a WrittenFiles,
pub ty: EntryType,
pub endian: EndianType,
}
io_transform! {
async fn write_pt_record<DATA: Write>(
data: &mut DATA,
endian: &EndianType,
parent_number: u16,
extent: LogicalSector,
name: &[u8],
) -> io::Result<()> {
let header = PathTableEntryHeader {
len: name.len() as u8,
extended_attr_record: 0,
parent_directory_number: endian.u16_bytes(parent_number),
parent_lba: endian.u32_bytes(extent.0 as u32),
};
data.write_all(bytemuck::bytes_of(&header)).await?;
data.write_all(name).await?;
if !name.len().is_multiple_of(2) {
data.write_all(&[0x00]).await?; }
Ok(())
}
impl PathTableWriter<'_> {
pub async fn write<DATA: Write>(&mut self, data: &mut DATA) -> io::Result<()> {
let mut queue: VecDeque<(&WrittenDirectory, u16)> = VecDeque::new();
let mut current_number: u16 = 1;
let root = &self.written_files.root;
let root_extent = *root.entries.get(&self.ty).unwrap();
write_pt_record(data, &self.endian, 1, root_extent.extent, &[0x00]).await?;
queue.push_back((root, 1));
while let Some((dir, parent_num)) = queue.pop_front() {
let my_number = parent_num;
for child_dir in &dir.dirs {
current_number += 1;
let name = self.ty.convert_name(&child_dir.name);
let name_bytes = name.as_bytes();
let extent = child_dir.entries.get(&self.ty).unwrap().extent;
write_pt_record(data, &self.endian, my_number, extent, name_bytes).await?;
queue.push_back((child_dir, current_number));
}
}
Ok(())
}
}
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
use alloc::format;
#[test]
fn test_convert_l1() {
let orig = "this-is-the-original-file.@very-long-ext";
let converted = convert_l1(orig, false);
assert_eq!(converted.as_str(), "THIS_IS_._VE;1");
let converted = convert_l1(orig, true);
assert_eq!(converted.as_str(), "this_is_._ve;1");
}
#[test]
fn test_convert_l2_short_name() {
let orig = "readme.txt";
let converted = convert_l2(orig, false);
assert_eq!(converted.as_str(), "README.TXT;1");
}
#[test]
fn test_convert_l2_long_name_truncation() {
let orig = "this-is-a-very-long-filename-that-should-be-truncated.extension";
let converted = convert_l2(orig, false);
assert!(
converted.len() <= 32,
"L2 name too long: {}",
converted.len()
);
assert!(converted.as_str().ends_with(";1"));
}
#[test]
fn test_convert_l2_no_extension() {
let orig = "this-is-a-very-long-directory-name-without-extension";
let converted = convert_l2(orig, false);
assert!(
converted.len() <= 32,
"L2 name too long: {}",
converted.len()
);
assert!(converted.as_str().ends_with(";1"));
assert_eq!(converted.as_str(), "THIS_IS_A_VERY_LONG_DIRECTORY_;1");
}
#[test]
fn test_convert_l3_short_name() {
let orig = "readme.txt";
let converted = convert_l3(orig, false);
assert_eq!(converted.as_str(), "README.TXT");
}
#[test]
fn test_convert_l3_long_name_truncation() {
let long_name = "a".repeat(250);
let converted = convert_l3(&long_name, false);
assert!(
converted.len() <= 207,
"L3 name too long: {}",
converted.len()
);
assert_eq!(converted.len(), 207);
}
#[test]
fn test_convert_l3_with_extension() {
let basename = "a".repeat(200);
let orig = format!("{}.txt", basename);
let converted = convert_l3(&orig, false);
assert!(
converted.len() <= 207,
"L3 name too long: {}",
converted.len()
);
}
#[test]
fn test_convert_l1_empty_extension() {
let converted = convert_l1("file.", false);
assert_eq!(converted.as_str(), "FILE.;1");
}
#[test]
fn test_convert_l1_dot_only() {
let converted = convert_l1(".", false);
assert_eq!(converted.as_str(), ".;1");
}
#[test]
fn test_convert_l1_dot_dot() {
let converted = convert_l1("..", false);
assert_eq!(converted.as_str(), "._;1");
}
#[test]
fn test_convert_l1_no_dot() {
let converted = convert_l1("README", false);
assert_eq!(converted.as_str(), "README;1");
}
#[test]
fn test_convert_l1_no_dot_long() {
let converted = convert_l1("LONGFILENAME", false);
assert_eq!(converted.as_str(), "LONGFILE;1");
}
#[test]
fn test_convert_l1_exact_8_3() {
let converted = convert_l1("12345678.abc", false);
assert_eq!(converted.as_str(), "12345678.ABC;1");
}
#[test]
fn test_convert_l1_oversized() {
let converted = convert_l1("longname1.longext", false);
assert_eq!(converted.as_str(), "LONGNAME.LON;1");
}
#[test]
fn test_convert_l1_single_char() {
let converted = convert_l1("a.b", false);
assert_eq!(converted.as_str(), "A.B;1");
}
#[test]
fn test_convert_l1_multibyte_utf8() {
let converted = convert_l1("café.txt", false);
assert!(converted.len() <= 14, "L1 overflow: {}", converted.len());
assert!(converted.as_str().ends_with(";1"));
}
#[test]
fn test_convert_l2_empty_extension() {
let converted = convert_l2("file.", false);
assert_eq!(converted.as_str(), "FILE.;1");
}
#[test]
fn test_convert_l2_no_dot() {
let converted = convert_l2("README", false);
assert_eq!(converted.as_str(), "README;1");
}
#[test]
fn test_convert_l2_single_char() {
let converted = convert_l2("a.b", false);
assert_eq!(converted.as_str(), "A.B;1");
}
#[test]
fn test_convert_l3_empty_extension() {
let converted = convert_l3("file.", false);
assert_eq!(converted.as_str(), "FILE.");
}
#[test]
fn test_convert_l3_no_dot() {
let converted = convert_l3("README", false);
assert_eq!(converted.as_str(), "README");
}
#[test]
fn test_convert_l3_single_char() {
let converted = convert_l3("a.b", false);
assert_eq!(converted.as_str(), "A.B");
}
#[test]
fn test_convert_joliet3_short_name() {
let converted = convert_joliet3("readme.txt");
assert_eq!(converted.len(), 20);
}
#[test]
fn test_convert_joliet3_long_name_truncation() {
let long_name = "a".repeat(150);
let converted = convert_joliet3(&long_name);
assert!(
converted.len() <= 207,
"Joliet overflow: {}",
converted.len()
);
assert_eq!(converted.len(), 206);
}
#[test]
fn test_convert_joliet3_multibyte_utf8() {
let converted = convert_joliet3("café.txt");
assert_eq!(converted.len(), 16);
}
}