affs_read/
varblock.rs

1//! Variable block size AFFS reader for hard disk partitions.
2//!
3//! AFFS on hard disks can use block sizes of 512, 1024, 2048, 4096, or 8192 bytes.
4//! The block size is not stored in the filesystem and must be determined by
5//! probing: try reading the root block at each possible block size until
6//! the checksum validates.
7
8use crate::checksum::{boot_sum, normal_sum_slice, read_i32_be_slice, read_u32_be_slice};
9use crate::constants::*;
10use crate::date::AmigaDate;
11use crate::error::{AffsError, Result};
12use crate::symlink::read_symlink_target_with_block_size;
13use crate::types::{EntryType, FsFlags, FsType, SectorDevice};
14
15/// Maximum block size supported (8192 bytes = 16 sectors).
16pub const MAX_BLOCK_SIZE: usize = 8192;
17
18/// Variable block size AFFS reader.
19///
20/// This reader supports AFFS filesystems with block sizes from 512 to 8192 bytes,
21/// as used on Amiga hard disk partitions. The block size is determined by probing
22/// the root block at different sizes until the checksum validates.
23pub struct AffsReaderVar<'a, D: SectorDevice> {
24    device: &'a D,
25    /// Filesystem type (OFS or FFS).
26    fs_type: FsType,
27    /// Filesystem flags.
28    fs_flags: FsFlags,
29    /// Root block number (in filesystem blocks).
30    root_block: u32,
31    /// Total blocks on device.
32    total_blocks: u32,
33    /// Log2 of block size relative to 512 (0=512, 1=1024, ..., 4=8192).
34    log_blocksize: u8,
35    /// Actual block size in bytes.
36    block_size: usize,
37    /// Hash table size (entries per directory).
38    hash_table_size: u32,
39    /// Boot block sector offset (0 or 1).
40    #[allow(dead_code)]
41    boot_sector: u32,
42    /// Disk name from root block.
43    disk_name: [u8; MAX_NAME_LEN],
44    /// Disk name length.
45    disk_name_len: u8,
46    /// Volume creation date.
47    creation_date: AmigaDate,
48    /// Volume last modification date.
49    last_modified: AmigaDate,
50}
51
52/// Probe result for mount operation.
53struct ProbeResult {
54    fs_type: FsType,
55    fs_flags: FsFlags,
56    root_block: u32,
57    log_blocksize: u8,
58    block_size: usize,
59    hash_table_size: u32,
60    boot_sector: u32,
61    disk_name: [u8; MAX_NAME_LEN],
62    disk_name_len: u8,
63    creation_date: AmigaDate,
64    last_modified: AmigaDate,
65}
66
67impl<'a, D: SectorDevice> AffsReaderVar<'a, D> {
68    /// Create a new variable block size AFFS reader.
69    ///
70    /// This probes the filesystem to determine the block size by trying
71    /// different block sizes until the root block checksum validates.
72    ///
73    /// # Arguments
74    /// * `device` - Sector device to read from
75    /// * `total_sectors` - Total number of 512-byte sectors on the device
76    pub fn new(device: &'a D, total_sectors: u64) -> Result<Self> {
77        let result = Self::probe(device, total_sectors)?;
78
79        Ok(Self {
80            device,
81            fs_type: result.fs_type,
82            fs_flags: result.fs_flags,
83            root_block: result.root_block,
84            total_blocks: (total_sectors >> result.log_blocksize) as u32,
85            log_blocksize: result.log_blocksize,
86            block_size: result.block_size,
87            hash_table_size: result.hash_table_size,
88            boot_sector: result.boot_sector,
89            disk_name: result.disk_name,
90            disk_name_len: result.disk_name_len,
91            creation_date: result.creation_date,
92            last_modified: result.last_modified,
93        })
94    }
95
96    /// Probe the filesystem to determine block size.
97    fn probe(device: &'a D, _total_sectors: u64) -> Result<ProbeResult> {
98        // Buffer for reading - we need max block size
99        let mut buf = [0u8; MAX_BLOCK_SIZE];
100
101        // Try boot block at sector 0 and sector 1
102        for boot_sector in 0..=MAX_BOOT_BLOCK {
103            // Read boot block (2 sectors)
104            if Self::read_sectors(device, boot_sector as u64, &mut buf[..BOOT_BLOCK_SIZE]).is_err()
105            {
106                continue;
107            }
108
109            // Check DOS signature
110            if &buf[0..3] != b"DOS" {
111                continue;
112            }
113
114            // Check FFS flag (we only support FFS like GRUB)
115            let flags = buf[3];
116            if (flags & DOSFS_FFS) == 0 {
117                continue; // OFS not supported for variable block size
118            }
119
120            let fs_type = FsType::Ffs;
121            let fs_flags = FsFlags::from_dos_type(flags);
122
123            // Verify boot checksum if boot code is present
124            if buf[12] != 0 {
125                let checksum = read_u32_be_slice(&buf, 4);
126                let boot_buf: &[u8; BOOT_BLOCK_SIZE] = buf[..BOOT_BLOCK_SIZE].try_into().unwrap();
127                let calculated = boot_sum(boot_buf);
128                if checksum != calculated {
129                    continue;
130                }
131            }
132
133            let root_block_num = read_u32_be_slice(&buf, 8);
134
135            // Try each block size
136            for log_blocksize in 0..=MAX_LOG_BLOCK_SIZE {
137                let block_size = 512usize << log_blocksize;
138
139                // Read root block
140                let root_sector = (root_block_num as u64) << log_blocksize;
141                if Self::read_sectors(device, root_sector, &mut buf[..block_size]).is_err() {
142                    continue;
143                }
144
145                // Validate root block type
146                let block_type = read_i32_be_slice(&buf, 0);
147                if block_type != T_HEADER {
148                    continue;
149                }
150
151                // Validate secondary type (at end of block)
152                let sec_type = read_i32_be_slice(&buf, block_size - 4);
153                if sec_type != ST_ROOT {
154                    continue;
155                }
156
157                // Validate hash table size
158                let hash_table_size = read_u32_be_slice(&buf, 12);
159                if hash_table_size == 0 {
160                    continue;
161                }
162
163                // Validate checksum
164                let checksum = read_u32_be_slice(&buf, 20);
165                let calculated = normal_sum_slice(&buf[..block_size], 20);
166                if checksum != calculated {
167                    continue;
168                }
169
170                // Parse root block data
171                let name_offset = block_size - FILE_LOCATION + 108; // 0x1B0 relative to end
172                let name_len = buf[name_offset].min(MAX_NAME_LEN as u8);
173                let mut disk_name = [0u8; MAX_NAME_LEN];
174                disk_name[..name_len as usize]
175                    .copy_from_slice(&buf[name_offset + 1..name_offset + 1 + name_len as usize]);
176
177                // Creation date is at offset 0x1A4 from start in 512-byte block
178                // For variable blocks, it's at block_size - FILE_LOCATION + 0x1A4 - (512 - FILE_LOCATION)
179                // Actually for root block, dates are at fixed offsets from end
180                let date_offset = block_size - FILE_LOCATION + 0x1A4 - (BLOCK_SIZE - FILE_LOCATION);
181                let creation_date = AmigaDate::new(
182                    read_i32_be_slice(&buf, date_offset),
183                    read_i32_be_slice(&buf, date_offset + 4),
184                    read_i32_be_slice(&buf, date_offset + 8),
185                );
186
187                let mod_offset = block_size - FILE_LOCATION + 0x1D8 - (BLOCK_SIZE - FILE_LOCATION);
188                let last_modified = AmigaDate::new(
189                    read_i32_be_slice(&buf, mod_offset),
190                    read_i32_be_slice(&buf, mod_offset + 4),
191                    read_i32_be_slice(&buf, mod_offset + 8),
192                );
193
194                return Ok(ProbeResult {
195                    fs_type,
196                    fs_flags,
197                    root_block: root_block_num,
198                    log_blocksize,
199                    block_size,
200                    hash_table_size,
201                    boot_sector,
202                    disk_name,
203                    disk_name_len: name_len,
204                    creation_date,
205                    last_modified,
206                });
207            }
208        }
209
210        Err(AffsError::InvalidDosType)
211    }
212
213    /// Read multiple sectors into a buffer.
214    fn read_sectors(device: &D, start_sector: u64, buf: &mut [u8]) -> Result<()> {
215        let num_sectors = buf.len() / BLOCK_SIZE;
216        let mut sector_buf = [0u8; BLOCK_SIZE];
217
218        for i in 0..num_sectors {
219            device
220                .read_sector(start_sector + i as u64, &mut sector_buf)
221                .map_err(|()| AffsError::BlockReadError)?;
222            buf[i * BLOCK_SIZE..(i + 1) * BLOCK_SIZE].copy_from_slice(&sector_buf);
223        }
224
225        Ok(())
226    }
227
228    /// Read a filesystem block into a buffer.
229    fn read_block_into(&self, block: u32, buf: &mut [u8]) -> Result<()> {
230        let start_sector = (block as u64) << self.log_blocksize;
231        Self::read_sectors(self.device, start_sector, &mut buf[..self.block_size])
232    }
233
234    /// Get the filesystem type (OFS or FFS).
235    #[inline]
236    pub const fn fs_type(&self) -> FsType {
237        self.fs_type
238    }
239
240    /// Get filesystem flags.
241    #[inline]
242    pub const fn fs_flags(&self) -> FsFlags {
243        self.fs_flags
244    }
245
246    /// Get the root block number.
247    #[inline]
248    pub const fn root_block(&self) -> u32 {
249        self.root_block
250    }
251
252    /// Get the total number of blocks.
253    #[inline]
254    pub const fn total_blocks(&self) -> u32 {
255        self.total_blocks
256    }
257
258    /// Get the block size in bytes.
259    #[inline]
260    pub const fn block_size(&self) -> usize {
261        self.block_size
262    }
263
264    /// Get the log2 block size (relative to 512).
265    #[inline]
266    pub const fn log_blocksize(&self) -> u8 {
267        self.log_blocksize
268    }
269
270    /// Get the disk name (volume label) as bytes.
271    #[inline]
272    pub fn disk_name(&self) -> &[u8] {
273        &self.disk_name[..self.disk_name_len as usize]
274    }
275
276    /// Get the disk name (volume label) as a string (if valid UTF-8).
277    #[inline]
278    pub fn disk_name_str(&self) -> Option<&str> {
279        core::str::from_utf8(self.disk_name()).ok()
280    }
281
282    /// Get the volume label (alias for disk_name).
283    #[inline]
284    pub fn label(&self) -> &[u8] {
285        self.disk_name()
286    }
287
288    /// Get the volume label as string (alias for disk_name_str).
289    #[inline]
290    pub fn label_str(&self) -> Option<&str> {
291        self.disk_name_str()
292    }
293
294    /// Get the volume creation date.
295    #[inline]
296    pub const fn creation_date(&self) -> AmigaDate {
297        self.creation_date
298    }
299
300    /// Get the volume last modification date.
301    #[inline]
302    pub const fn last_modified(&self) -> AmigaDate {
303        self.last_modified
304    }
305
306    /// Get the volume modification time as Unix timestamp.
307    ///
308    /// This matches GRUB's `grub_affs_mtime()` behavior:
309    /// - days * 86400 + min * 60 + hz / 50 + epoch offset
310    #[inline]
311    pub fn mtime(&self) -> i64 {
312        self.last_modified.to_unix_timestamp()
313    }
314
315    /// Get the hash table size.
316    #[inline]
317    pub const fn hash_table_size(&self) -> u32 {
318        self.hash_table_size
319    }
320
321    /// Check if international mode is enabled.
322    #[inline]
323    pub const fn is_intl(&self) -> bool {
324        self.fs_flags.intl || self.fs_flags.dircache
325    }
326
327    /// Read a symlink target.
328    ///
329    /// # Arguments
330    /// * `block` - Block number of the symlink entry
331    /// * `out` - Buffer to write the UTF-8 symlink target into
332    ///
333    /// # Returns
334    /// The number of bytes written to `out`.
335    pub fn read_symlink(&self, block: u32, out: &mut [u8]) -> Result<usize> {
336        let mut buf = [0u8; MAX_BLOCK_SIZE];
337        self.read_block_into(block, &mut buf)?;
338
339        // Verify this is a symlink
340        let sec_type = read_i32_be_slice(&buf, self.block_size - 4);
341        if sec_type != ST_LSOFT {
342            return Err(AffsError::NotASymlink);
343        }
344
345        Ok(read_symlink_target_with_block_size(
346            &buf[..self.block_size],
347            self.block_size,
348            out,
349        ))
350    }
351
352    /// Iterate over entries in the root directory.
353    pub fn read_root_dir(&self) -> Result<VarDirIter<'_, D>> {
354        let mut buf = [0u8; MAX_BLOCK_SIZE];
355        self.read_block_into(self.root_block, &mut buf)?;
356
357        // Read hash table
358        let mut hash_table = [0u32; 256]; // Max possible hash table size
359        let ht_size = self.hash_table_size as usize;
360        for (i, slot) in hash_table.iter_mut().enumerate().take(ht_size.min(256)) {
361            *slot = read_u32_be_slice(&buf, SYMLINK_OFFSET + i * 4);
362        }
363
364        Ok(VarDirIter::new(
365            self.device,
366            hash_table,
367            ht_size,
368            self.is_intl(),
369            self.log_blocksize,
370            self.block_size,
371        ))
372    }
373
374    /// Iterate over entries in a directory.
375    pub fn read_dir(&self, block: u32) -> Result<VarDirIter<'_, D>> {
376        if block == self.root_block {
377            return self.read_root_dir();
378        }
379
380        let mut buf = [0u8; MAX_BLOCK_SIZE];
381        self.read_block_into(block, &mut buf)?;
382
383        // Validate block type
384        let block_type = read_i32_be_slice(&buf, 0);
385        if block_type != T_HEADER {
386            return Err(AffsError::InvalidBlockType);
387        }
388
389        // Validate this is a directory
390        let sec_type = read_i32_be_slice(&buf, self.block_size - 4);
391        if sec_type != ST_DIR && sec_type != ST_LDIR {
392            return Err(AffsError::NotADirectory);
393        }
394
395        // Read hash table
396        let mut hash_table = [0u32; 256];
397        let ht_size = self.hash_table_size as usize;
398        for (i, slot) in hash_table.iter_mut().enumerate().take(ht_size.min(256)) {
399            *slot = read_u32_be_slice(&buf, SYMLINK_OFFSET + i * 4);
400        }
401
402        Ok(VarDirIter::new(
403            self.device,
404            hash_table,
405            ht_size,
406            self.is_intl(),
407            self.log_blocksize,
408            self.block_size,
409        ))
410    }
411}
412
413/// Directory entry for variable block size filesystem.
414#[derive(Debug, Clone)]
415pub struct VarDirEntry {
416    /// Entry name.
417    pub name: [u8; MAX_NAME_LEN],
418    /// Name length.
419    pub name_len: u8,
420    /// Entry type.
421    pub entry_type: EntryType,
422    /// Block number.
423    pub block: u32,
424    /// Parent block.
425    pub parent: u32,
426    /// File size.
427    pub size: u32,
428    /// Modification date.
429    pub date: AmigaDate,
430}
431
432impl VarDirEntry {
433    /// Get entry name as bytes.
434    #[inline]
435    pub fn name(&self) -> &[u8] {
436        &self.name[..self.name_len as usize]
437    }
438
439    /// Get entry name as string.
440    #[inline]
441    pub fn name_str(&self) -> Option<&str> {
442        core::str::from_utf8(self.name()).ok()
443    }
444
445    /// Check if this is a directory.
446    #[inline]
447    pub const fn is_dir(&self) -> bool {
448        self.entry_type.is_dir()
449    }
450
451    /// Check if this is a file.
452    #[inline]
453    pub const fn is_file(&self) -> bool {
454        self.entry_type.is_file()
455    }
456
457    /// Check if this is a symlink.
458    #[inline]
459    pub const fn is_symlink(&self) -> bool {
460        matches!(self.entry_type, EntryType::SoftLink)
461    }
462}
463
464/// Directory iterator for variable block size filesystem.
465pub struct VarDirIter<'a, D: SectorDevice> {
466    device: &'a D,
467    hash_table: [u32; 256],
468    hash_table_size: usize,
469    hash_index: usize,
470    current_chain: u32,
471    #[allow(dead_code)]
472    intl: bool,
473    log_blocksize: u8,
474    block_size: usize,
475    buf: [u8; MAX_BLOCK_SIZE],
476}
477
478impl<'a, D: SectorDevice> VarDirIter<'a, D> {
479    fn new(
480        device: &'a D,
481        hash_table: [u32; 256],
482        hash_table_size: usize,
483        intl: bool,
484        log_blocksize: u8,
485        block_size: usize,
486    ) -> Self {
487        Self {
488            device,
489            hash_table,
490            hash_table_size,
491            hash_index: 0,
492            current_chain: 0,
493            intl,
494            log_blocksize,
495            block_size,
496            buf: [0u8; MAX_BLOCK_SIZE],
497        }
498    }
499
500    fn read_block_into(&mut self, block: u32) -> Result<()> {
501        let start_sector = (block as u64) << self.log_blocksize;
502        let num_sectors = 1usize << self.log_blocksize;
503        let mut sector_buf = [0u8; BLOCK_SIZE];
504
505        for i in 0..num_sectors {
506            self.device
507                .read_sector(start_sector + i as u64, &mut sector_buf)
508                .map_err(|()| AffsError::BlockReadError)?;
509            self.buf[i * BLOCK_SIZE..(i + 1) * BLOCK_SIZE].copy_from_slice(&sector_buf);
510        }
511
512        Ok(())
513    }
514
515    fn parse_entry(&self, block: u32) -> Option<VarDirEntry> {
516        let buf = &self.buf[..self.block_size];
517
518        // Entry type is at end of block - 4
519        let sec_type = read_i32_be_slice(buf, self.block_size - 4);
520        let entry_type = EntryType::from_sec_type(sec_type)?;
521
522        // Name is at block_size - FILE_LOCATION + offset
523        let name_offset = self.block_size - FILE_LOCATION + 108;
524        let name_len = buf[name_offset].min(MAX_NAME_LEN as u8);
525        let mut name = [0u8; MAX_NAME_LEN];
526        name[..name_len as usize]
527            .copy_from_slice(&buf[name_offset + 1..name_offset + 1 + name_len as usize]);
528
529        // Size at offset 0x144 relative to start in standard block
530        // For variable blocks: block_size - FILE_LOCATION + 12
531        let size_offset = self.block_size - FILE_LOCATION + 12;
532        let size = read_u32_be_slice(buf, size_offset);
533
534        // Parent at block_size - 12
535        let parent = read_u32_be_slice(buf, self.block_size - 12);
536
537        // Date at block_size - FILE_LOCATION + 0x1A4 - (512 - FILE_LOCATION)
538        let date_offset = self.block_size - FILE_LOCATION + 0x1A4 - (BLOCK_SIZE - FILE_LOCATION);
539        let date = AmigaDate::new(
540            read_i32_be_slice(buf, date_offset),
541            read_i32_be_slice(buf, date_offset + 4),
542            read_i32_be_slice(buf, date_offset + 8),
543        );
544
545        Some(VarDirEntry {
546            name,
547            name_len,
548            entry_type,
549            block,
550            parent,
551            size,
552            date,
553        })
554    }
555}
556
557impl<D: SectorDevice> Iterator for VarDirIter<'_, D> {
558    type Item = Result<VarDirEntry>;
559
560    fn next(&mut self) -> Option<Self::Item> {
561        loop {
562            // If we're in a hash chain, continue it
563            if self.current_chain != 0 {
564                if let Err(e) = self.read_block_into(self.current_chain) {
565                    return Some(Err(e));
566                }
567
568                let block = self.current_chain;
569
570                // Next in chain at block_size - 16
571                self.current_chain = read_u32_be_slice(&self.buf, self.block_size - 16);
572
573                if let Some(entry) = self.parse_entry(block) {
574                    return Some(Ok(entry));
575                }
576                continue;
577            }
578
579            // Find next non-empty hash slot
580            while self.hash_index < self.hash_table_size {
581                let block = self.hash_table[self.hash_index];
582                self.hash_index += 1;
583
584                if block != 0 {
585                    self.current_chain = block;
586                    break;
587                }
588            }
589
590            if self.current_chain == 0 {
591                return None;
592            }
593        }
594    }
595}
596
597#[cfg(test)]
598mod tests {
599    use super::*;
600
601    struct DummySectorDevice;
602
603    impl SectorDevice for DummySectorDevice {
604        fn read_sector(&self, _sector: u64, _buf: &mut [u8; 512]) -> core::result::Result<(), ()> {
605            Err(())
606        }
607    }
608
609    #[test]
610    fn test_var_reader_error_on_bad_device() {
611        let device = DummySectorDevice;
612        let result = AffsReaderVar::new(&device, 1760);
613        assert!(result.is_err());
614    }
615
616    /// Good device that returns a valid boot block and root block and one
617    /// directory entry block so we can exercise probing and iteration.
618    struct DummyGoodDevice;
619
620    impl DummyGoodDevice {
621        fn write_u32_be(buf: &mut [u8], offset: usize, val: u32) {
622            let bytes = val.to_be_bytes();
623            buf[offset..offset + 4].copy_from_slice(&bytes);
624        }
625
626        fn write_i32_be(buf: &mut [u8], offset: usize, val: i32) {
627            let bytes = val.to_be_bytes();
628            buf[offset..offset + 4].copy_from_slice(&bytes);
629        }
630    }
631
632    impl SectorDevice for DummyGoodDevice {
633        fn read_sector(&self, sector: u64, buf: &mut [u8; 512]) -> core::result::Result<(), ()> {
634            // Sector mapping:
635            // 0..=1 -> boot block (1024 bytes split)
636            // 2 -> root block (512 bytes)
637            // 5 -> directory entry block (512 bytes)
638            for b in buf.iter_mut() {
639                *b = 0;
640            }
641
642            match sector {
643                0 => {
644                    // First half of boot block
645                    let mut boot = [0u8; 1024];
646                    boot.fill(0);
647                    boot[0..3].copy_from_slice(b"DOS");
648                    boot[3] = DOSFS_FFS; // FFS flag
649                    // buf[12] = 0 => skip boot checksum validation
650                    DummyGoodDevice::write_u32_be(&mut boot, 8, 2); // root block = 2
651                    buf.copy_from_slice(&boot[0..512]);
652                    Ok(())
653                }
654                1 => {
655                    // Second half of boot block
656                    let mut boot = [0u8; 1024];
657                    boot.fill(0);
658                    boot[0..3].copy_from_slice(b"DOS");
659                    boot[3] = DOSFS_FFS;
660                    DummyGoodDevice::write_u32_be(&mut boot, 8, 2);
661                    buf.copy_from_slice(&boot[512..1024]);
662                    Ok(())
663                }
664                2 => {
665                    // Root block (512 bytes)
666                    let mut rb = [0u8; 512];
667                    rb.fill(0);
668                    // Block type header
669                    DummyGoodDevice::write_i32_be(&mut rb, 0, T_HEADER);
670                    // hash table size at offset 12
671                    DummyGoodDevice::write_u32_be(&mut rb, 12, 4);
672                    // We'll set checksum at offset 20 later
673                    // Secondary type at end
674                    DummyGoodDevice::write_i32_be(&mut rb, 512 - 4, ST_ROOT);
675                    // Set hash table first slot to point to block 5 at SYMLINK_OFFSET
676                    DummyGoodDevice::write_u32_be(&mut rb, SYMLINK_OFFSET, 5);
677                    // Name offset and name
678                    let name_offset = 512 - FILE_LOCATION + 108;
679                    rb[name_offset] = 4; // length
680                    rb[name_offset + 1..name_offset + 1 + 4].copy_from_slice(b"test");
681                    // Date fields (three i32) - leave zero
682                    // Calculate checksum excluding offset 20
683                    let checksum = normal_sum_slice(&rb[..512], 20);
684                    DummyGoodDevice::write_u32_be(&mut rb, 20, checksum);
685                    buf.copy_from_slice(&rb);
686                    Ok(())
687                }
688                5 => {
689                    // Directory entry block for block number 5
690                    let mut eb = [0u8; 512];
691                    eb.fill(0);
692                    DummyGoodDevice::write_i32_be(&mut eb, 0, T_HEADER);
693                    // Secondary type -> file
694                    DummyGoodDevice::write_i32_be(&mut eb, 512 - 4, ST_FILE);
695                    // Name
696                    let name_offset = 512 - FILE_LOCATION + 108;
697                    eb[name_offset] = 4;
698                    eb[name_offset + 1..name_offset + 1 + 4].copy_from_slice(b"file");
699                    // Size at size_offset = block_size - FILE_LOCATION + 12
700                    let size_offset = 512 - FILE_LOCATION + 12;
701                    DummyGoodDevice::write_u32_be(&mut eb, size_offset, 123);
702                    // Parent at block_size - 12
703                    DummyGoodDevice::write_u32_be(&mut eb, 512 - 12, 2);
704                    buf.copy_from_slice(&eb);
705                    Ok(())
706                }
707                _ => Err(()),
708            }
709        }
710    }
711
712    #[test]
713    fn test_var_probe_and_dir_iter() {
714        let device = DummyGoodDevice;
715        // total sectors arbitrary but >= 6
716        let reader = AffsReaderVar::new(&device, 100).expect("probe should succeed");
717        assert_eq!(reader.block_size(), 512);
718        assert_eq!(reader.root_block(), 2);
719        assert_eq!(reader.disk_name_str(), Some("test"));
720
721        // Read root dir and iterate
722        let mut iter = reader.read_root_dir().expect("read_root_dir");
723        let first = iter.next().expect("entry").expect("ok entry");
724        assert_eq!(first.name_str(), Some("file"));
725        assert_eq!(first.size, 123);
726        assert_eq!(first.block, 5);
727    }
728}