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::types::{BlockDevice, FsFlags, FsType};
9
10/// Main AFFS filesystem reader.
11///
12/// Provides read-only access to an AFFS/OFS filesystem image.
13///
14/// # Example
15///
16/// ```ignore
17/// use affs_read::{AffsReader, BlockDevice};
18///
19/// struct MyDevice { data: Vec<u8> }
20///
21/// impl BlockDevice for MyDevice {
22///     fn read_block(&self, block: u32, buf: &mut [u8; 512]) -> Result<(), ()> {
23///         let offset = block as usize * 512;
24///         buf.copy_from_slice(&self.data[offset..offset + 512]);
25///         Ok(())
26///     }
27/// }
28///
29/// let device = MyDevice { data: adf_data };
30/// let reader = AffsReader::new(&device)?;
31///
32/// // Get disk info
33/// println!("Disk: {:?}", reader.disk_name());
34/// println!("Type: {:?}", reader.fs_type());
35///
36/// // List root directory
37/// for entry in reader.read_dir(reader.root_block())? {
38///     let entry = entry?;
39///     println!("{:?}: {} bytes", entry.name(), entry.size);
40/// }
41/// ```
42pub struct AffsReader<'a, D: BlockDevice> {
43    device: &'a D,
44    /// Boot block info.
45    boot: BootBlock,
46    /// Root block info.
47    root: RootBlock,
48    /// Calculated root block number.
49    root_block: u32,
50    /// Total blocks on device.
51    total_blocks: u32,
52}
53
54impl<'a, D: BlockDevice> AffsReader<'a, D> {
55    /// Create a new AFFS reader for a standard DD floppy (880KB).
56    pub fn new(device: &'a D) -> Result<Self> {
57        Self::with_size(device, FLOPPY_DD_SECTORS)
58    }
59
60    /// Create a new AFFS reader for an HD floppy (1.76MB).
61    pub fn new_hd(device: &'a D) -> Result<Self> {
62        Self::with_size(device, FLOPPY_HD_SECTORS)
63    }
64
65    /// Create a new AFFS reader with a specific block count.
66    pub fn with_size(device: &'a D, total_blocks: u32) -> Result<Self> {
67        // Read boot block (2 sectors)
68        let mut boot_buf = [0u8; BOOT_BLOCK_SIZE];
69        device
70            .read_block(0, array_ref_mut(&mut boot_buf, 0))
71            .map_err(|()| AffsError::BlockReadError)?;
72        device
73            .read_block(1, array_ref_mut(&mut boot_buf, BLOCK_SIZE))
74            .map_err(|()| AffsError::BlockReadError)?;
75
76        let boot = BootBlock::parse(&boot_buf)?;
77
78        // Calculate root block position (middle of disk)
79        let root_block = if boot.root_block != 0 {
80            boot.root_block
81        } else {
82            total_blocks / 2
83        };
84
85        // Validate root block is in range
86        if root_block >= total_blocks {
87            return Err(AffsError::BlockOutOfRange);
88        }
89
90        // Read root block
91        let mut root_buf = [0u8; BLOCK_SIZE];
92        device
93            .read_block(root_block, &mut root_buf)
94            .map_err(|()| AffsError::BlockReadError)?;
95
96        let root = RootBlock::parse(&root_buf)?;
97
98        Ok(Self {
99            device,
100            boot,
101            root,
102            root_block,
103            total_blocks,
104        })
105    }
106
107    /// Get the filesystem type (OFS or FFS).
108    #[inline]
109    pub const fn fs_type(&self) -> FsType {
110        self.boot.fs_type()
111    }
112
113    /// Get filesystem flags.
114    #[inline]
115    pub const fn fs_flags(&self) -> FsFlags {
116        self.boot.fs_flags()
117    }
118
119    /// Check if international mode is enabled.
120    #[inline]
121    pub const fn is_intl(&self) -> bool {
122        self.boot.fs_flags().intl
123    }
124
125    /// Get the root block number.
126    #[inline]
127    pub const fn root_block(&self) -> u32 {
128        self.root_block
129    }
130
131    /// Get the total number of blocks.
132    #[inline]
133    pub const fn total_blocks(&self) -> u32 {
134        self.total_blocks
135    }
136
137    /// Get the disk name as bytes.
138    #[inline]
139    pub fn disk_name(&self) -> &[u8] {
140        self.root.name()
141    }
142
143    /// Get the disk name as a string (if valid UTF-8).
144    #[inline]
145    pub fn disk_name_str(&self) -> Option<&str> {
146        core::str::from_utf8(self.disk_name()).ok()
147    }
148
149    /// Check if the bitmap is valid.
150    #[inline]
151    pub const fn bitmap_valid(&self) -> bool {
152        self.root.bitmap_valid()
153    }
154
155    /// Get the root directory hash table.
156    #[inline]
157    pub fn root_hash_table(&self) -> &[u32; HASH_TABLE_SIZE] {
158        &self.root.hash_table
159    }
160
161    /// Get a reference to the block device.
162    #[inline]
163    pub const fn device(&self) -> &'a D {
164        self.device
165    }
166
167    /// Iterate over entries in the root directory.
168    pub fn read_root_dir(&self) -> DirIter<'_, D> {
169        DirIter::new(self.device, self.root.hash_table, self.is_intl())
170    }
171
172    /// Iterate over entries in a directory.
173    ///
174    /// # Arguments
175    /// * `block` - Block number of the directory entry
176    pub fn read_dir(&self, block: u32) -> Result<DirIter<'_, D>> {
177        if block == self.root_block {
178            return Ok(self.read_root_dir());
179        }
180
181        let mut buf = [0u8; BLOCK_SIZE];
182        self.device
183            .read_block(block, &mut buf)
184            .map_err(|()| AffsError::BlockReadError)?;
185
186        let entry = EntryBlock::parse(&buf)?;
187
188        if !entry.is_dir() {
189            return Err(AffsError::NotADirectory);
190        }
191
192        Ok(DirIter::new(self.device, entry.hash_table, self.is_intl()))
193    }
194
195    /// Find an entry by name in a directory.
196    ///
197    /// # Arguments
198    /// * `dir_block` - Block number of the directory
199    /// * `name` - Name to search for
200    pub fn find_entry(&self, dir_block: u32, name: &[u8]) -> Result<DirEntry> {
201        let dir = self.read_dir(dir_block)?;
202        dir.find(name)
203    }
204
205    /// Find an entry by path from the root.
206    ///
207    /// Path components are separated by '/'.
208    pub fn find_path(&self, path: &[u8]) -> Result<DirEntry> {
209        let mut current_block = self.root_block;
210        let mut final_entry: Option<DirEntry> = None;
211
212        for component in path.split(|&b| b == b'/') {
213            if component.is_empty() {
214                continue;
215            }
216
217            let entry = self.find_entry(current_block, component)?;
218
219            if entry.is_dir() {
220                current_block = entry.block;
221            }
222
223            final_entry = Some(entry);
224        }
225
226        final_entry.ok_or(AffsError::EntryNotFound)
227    }
228
229    /// Read a file's contents.
230    ///
231    /// # Arguments
232    /// * `block` - Block number of the file header
233    pub fn read_file(&self, block: u32) -> Result<FileReader<'_, D>> {
234        FileReader::new(self.device, self.fs_type(), block)
235    }
236
237    /// Read an entry block.
238    pub fn read_entry(&self, block: u32) -> Result<EntryBlock> {
239        let mut buf = [0u8; BLOCK_SIZE];
240        self.device
241            .read_block(block, &mut buf)
242            .map_err(|()| AffsError::BlockReadError)?;
243        EntryBlock::parse(&buf)
244    }
245
246    /// Get a DirEntry for the root directory.
247    pub fn root_entry(&self) -> DirEntry {
248        DirEntry::from_root(&self.root, self.root_block)
249    }
250}
251
252/// Helper to get a mutable array reference from a slice.
253#[inline]
254fn array_ref_mut(slice: &mut [u8], offset: usize) -> &mut [u8; BLOCK_SIZE] {
255    (&mut slice[offset..offset + BLOCK_SIZE])
256        .try_into()
257        .expect("slice size mismatch")
258}
259
260// Extension of DirEntry to support root
261impl crate::dir::DirEntry {
262    /// Create a DirEntry representing the root directory.
263    pub(crate) fn from_root(root: &RootBlock, block: u32) -> Self {
264        let mut name = [0u8; MAX_NAME_LEN];
265        let name_len = root.name_len.min(MAX_NAME_LEN as u8);
266        name[..name_len as usize].copy_from_slice(&root.disk_name[..name_len as usize]);
267
268        Self {
269            name,
270            name_len,
271            entry_type: crate::types::EntryType::Root,
272            block,
273            parent: 0,
274            size: 0,
275            access: crate::types::Access::new(0),
276            date: root.last_modified,
277            real_entry: 0,
278            comment: [0u8; MAX_COMMENT_LEN],
279            comment_len: 0,
280        }
281    }
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287
288    struct DummyDevice;
289
290    impl BlockDevice for DummyDevice {
291        fn read_block(&self, _block: u32, _buf: &mut [u8; 512]) -> core::result::Result<(), ()> {
292            Err(())
293        }
294    }
295
296    #[test]
297    fn test_reader_error_on_bad_device() {
298        let device = DummyDevice;
299        let result = AffsReader::new(&device);
300        assert!(result.is_err());
301    }
302}