1use super::super::io::{self, Write};
2use alloc::vec;
3use alloc::{collections::BTreeMap, collections::VecDeque, string::String, sync::Arc, vec::Vec};
4use hadris_common::types::endian::EndianType;
5use hadris_path::{Component, Separators, VPath};
6
7use super::super::io::LogicalSector;
8use super::super::path::PathTableEntryHeader;
9use super::super::read::PathSeparator;
10use crate::file::EntryType;
11#[cfg(test)]
12use crate::file::{convert_joliet3, convert_l1, convert_l2, convert_l3};
13
14use super::super::directory::DirectoryRef;
15use super::{InputEntryKind, InputMetadata};
16
17#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
29pub struct DirectoryId {
30 indices: Vec<usize>,
31}
32
33impl DirectoryId {
34 pub(crate) const fn new() -> Self {
36 Self { indices: vec![] }
37 }
38
39 pub fn push(&mut self, index: usize) {
44 self.indices.push(index);
45 }
46
47 pub fn pop(&mut self) -> usize {
55 self.indices.pop().expect("directory underflow")
56 }
57}
58
59#[derive(Debug)]
60pub struct WrittenFiles {
62 root: WrittenDirectory,
63}
64
65impl Default for WrittenFiles {
66 fn default() -> Self {
67 Self::new()
68 }
69}
70
71impl WrittenFiles {
72 pub fn new() -> Self {
74 Self {
75 root: WrittenDirectory::new(Arc::new(String::new())),
76 }
77 }
78
79 pub fn find_file(&self, name: &str, _sep: PathSeparator) -> Option<DirectoryRef> {
81 let mut current_dir = DirectoryId::new();
82 let mut parts = VPath::with_separators(name, Separators::SlashOrBackslash)
83 .components()
84 .filter_map(|component| match component {
85 Component::Root | Component::Current => None,
86 Component::Parent => Some(None),
87 Component::Normal(component) => Some(Some(component)),
88 })
89 .peekable();
90 'parts: while let Some(part) = parts.next() {
91 let part = part?;
92 if parts.peek().is_none() {
93 let dir = self.get(¤t_dir);
94 return dir
95 .files
96 .iter()
97 .find(|file| file.name.as_str() == part)
98 .map(|file| file.entry);
99 }
100 let dir = self.get(¤t_dir);
101 for (idx, dir) in dir.dirs.iter().enumerate() {
102 if dir.name.as_str() == part {
103 current_dir.push(idx);
104 continue 'parts;
105 }
106 }
107 return None;
108 }
109 None
110 }
111
112 pub fn root_dir(&self) -> DirectoryId {
114 DirectoryId::new()
115 }
116
117 pub fn root_refs(&self) -> &BTreeMap<EntryType, DirectoryRef> {
119 &self.root.entries
120 }
121
122 pub fn get(&self, id: &DirectoryId) -> &WrittenDirectory {
124 let mut dir = &self.root;
125 for index in &id.indices {
126 dir = &dir.dirs[*index];
127 }
128 dir
129 }
130
131 pub fn get_mut(&mut self, id: &DirectoryId) -> &mut WrittenDirectory {
133 let mut dir = &mut self.root;
134 for index in &id.indices {
135 dir = &mut dir.dirs[*index];
136 }
137 dir
138 }
139}
140
141#[derive(Debug)]
145pub struct WrittenDirectory {
146 pub(crate) id: usize,
148
149 pub name: Arc<String>,
151
152 pub(crate) rrip_name: Arc<String>,
154
155 pub entries: BTreeMap<EntryType, DirectoryRef>,
157
158 pub dirs: Vec<WrittenDirectory>,
160
161 pub files: Vec<WrittenFile>,
163
164 pub metadata: InputMetadata,
166
167 pub(crate) relocation: DirectoryRelocation,
169}
170
171#[derive(Debug, Clone, Copy, Default)]
176pub(crate) enum DirectoryRelocation {
177 #[default]
179 None,
180
181 Placeholder {
183 target: usize,
185 },
186
187 Moved {
189 id: usize,
191 logical_parent: usize,
193 },
194}
195
196impl WrittenDirectory {
197 pub fn new(name: Arc<String>) -> Self {
202 Self {
203 id: 0,
204 rrip_name: name.clone(),
205 name,
206 entries: BTreeMap::new(),
207 dirs: Vec::new(),
208 files: Vec::new(),
209 metadata: InputMetadata::default(),
210 relocation: DirectoryRelocation::None,
211 }
212 }
213
214 pub(crate) fn get_dir_ref(
219 &self,
220 ty: EntryType,
221 relocation_refs: &BTreeMap<(usize, EntryType), DirectoryRef>,
222 ) -> DirectoryRef {
223 match self.relocation {
224 DirectoryRelocation::Placeholder { target } => relocation_refs
225 .get(&(target, ty))
226 .copied()
227 .unwrap_or_default(),
228 _ => *self.entries.get(&ty).unwrap(),
229 }
230 }
231
232 pub(crate) fn push_file(&mut self, file: WrittenFile) {
237 self.files.push(file);
238 }
239
240 pub fn push_dir(&mut self, name: Arc<String>, metadata: InputMetadata) -> usize {
249 let mut directory = Self::new(name);
250 directory.metadata = metadata;
251 self.dirs.push(directory);
252 self.dirs.len() - 1
253 }
254}
255
256#[derive(Debug)]
260pub struct WrittenFile {
261 pub name: Arc<String>,
263
264 pub entry: DirectoryRef,
269
270 pub kind: InputEntryKind,
272
273 pub metadata: InputMetadata,
275}
276
277impl WrittenFile {
278 pub(crate) fn new(name: Arc<String>, kind: InputEntryKind, metadata: InputMetadata) -> Self {
282 Self {
283 name,
284 entry: DirectoryRef::default(),
285 kind,
286 metadata,
287 }
288 }
289
290 pub(crate) fn with_extent(
294 name: Arc<String>,
295 kind: InputEntryKind,
296 metadata: InputMetadata,
297 extent: DirectoryRef,
298 ) -> Self {
299 Self {
300 name,
301 entry: extent,
302 kind,
303 metadata,
304 }
305 }
306}
307
308pub(crate) struct PathTableWriter<'a> {
309 pub written_files: &'a WrittenFiles,
310 pub ty: EntryType,
311 pub endian: EndianType,
312}
313
314io_transform! {
315
316async fn write_pt_record<DATA: Write>(
318 data: &mut DATA,
319 endian: &EndianType,
320 parent_number: u16,
321 extent: LogicalSector,
322 name: &[u8],
323) -> io::Result<()> {
324 let header = PathTableEntryHeader {
325 len: name.len() as u8,
326 extended_attr_record: 0,
327 parent_directory_number: endian.u16_bytes(parent_number),
328 parent_lba: endian.u32_bytes(extent.0 as u32),
329 };
330 data.write_all(bytemuck::bytes_of(&header)).await?;
331 data.write_all(name).await?;
332 if !name.len().is_multiple_of(2) {
333 data.write_all(&[0x00]).await?; }
335 Ok(())
336}
337
338impl PathTableWriter<'_> {
339 pub async fn write<DATA: Write>(&mut self, data: &mut DATA) -> io::Result<()> {
340 let mut queue: VecDeque<(&WrittenDirectory, u16)> = VecDeque::new();
343 let mut current_number: u16 = 1;
344
345 let root = &self.written_files.root;
347 let root_extent = *root.entries.get(&self.ty).unwrap();
348 write_pt_record(data, &self.endian, 1, root_extent.extent, &[0x00]).await?;
349 queue.push_back((root, 1));
350
351 while let Some((dir, parent_num)) = queue.pop_front() {
352 let my_number = parent_num;
353 let mut children: Vec<_> = dir
354 .dirs
355 .iter()
356 .filter(|child| {
357 !matches!(
358 child.relocation,
359 DirectoryRelocation::Placeholder { .. }
360 )
361 })
362 .map(|child| (child, self.ty.convert_directory_name(&child.name)))
363 .collect();
364 children.sort_by(|(_, left), (_, right)| left.as_bytes().cmp(right.as_bytes()));
365 for (child_dir, name) in children {
366 current_number += 1;
367 let name_bytes = name.as_bytes();
368 let extent = child_dir.entries.get(&self.ty).unwrap().extent;
369 write_pt_record(data, &self.endian, my_number, extent, name_bytes).await?;
370 queue.push_back((child_dir, current_number));
371 }
372 }
373 Ok(())
374 }
375}
376
377} #[cfg(all(test, feature = "std"))]
380mod tests {
381 use super::*;
382 use alloc::format;
383
384 #[test]
385 fn test_convert_l1() {
386 let orig = "this-is-the-original-file.@very-long-ext";
387 let converted = convert_l1(orig, false);
388 assert_eq!(converted.as_str(), "THIS_IS_._VE;1");
389 let converted = convert_l1(orig, true);
390 assert_eq!(converted.as_str(), "this_is_._ve;1");
391 }
392
393 #[test]
394 fn test_convert_l2_short_name() {
395 let orig = "readme.txt";
396 let converted = convert_l2(orig, false);
397 assert_eq!(converted.as_str(), "README.TXT;1");
398 }
399
400 #[test]
401 fn test_convert_l2_long_name_truncation() {
402 let orig = "this-is-a-very-long-filename-that-should-be-truncated.extension";
404 let converted = convert_l2(orig, false);
405 assert!(
407 converted.len() <= 32,
408 "L2 name too long: {}",
409 converted.len()
410 );
411 assert!(converted.as_str().ends_with(";1"));
412 }
413
414 #[test]
415 fn test_convert_l2_no_extension() {
416 let orig = "this-is-a-very-long-directory-name-without-extension";
417 let converted = convert_l2(orig, false);
418 assert!(
419 converted.len() <= 32,
420 "L2 name too long: {}",
421 converted.len()
422 );
423 assert!(converted.as_str().ends_with(";1"));
424 assert_eq!(converted.as_str(), "THIS_IS_A_VERY_LONG_DIRECTORY_;1");
426 }
427
428 #[test]
429 fn test_convert_l3_short_name() {
430 let orig = "readme.txt";
431 let converted = convert_l3(orig, false);
432 assert_eq!(converted.as_str(), "README.TXT");
433 }
434
435 #[test]
436 fn test_convert_l3_long_name_truncation() {
437 let long_name = "a".repeat(250);
439 let converted = convert_l3(&long_name, false);
440 assert!(
441 converted.len() <= 207,
442 "L3 name too long: {}",
443 converted.len()
444 );
445 assert_eq!(converted.len(), 207);
446 }
447
448 #[test]
449 fn test_convert_l3_with_extension() {
450 let basename = "a".repeat(200);
452 let orig = format!("{basename}.txt");
453 let converted = convert_l3(&orig, false);
454 assert!(
455 converted.len() <= 207,
456 "L3 name too long: {}",
457 converted.len()
458 );
459 }
460
461 #[test]
464 fn test_convert_l1_empty_extension() {
465 let converted = convert_l1("file.", false);
466 assert_eq!(converted.as_str(), "FILE.;1");
467 }
468
469 #[test]
470 fn test_convert_l1_dot_only() {
471 let converted = convert_l1(".", false);
472 assert_eq!(converted.as_str(), ".;1");
473 }
474
475 #[test]
476 fn test_convert_l1_dot_dot() {
477 let converted = convert_l1("..", false);
479 assert_eq!(converted.as_str(), "._;1");
480 }
481
482 #[test]
483 fn test_convert_l1_no_dot() {
484 let converted = convert_l1("README", false);
485 assert_eq!(converted.as_str(), "README;1");
486 }
487
488 #[test]
489 fn test_convert_l1_no_dot_long() {
490 let converted = convert_l1("LONGFILENAME", false);
491 assert_eq!(converted.as_str(), "LONGFILE;1");
492 }
493
494 #[test]
495 fn test_convert_l1_exact_8_3() {
496 let converted = convert_l1("12345678.abc", false);
497 assert_eq!(converted.as_str(), "12345678.ABC;1");
498 }
499
500 #[test]
501 fn test_convert_l1_oversized() {
502 let converted = convert_l1("longname1.longext", false);
503 assert_eq!(converted.as_str(), "LONGNAME.LON;1");
504 }
505
506 #[test]
507 fn test_convert_l1_single_char() {
508 let converted = convert_l1("a.b", false);
509 assert_eq!(converted.as_str(), "A.B;1");
510 }
511
512 #[test]
513 fn test_convert_l1_multibyte_utf8() {
514 let converted = convert_l1("café.txt", false);
516 assert!(converted.len() <= 14, "L1 overflow: {}", converted.len());
518 assert!(converted.as_str().ends_with(";1"));
519 }
520
521 #[test]
524 fn test_convert_l2_empty_extension() {
525 let converted = convert_l2("file.", false);
526 assert_eq!(converted.as_str(), "FILE.;1");
527 }
528
529 #[test]
530 fn test_convert_l2_no_dot() {
531 let converted = convert_l2("README", false);
532 assert_eq!(converted.as_str(), "README;1");
533 }
534
535 #[test]
536 fn test_convert_l2_single_char() {
537 let converted = convert_l2("a.b", false);
538 assert_eq!(converted.as_str(), "A.B;1");
539 }
540
541 #[test]
544 fn test_convert_l3_empty_extension() {
545 let converted = convert_l3("file.", false);
546 assert_eq!(converted.as_str(), "FILE.");
547 }
548
549 #[test]
550 fn test_convert_l3_no_dot() {
551 let converted = convert_l3("README", false);
552 assert_eq!(converted.as_str(), "README");
553 }
554
555 #[test]
556 fn test_convert_l3_single_char() {
557 let converted = convert_l3("a.b", false);
558 assert_eq!(converted.as_str(), "A.B");
559 }
560
561 #[test]
564 fn test_convert_joliet3_short_name() {
565 let converted = convert_joliet3("readme.txt");
566 assert_eq!(converted.len(), 20);
568 }
569
570 #[test]
571 fn test_convert_joliet3_long_name_truncation() {
572 let long_name = "a".repeat(150);
574 let converted = convert_joliet3(&long_name);
575 assert_eq!(converted.len(), 128);
577 }
578
579 #[test]
580 fn test_convert_joliet3_multibyte_utf8() {
581 let converted = convert_joliet3("café.txt");
583 assert_eq!(converted.len(), 16);
585 }
586}