affs_read/
reader.rs

1//! Main AFFS reader interface.
2
3use crate::block::{BootBlock, EntryBlock, RootBlock};
4use crate::constants::*;
5use crate::dir::{DirEntry, DirIter};
6use crate::error::{AffsError, Result};
7use crate::file::FileReader;
8use crate::symlink::read_symlink_target;
9use crate::types::{BlockDevice, EntryType, FsFlags, FsType};
10
11/// Main AFFS filesystem reader.
12///
13/// Provides read-only access to an AFFS/OFS filesystem image.
14///
15/// # Example
16///
17/// ```ignore
18/// use affs_read::{AffsReader, BlockDevice};
19///
20/// struct MyDevice { data: Vec<u8> }
21///
22/// impl BlockDevice for MyDevice {
23///     fn read_block(&self, block: u32, buf: &mut [u8; 512]) -> Result<(), ()> {
24///         let offset = block as usize * 512;
25///         buf.copy_from_slice(&self.data[offset..offset + 512]);
26///         Ok(())
27///     }
28/// }
29///
30/// let device = MyDevice { data: adf_data };
31/// let reader = AffsReader::new(&device)?;
32///
33/// // Get disk info
34/// println!("Disk: {:?}", reader.disk_name());
35/// println!("Type: {:?}", reader.fs_type());
36///
37/// // List root directory
38/// for entry in reader.read_dir(reader.root_block())? {
39///     let entry = entry?;
40///     println!("{:?}: {} bytes", entry.name(), entry.size);
41/// }
42/// ```
43pub struct AffsReader<'a, D: BlockDevice> {
44    device: &'a D,
45    /// Boot block info.
46    boot: BootBlock,
47    /// Root block info.
48    root: RootBlock,
49    /// Calculated root block number.
50    root_block: u32,
51    /// Total blocks on device.
52    total_blocks: u32,
53}
54
55impl<'a, D: BlockDevice> AffsReader<'a, D> {
56    /// Create a new AFFS reader for a standard DD floppy (880KB).
57    pub fn new(device: &'a D) -> Result<Self> {
58        Self::with_size(device, FLOPPY_DD_SECTORS)
59    }
60
61    /// Create a new AFFS reader for an HD floppy (1.76MB).
62    pub fn new_hd(device: &'a D) -> Result<Self> {
63        Self::with_size(device, FLOPPY_HD_SECTORS)
64    }
65
66    /// Create a new AFFS reader with a specific block count.
67    pub fn with_size(device: &'a D, total_blocks: u32) -> Result<Self> {
68        // Read boot block (2 sectors)
69        let mut boot_buf = [0u8; BOOT_BLOCK_SIZE];
70        device
71            .read_block(0, array_ref_mut(&mut boot_buf, 0))
72            .map_err(|()| AffsError::BlockReadError)?;
73        device
74            .read_block(1, array_ref_mut(&mut boot_buf, BLOCK_SIZE))
75            .map_err(|()| AffsError::BlockReadError)?;
76
77        let boot = BootBlock::parse(&boot_buf)?;
78
79        // Calculate root block position (middle of disk)
80        let root_block = if boot.root_block != 0 {
81            boot.root_block
82        } else {
83            total_blocks / 2
84        };
85
86        // Validate root block is in range
87        if root_block >= total_blocks {
88            return Err(AffsError::BlockOutOfRange);
89        }
90
91        // Read root block
92        let mut root_buf = [0u8; BLOCK_SIZE];
93        device
94            .read_block(root_block, &mut root_buf)
95            .map_err(|()| AffsError::BlockReadError)?;
96
97        let root = RootBlock::parse(&root_buf)?;
98
99        Ok(Self {
100            device,
101            boot,
102            root,
103            root_block,
104            total_blocks,
105        })
106    }
107
108    /// Get the filesystem type (OFS or FFS).
109    #[inline]
110    pub const fn fs_type(&self) -> FsType {
111        self.boot.fs_type()
112    }
113
114    /// Get filesystem flags.
115    #[inline]
116    pub const fn fs_flags(&self) -> FsFlags {
117        self.boot.fs_flags()
118    }
119
120    /// Check if international mode is enabled.
121    #[inline]
122    pub const fn is_intl(&self) -> bool {
123        self.boot.fs_flags().intl
124    }
125
126    /// Get the root block number.
127    #[inline]
128    pub const fn root_block(&self) -> u32 {
129        self.root_block
130    }
131
132    /// Get the total number of blocks.
133    #[inline]
134    pub const fn total_blocks(&self) -> u32 {
135        self.total_blocks
136    }
137
138    /// Get the disk name as bytes.
139    #[inline]
140    pub fn disk_name(&self) -> &[u8] {
141        self.root.name()
142    }
143
144    /// Get the disk name as a string (if valid UTF-8).
145    #[inline]
146    pub fn disk_name_str(&self) -> Option<&str> {
147        crate::utf8::from_utf8(self.disk_name())
148    }
149
150    /// Get the volume label (alias for disk_name).
151    ///
152    /// This matches GRUB's `grub_affs_label()` behavior.
153    #[inline]
154    pub fn label(&self) -> &[u8] {
155        self.disk_name()
156    }
157
158    /// Get the volume label as string (alias for disk_name_str).
159    #[inline]
160    pub fn label_str(&self) -> Option<&str> {
161        self.disk_name_str()
162    }
163
164    /// Get the volume creation date.
165    #[inline]
166    pub fn creation_date(&self) -> crate::date::AmigaDate {
167        self.root.creation_date
168    }
169
170    /// Get the volume last modification date.
171    #[inline]
172    pub fn last_modified(&self) -> crate::date::AmigaDate {
173        self.root.last_modified
174    }
175
176    /// Get the volume modification time as Unix timestamp.
177    ///
178    /// This matches GRUB's `grub_affs_mtime()` behavior:
179    /// - days * 86400 + min * 60 + hz / 50 + epoch offset
180    #[inline]
181    pub fn mtime(&self) -> i64 {
182        self.root.last_modified.to_unix_timestamp()
183    }
184
185    /// Check if the bitmap is valid.
186    #[inline]
187    pub const fn bitmap_valid(&self) -> bool {
188        self.root.bitmap_valid()
189    }
190
191    /// Get the root directory hash table.
192    #[inline]
193    pub fn root_hash_table(&self) -> &[u32; HASH_TABLE_SIZE] {
194        &self.root.hash_table
195    }
196
197    /// Get a reference to the block device.
198    #[inline]
199    pub const fn device(&self) -> &'a D {
200        self.device
201    }
202
203    /// Iterate over entries in the root directory.
204    pub fn read_root_dir(&self) -> DirIter<'_, D> {
205        DirIter::new(self.device, self.root.hash_table, self.is_intl())
206    }
207
208    /// Iterate over entries in a directory.
209    ///
210    /// # Arguments
211    /// * `block` - Block number of the directory entry
212    pub fn read_dir(&self, block: u32) -> Result<DirIter<'_, D>> {
213        if block == self.root_block {
214            return Ok(self.read_root_dir());
215        }
216
217        let mut buf = [0u8; BLOCK_SIZE];
218        self.device
219            .read_block(block, &mut buf)
220            .map_err(|()| AffsError::BlockReadError)?;
221
222        let entry = EntryBlock::parse(&buf)?;
223
224        if !entry.is_dir() {
225            return Err(AffsError::NotADirectory);
226        }
227
228        Ok(DirIter::new(self.device, entry.hash_table, self.is_intl()))
229    }
230
231    /// Find an entry by name in a directory.
232    ///
233    /// # Arguments
234    /// * `dir_block` - Block number of the directory
235    /// * `name` - Name to search for
236    pub fn find_entry(&self, dir_block: u32, name: &[u8]) -> Result<DirEntry> {
237        let dir = self.read_dir(dir_block)?;
238        dir.find(name)
239    }
240
241    /// Find an entry by path from the root.
242    ///
243    /// Path components are separated by '/'.
244    pub fn find_path(&self, path: &[u8]) -> Result<DirEntry> {
245        let mut current_block = self.root_block;
246        let mut final_entry: Option<DirEntry> = None;
247
248        let mut start = 0;
249        while start < path.len() {
250            let end = memchr::memchr(b'/', &path[start..])
251                .map(|pos| start + pos)
252                .unwrap_or(path.len());
253
254            let component = &path[start..end];
255            if !component.is_empty() {
256                let entry = self.find_entry(current_block, component)?;
257
258                if entry.is_dir() {
259                    current_block = entry.block;
260                }
261
262                final_entry = Some(entry);
263            }
264
265            start = end + 1;
266        }
267
268        final_entry.ok_or(AffsError::EntryNotFound)
269    }
270
271    /// Read a file's contents.
272    ///
273    /// # Arguments
274    /// * `block` - Block number of the file header
275    pub fn read_file(&self, block: u32) -> Result<FileReader<'_, D>> {
276        FileReader::new(self.device, self.fs_type(), block)
277    }
278
279    /// Read an entry block.
280    pub fn read_entry(&self, block: u32) -> Result<EntryBlock> {
281        let mut buf = [0u8; BLOCK_SIZE];
282        self.device
283            .read_block(block, &mut buf)
284            .map_err(|()| AffsError::BlockReadError)?;
285        EntryBlock::parse(&buf)
286    }
287
288    /// Read a symlink target.
289    ///
290    /// # Arguments
291    /// * `block` - Block number of the symlink entry
292    /// * `out` - Buffer to write the UTF-8 symlink target into
293    ///
294    /// # Returns
295    /// The number of bytes written to `out`.
296    ///
297    /// # Notes
298    /// - The symlink target is stored as Latin1 and converted to UTF-8
299    /// - Leading `:` (Amiga volume reference) is replaced with `/`
300    /// - The output buffer should be at least `MAX_SYMLINK_LEN * 2` bytes
301    ///   to handle worst-case Latin1 to UTF-8 expansion
302    pub fn read_symlink(&self, block: u32, out: &mut [u8]) -> Result<usize> {
303        let mut buf = [0u8; BLOCK_SIZE];
304        self.device
305            .read_block(block, &mut buf)
306            .map_err(|()| AffsError::BlockReadError)?;
307
308        // Verify this is a symlink
309        let entry = EntryBlock::parse(&buf)?;
310        if entry.entry_type() != Some(EntryType::SoftLink) {
311            return Err(AffsError::NotASymlink);
312        }
313
314        Ok(read_symlink_target(&buf, out))
315    }
316
317    /// Read a symlink target from a DirEntry.
318    ///
319    /// Convenience method that takes a DirEntry instead of a block number.
320    pub fn read_symlink_entry(&self, entry: &DirEntry, out: &mut [u8]) -> Result<usize> {
321        if !entry.is_symlink() {
322            return Err(AffsError::NotASymlink);
323        }
324        self.read_symlink(entry.block, out)
325    }
326
327    /// Get a DirEntry for the root directory.
328    pub fn root_entry(&self) -> DirEntry {
329        DirEntry::from_root(&self.root, self.root_block)
330    }
331}
332
333/// Helper to get a mutable array reference from a slice.
334#[inline]
335fn array_ref_mut(slice: &mut [u8], offset: usize) -> &mut [u8; BLOCK_SIZE] {
336    (&mut slice[offset..offset + BLOCK_SIZE])
337        .try_into()
338        .expect("slice size mismatch")
339}
340
341// Extension of DirEntry to support root
342impl crate::dir::DirEntry {
343    /// Create a DirEntry representing the root directory.
344    pub(crate) fn from_root(root: &RootBlock, block: u32) -> Self {
345        let mut name = [0u8; MAX_NAME_LEN];
346        let name_len = root.name_len.min(MAX_NAME_LEN as u8);
347        name[..name_len as usize].copy_from_slice(&root.disk_name[..name_len as usize]);
348
349        Self {
350            name,
351            name_len,
352            entry_type: crate::types::EntryType::Root,
353            block,
354            parent: 0,
355            size: 0,
356            access: crate::types::Access::new(0),
357            date: root.last_modified,
358            real_entry: 0,
359            comment: [0u8; MAX_COMMENT_LEN],
360            comment_len: 0,
361        }
362    }
363}
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368
369    struct DummyDevice;
370
371    impl BlockDevice for DummyDevice {
372        fn read_block(&self, _block: u32, _buf: &mut [u8; 512]) -> core::result::Result<(), ()> {
373            Err(())
374        }
375    }
376
377    #[test]
378    fn test_reader_error_on_bad_device() {
379        let device = DummyDevice;
380        let result = AffsReader::new(&device);
381        assert!(result.is_err());
382    }
383}