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