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        crate::utf8::from_utf8(self.name())
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        crate::utf8::from_utf8(self.comment())
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    /// Check if this is a symlink.
101    #[inline]
102    pub const fn is_symlink(&self) -> bool {
103        matches!(self.entry_type, EntryType::SoftLink)
104    }
105}
106
107/// Iterator over directory entries.
108///
109/// This iterator reads entries lazily from the hash table.
110pub struct DirIter<'a, D: BlockDevice> {
111    device: &'a D,
112    hash_table: [u32; HASH_TABLE_SIZE],
113    hash_index: usize,
114    current_chain: u32,
115    intl: bool,
116    buf: [u8; BLOCK_SIZE],
117}
118
119impl<'a, D: BlockDevice> DirIter<'a, D> {
120    /// Create a new directory iterator.
121    pub(crate) fn new(device: &'a D, hash_table: [u32; HASH_TABLE_SIZE], intl: bool) -> Self {
122        Self {
123            device,
124            hash_table,
125            hash_index: 0,
126            current_chain: 0,
127            intl,
128            buf: [0u8; BLOCK_SIZE],
129        }
130    }
131
132    /// Find an entry by name in this directory.
133    pub fn find(mut self, name: &[u8]) -> Result<DirEntry> {
134        if name.len() > MAX_NAME_LEN {
135            return Err(AffsError::NameTooLong);
136        }
137
138        let hash = hash_name(name, self.intl);
139        let mut block = self.hash_table[hash];
140
141        while block != 0 {
142            self.device
143                .read_block(block, &mut self.buf)
144                .map_err(|()| AffsError::BlockReadError)?;
145
146            let entry = EntryBlock::parse(&self.buf)?;
147
148            if names_equal(entry.name(), name, self.intl) {
149                return DirEntry::from_entry_block(block, &entry).ok_or(AffsError::InvalidSecType);
150            }
151
152            block = entry.next_same_hash;
153        }
154
155        Err(AffsError::EntryNotFound)
156    }
157}
158
159impl<D: BlockDevice> Iterator for DirIter<'_, D> {
160    type Item = Result<DirEntry>;
161
162    fn next(&mut self) -> Option<Self::Item> {
163        loop {
164            // If we're in a hash chain, continue it
165            if self.current_chain != 0 {
166                let result = self.device.read_block(self.current_chain, &mut self.buf);
167                if result.is_err() {
168                    return Some(Err(AffsError::BlockReadError));
169                }
170
171                match EntryBlock::parse(&self.buf) {
172                    Ok(entry) => {
173                        let block = self.current_chain;
174                        self.current_chain = entry.next_same_hash;
175
176                        match DirEntry::from_entry_block(block, &entry) {
177                            Some(dir_entry) => return Some(Ok(dir_entry)),
178                            None => continue, // Skip invalid entries
179                        }
180                    }
181                    Err(e) => return Some(Err(e)),
182                }
183            }
184
185            // Find next non-empty hash slot
186            while self.hash_index < HASH_TABLE_SIZE {
187                let block = self.hash_table[self.hash_index];
188                self.hash_index += 1;
189
190                if block != 0 {
191                    self.current_chain = block;
192                    break;
193                }
194            }
195
196            // No more entries
197            if self.current_chain == 0 {
198                return None;
199            }
200        }
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    #[test]
209    fn test_dir_entry_name() {
210        let mut entry = DirEntry {
211            name: [0u8; MAX_NAME_LEN],
212            name_len: 4,
213            entry_type: EntryType::File,
214            block: 100,
215            parent: 880,
216            size: 1024,
217            access: Access::new(0),
218            date: AmigaDate::default(),
219            real_entry: 0,
220            comment: [0u8; MAX_COMMENT_LEN],
221            comment_len: 0,
222        };
223        entry.name[..4].copy_from_slice(b"test");
224
225        assert_eq!(entry.name(), b"test");
226        assert_eq!(entry.name_str(), Some("test"));
227    }
228}