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        core::str::from_utf8(self.disk_name()).ok()
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        for component in path.split(|&b| b == b'/') {
249            if component.is_empty() {
250                continue;
251            }
252
253            let entry = self.find_entry(current_block, component)?;
254
255            if entry.is_dir() {
256                current_block = entry.block;
257            }
258
259            final_entry = Some(entry);
260        }
261
262        final_entry.ok_or(AffsError::EntryNotFound)
263    }
264
265    /// Read a file's contents.
266    ///
267    /// # Arguments
268    /// * `block` - Block number of the file header
269    pub fn read_file(&self, block: u32) -> Result<FileReader<'_, D>> {
270        FileReader::new(self.device, self.fs_type(), block)
271    }
272
273    /// Read an entry block.
274    pub fn read_entry(&self, block: u32) -> Result<EntryBlock> {
275        let mut buf = [0u8; BLOCK_SIZE];
276        self.device
277            .read_block(block, &mut buf)
278            .map_err(|()| AffsError::BlockReadError)?;
279        EntryBlock::parse(&buf)
280    }
281
282    /// Read a symlink target.
283    ///
284    /// # Arguments
285    /// * `block` - Block number of the symlink entry
286    /// * `out` - Buffer to write the UTF-8 symlink target into
287    ///
288    /// # Returns
289    /// The number of bytes written to `out`.
290    ///
291    /// # Notes
292    /// - The symlink target is stored as Latin1 and converted to UTF-8
293    /// - Leading `:` (Amiga volume reference) is replaced with `/`
294    /// - The output buffer should be at least `MAX_SYMLINK_LEN * 2` bytes
295    ///   to handle worst-case Latin1 to UTF-8 expansion
296    pub fn read_symlink(&self, block: u32, out: &mut [u8]) -> Result<usize> {
297        let mut buf = [0u8; BLOCK_SIZE];
298        self.device
299            .read_block(block, &mut buf)
300            .map_err(|()| AffsError::BlockReadError)?;
301
302        // Verify this is a symlink
303        let entry = EntryBlock::parse(&buf)?;
304        if entry.entry_type() != Some(EntryType::SoftLink) {
305            return Err(AffsError::NotASymlink);
306        }
307
308        Ok(read_symlink_target(&buf, out))
309    }
310
311    /// Read a symlink target from a DirEntry.
312    ///
313    /// Convenience method that takes a DirEntry instead of a block number.
314    pub fn read_symlink_entry(&self, entry: &DirEntry, out: &mut [u8]) -> Result<usize> {
315        if !entry.is_symlink() {
316            return Err(AffsError::NotASymlink);
317        }
318        self.read_symlink(entry.block, out)
319    }
320
321    /// Get a DirEntry for the root directory.
322    pub fn root_entry(&self) -> DirEntry {
323        DirEntry::from_root(&self.root, self.root_block)
324    }
325}
326
327/// Helper to get a mutable array reference from a slice.
328#[inline]
329fn array_ref_mut(slice: &mut [u8], offset: usize) -> &mut [u8; BLOCK_SIZE] {
330    (&mut slice[offset..offset + BLOCK_SIZE])
331        .try_into()
332        .expect("slice size mismatch")
333}
334
335// Extension of DirEntry to support root
336impl crate::dir::DirEntry {
337    /// Create a DirEntry representing the root directory.
338    pub(crate) fn from_root(root: &RootBlock, block: u32) -> Self {
339        let mut name = [0u8; MAX_NAME_LEN];
340        let name_len = root.name_len.min(MAX_NAME_LEN as u8);
341        name[..name_len as usize].copy_from_slice(&root.disk_name[..name_len as usize]);
342
343        Self {
344            name,
345            name_len,
346            entry_type: crate::types::EntryType::Root,
347            block,
348            parent: 0,
349            size: 0,
350            access: crate::types::Access::new(0),
351            date: root.last_modified,
352            real_entry: 0,
353            comment: [0u8; MAX_COMMENT_LEN],
354            comment_len: 0,
355        }
356    }
357}
358
359#[cfg(test)]
360mod tests {
361    use super::*;
362
363    struct DummyDevice;
364
365    impl BlockDevice for DummyDevice {
366        fn read_block(&self, _block: u32, _buf: &mut [u8; 512]) -> core::result::Result<(), ()> {
367            Err(())
368        }
369    }
370
371    #[test]
372    fn test_reader_error_on_bad_device() {
373        let device = DummyDevice;
374        let result = AffsReader::new(&device);
375        assert!(result.is_err());
376    }
377}