a2fuse/prodos/
directory.rs1use std::collections::HashSet;
2
3use crate::error::{A2FuseError, Result};
4
5use super::block::BlockDevice;
6use super::path::decode_filename_with_case;
7use super::types::{AccessFlags, ProdosTimestamp, StorageType};
8
9pub const PRODOS_ENTRY_LENGTH: usize = 39;
10pub const PRODOS_ENTRIES_PER_BLOCK: usize = 13;
11
12#[derive(Clone, Debug, Eq, PartialEq)]
13pub struct DirectoryEntry {
14 pub name: String,
15 pub storage_type: StorageType,
16 pub file_type: u8,
17 pub key_pointer: u16,
18 pub blocks_used: u16,
19 pub eof: u32,
20 pub creation: Option<ProdosTimestamp>,
21 pub modification: Option<ProdosTimestamp>,
22 pub access: AccessFlags,
23 pub aux_type: u16,
24 pub header_pointer: u16,
25}
26
27impl DirectoryEntry {
28 pub fn parse(bytes: &[u8]) -> Result<Option<Self>> {
29 if bytes.len() < PRODOS_ENTRY_LENGTH {
30 return Err(A2FuseError::InvalidDirectoryEntry(format!(
31 "expected {PRODOS_ENTRY_LENGTH} bytes, got {}",
32 bytes.len()
33 )));
34 }
35
36 let storage_nibble = bytes[0] >> 4;
37 let name_length = usize::from(bytes[0] & 0x0f);
38 if storage_nibble == 0 {
39 return Ok(None);
40 }
41 if name_length == 0 || name_length > 15 {
42 return Err(A2FuseError::InvalidDirectoryEntry(format!(
43 "invalid filename length {name_length}"
44 )));
45 }
46 let storage_type = StorageType::from_nibble(storage_nibble).ok_or_else(|| {
47 A2FuseError::InvalidDirectoryEntry(format!("unknown storage type {storage_nibble:#x}"))
48 })?;
49
50 Ok(Some(Self {
51 name: decode_filename_with_case(&bytes[1..1 + name_length], read_u16(bytes, 0x1c)),
52 storage_type,
53 file_type: bytes[0x10],
54 key_pointer: read_u16(bytes, 0x11),
55 blocks_used: read_u16(bytes, 0x13),
56 eof: read_u24(bytes, 0x15),
57 creation: ProdosTimestamp::decode(read_u16(bytes, 0x18), read_u16(bytes, 0x1a)),
58 access: AccessFlags(bytes[0x1e]),
59 aux_type: read_u16(bytes, 0x1f),
60 modification: ProdosTimestamp::decode(read_u16(bytes, 0x21), read_u16(bytes, 0x23)),
61 header_pointer: read_u16(bytes, 0x25),
62 }))
63 }
64
65 pub fn is_directory(&self) -> bool {
66 self.storage_type == StorageType::Subdirectory
67 }
68
69 pub fn is_file(&self) -> bool {
70 self.storage_type.is_regular_file()
71 }
72}
73
74#[derive(Clone, Debug, Eq, PartialEq)]
75pub struct Directory {
76 pub key_block: u16,
77 pub entries: Vec<DirectoryEntry>,
78}
79
80impl Directory {
81 pub fn read(device: &BlockDevice, key_block: u16) -> Result<Self> {
82 let mut entries = Vec::new();
83 let mut block_number = key_block;
84 let mut visited = HashSet::new();
85 let mut first_block = true;
86
87 while block_number != 0 {
88 if !visited.insert(block_number) {
89 return Err(A2FuseError::InvalidDirectory(format!(
90 "directory block chain contains a cycle at block {block_number}"
91 )));
92 }
93
94 let block = device.read_block(block_number)?;
95 let next_block = u16::from_le_bytes([block[2], block[3]]);
96
97 for slot in 0..PRODOS_ENTRIES_PER_BLOCK {
98 if first_block && slot == 0 {
99 continue;
100 }
101 let start = 4 + slot * PRODOS_ENTRY_LENGTH;
102 let end = start + PRODOS_ENTRY_LENGTH;
103 if let Some(entry) = DirectoryEntry::parse(&block[start..end])?
104 && !matches!(
105 entry.storage_type,
106 StorageType::VolumeHeader | StorageType::SubdirectoryHeader
107 )
108 {
109 entries.push(entry);
110 }
111 }
112
113 first_block = false;
114 block_number = next_block;
115 }
116
117 Ok(Self { key_block, entries })
118 }
119}
120
121fn read_u16(bytes: &[u8], offset: usize) -> u16 {
122 u16::from_le_bytes([bytes[offset], bytes[offset + 1]])
123}
124
125fn read_u24(bytes: &[u8], offset: usize) -> u32 {
126 u32::from(bytes[offset])
127 | (u32::from(bytes[offset + 1]) << 8)
128 | (u32::from(bytes[offset + 2]) << 16)
129}