affs_read/
dir.rs

1//! Directory traversal.
2
3use crate::block::{EntryBlock, hash_name, names_equal};
4use crate::constants::*;
5use crate::date::AmigaDate;
6use crate::error::{AffsError, Result};
7use crate::types::{Access, BlockDevice, EntryType};
8
9/// Directory entry information.
10#[derive(Debug, Clone)]
11pub struct DirEntry {
12    /// Entry name (up to 30 bytes).
13    pub(crate) name: [u8; MAX_NAME_LEN],
14    /// Name length.
15    pub(crate) name_len: u8,
16    /// Entry type.
17    pub entry_type: EntryType,
18    /// Block number of this entry.
19    pub block: u32,
20    /// Parent block number.
21    pub parent: u32,
22    /// File size (0 for directories).
23    pub size: u32,
24    /// Access permissions.
25    pub access: Access,
26    /// Last modification date.
27    pub date: AmigaDate,
28    /// Real entry (for hard links).
29    pub real_entry: u32,
30    /// Comment (if any).
31    pub(crate) comment: [u8; MAX_COMMENT_LEN],
32    /// Comment length.
33    pub(crate) comment_len: u8,
34}
35
36impl DirEntry {
37    /// Create from an entry block.
38    pub(crate) fn from_entry_block(block_num: u32, entry: &EntryBlock) -> Option<Self> {
39        let entry_type = entry.entry_type()?;
40
41        let mut name = [0u8; MAX_NAME_LEN];
42        let name_len = entry.name_len.min(MAX_NAME_LEN as u8);
43        name[..name_len as usize].copy_from_slice(&entry.name[..name_len as usize]);
44
45        let mut comment = [0u8; MAX_COMMENT_LEN];
46        let comment_len = entry.comment_len.min(MAX_COMMENT_LEN as u8);
47        comment[..comment_len as usize].copy_from_slice(&entry.comment[..comment_len as usize]);
48
49        Some(Self {
50            name,
51            name_len,
52            entry_type,
53            block: block_num,
54            parent: entry.parent,
55            size: entry.byte_size,
56            access: Access::new(entry.access),
57            date: entry.date,
58            real_entry: entry.real_entry,
59            comment,
60            comment_len,
61        })
62    }
63
64    /// Get entry name as byte slice.
65    #[inline]
66    pub fn name(&self) -> &[u8] {
67        &self.name[..self.name_len as usize]
68    }
69
70    /// Get entry name as str (if valid UTF-8).
71    #[inline]
72    pub fn name_str(&self) -> Option<&str> {
73        core::str::from_utf8(self.name()).ok()
74    }
75
76    /// Get comment as byte slice.
77    #[inline]
78    pub fn comment(&self) -> &[u8] {
79        &self.comment[..self.comment_len as usize]
80    }
81
82    /// Get comment as str (if valid UTF-8).
83    #[inline]
84    pub fn comment_str(&self) -> Option<&str> {
85        core::str::from_utf8(self.comment()).ok()
86    }
87
88    /// Check if this is a directory.
89    #[inline]
90    pub const fn is_dir(&self) -> bool {
91        self.entry_type.is_dir()
92    }
93
94    /// Check if this is a file.
95    #[inline]
96    pub const fn is_file(&self) -> bool {
97        self.entry_type.is_file()
98    }
99}
100
101/// Iterator over directory entries.
102///
103/// This iterator reads entries lazily from the hash table.
104pub struct DirIter<'a, D: BlockDevice> {
105    device: &'a D,
106    hash_table: [u32; HASH_TABLE_SIZE],
107    hash_index: usize,
108    current_chain: u32,
109    intl: bool,
110    buf: [u8; BLOCK_SIZE],
111}
112
113impl<'a, D: BlockDevice> DirIter<'a, D> {
114    /// Create a new directory iterator.
115    pub(crate) fn new(device: &'a D, hash_table: [u32; HASH_TABLE_SIZE], intl: bool) -> Self {
116        Self {
117            device,
118            hash_table,
119            hash_index: 0,
120            current_chain: 0,
121            intl,
122            buf: [0u8; BLOCK_SIZE],
123        }
124    }
125
126    /// Find an entry by name in this directory.
127    pub fn find(mut self, name: &[u8]) -> Result<DirEntry> {
128        if name.len() > MAX_NAME_LEN {
129            return Err(AffsError::NameTooLong);
130        }
131
132        let hash = hash_name(name, self.intl);
133        let mut block = self.hash_table[hash];
134
135        while block != 0 {
136            self.device
137                .read_block(block, &mut self.buf)
138                .map_err(|()| AffsError::BlockReadError)?;
139
140            let entry = EntryBlock::parse(&self.buf)?;
141
142            if names_equal(entry.name(), name, self.intl) {
143                return DirEntry::from_entry_block(block, &entry).ok_or(AffsError::InvalidSecType);
144            }
145
146            block = entry.next_same_hash;
147        }
148
149        Err(AffsError::EntryNotFound)
150    }
151}
152
153impl<D: BlockDevice> Iterator for DirIter<'_, D> {
154    type Item = Result<DirEntry>;
155
156    fn next(&mut self) -> Option<Self::Item> {
157        loop {
158            // If we're in a hash chain, continue it
159            if self.current_chain != 0 {
160                let result = self.device.read_block(self.current_chain, &mut self.buf);
161                if result.is_err() {
162                    return Some(Err(AffsError::BlockReadError));
163                }
164
165                match EntryBlock::parse(&self.buf) {
166                    Ok(entry) => {
167                        let block = self.current_chain;
168                        self.current_chain = entry.next_same_hash;
169
170                        match DirEntry::from_entry_block(block, &entry) {
171                            Some(dir_entry) => return Some(Ok(dir_entry)),
172                            None => continue, // Skip invalid entries
173                        }
174                    }
175                    Err(e) => return Some(Err(e)),
176                }
177            }
178
179            // Find next non-empty hash slot
180            while self.hash_index < HASH_TABLE_SIZE {
181                let block = self.hash_table[self.hash_index];
182                self.hash_index += 1;
183
184                if block != 0 {
185                    self.current_chain = block;
186                    break;
187                }
188            }
189
190            // No more entries
191            if self.current_chain == 0 {
192                return None;
193            }
194        }
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    #[test]
203    fn test_dir_entry_name() {
204        let mut entry = DirEntry {
205            name: [0u8; MAX_NAME_LEN],
206            name_len: 4,
207            entry_type: EntryType::File,
208            block: 100,
209            parent: 880,
210            size: 1024,
211            access: Access::new(0),
212            date: AmigaDate::default(),
213            real_entry: 0,
214            comment: [0u8; MAX_COMMENT_LEN],
215            comment_len: 0,
216        };
217        entry.name[..4].copy_from_slice(b"test");
218
219        assert_eq!(entry.name(), b"test");
220        assert_eq!(entry.name_str(), Some("test"));
221    }
222}