affs_read/
block.rs

1//! Block structure parsing.
2
3use crate::checksum::{boot_sum, normal_sum, read_i32_be, read_u32_be, read_u32_be_slice};
4use crate::constants::*;
5use crate::date::AmigaDate;
6use crate::error::{AffsError, Result};
7use crate::types::{EntryType, FsFlags, FsType};
8
9/// Parsed boot block.
10#[derive(Debug, Clone)]
11pub struct BootBlock {
12    /// DOS type bytes ("DOS\x00" - "DOS\x07").
13    pub dos_type: [u8; 4],
14    /// Checksum.
15    pub checksum: u32,
16    /// Root block number.
17    pub root_block: u32,
18}
19
20impl BootBlock {
21    /// Parse boot block from raw data (1024 bytes).
22    pub fn parse(buf: &[u8; BOOT_BLOCK_SIZE]) -> Result<Self> {
23        let dos_type = [buf[0], buf[1], buf[2], buf[3]];
24
25        // Check for "DOS" signature
26        if &dos_type[0..3] != b"DOS" {
27            return Err(AffsError::InvalidDosType);
28        }
29
30        let checksum = read_u32_be_slice(buf, 4);
31        let root_block = read_u32_be_slice(buf, 8);
32
33        // Verify checksum if boot code is present
34        if buf[12] != 0 {
35            let calculated = boot_sum(buf);
36            if checksum != calculated {
37                return Err(AffsError::ChecksumMismatch);
38            }
39        }
40
41        Ok(Self {
42            dos_type,
43            checksum,
44            root_block,
45        })
46    }
47
48    /// Get filesystem type (OFS or FFS).
49    #[inline]
50    pub const fn fs_type(&self) -> FsType {
51        if (self.dos_type[3] & DOSFS_FFS) != 0 {
52            FsType::Ffs
53        } else {
54            FsType::Ofs
55        }
56    }
57
58    /// Get filesystem flags.
59    #[inline]
60    pub const fn fs_flags(&self) -> FsFlags {
61        FsFlags::from_dos_type(self.dos_type[3])
62    }
63}
64
65/// Parsed root block.
66#[derive(Debug, Clone)]
67pub struct RootBlock {
68    /// Block type (should be T_HEADER).
69    pub block_type: i32,
70    /// Hash table size (always 72).
71    pub hash_table_size: i32,
72    /// Checksum.
73    pub checksum: u32,
74    /// Hash table entries.
75    pub hash_table: [u32; HASH_TABLE_SIZE],
76    /// Bitmap valid flag (-1 = valid).
77    pub bm_flag: i32,
78    /// Bitmap block pointers.
79    pub bm_pages: [u32; BM_PAGES_ROOT_SIZE],
80    /// Bitmap extension block.
81    pub bm_ext: u32,
82    /// Creation date.
83    pub creation_date: AmigaDate,
84    /// Disk name length.
85    pub name_len: u8,
86    /// Disk name (up to 30 chars).
87    pub disk_name: [u8; MAX_NAME_LEN],
88    /// Last modification date.
89    pub last_modified: AmigaDate,
90    /// Directory cache extension (FFS only).
91    pub extension: u32,
92    /// Secondary type (should be ST_ROOT).
93    pub sec_type: i32,
94}
95
96impl RootBlock {
97    /// Parse root block from raw data.
98    pub fn parse(buf: &[u8; BLOCK_SIZE]) -> Result<Self> {
99        let block_type = read_i32_be(buf, 0);
100        if block_type != T_HEADER {
101            return Err(AffsError::InvalidBlockType);
102        }
103
104        let sec_type = read_i32_be(buf, 508);
105        if sec_type != ST_ROOT {
106            return Err(AffsError::InvalidSecType);
107        }
108
109        let checksum = read_u32_be(buf, 20);
110        let calculated = normal_sum(buf, 20);
111        if checksum != calculated {
112            return Err(AffsError::ChecksumMismatch);
113        }
114
115        let hash_table_size = read_i32_be(buf, 12);
116
117        let mut hash_table = [0u32; HASH_TABLE_SIZE];
118        for (i, entry) in hash_table.iter_mut().enumerate() {
119            *entry = read_u32_be(buf, 24 + i * 4);
120        }
121
122        let bm_flag = read_i32_be(buf, 0x138);
123
124        let mut bm_pages = [0u32; BM_PAGES_ROOT_SIZE];
125        for (i, page) in bm_pages.iter_mut().enumerate() {
126            *page = read_u32_be(buf, 0x13C + i * 4);
127        }
128
129        let bm_ext = read_u32_be(buf, 0x1A0);
130
131        let creation_date = AmigaDate::new(
132            read_i32_be(buf, 0x1A4),
133            read_i32_be(buf, 0x1A8),
134            read_i32_be(buf, 0x1AC),
135        );
136
137        let name_len = buf[0x1B0].min(MAX_NAME_LEN as u8);
138        let mut disk_name = [0u8; MAX_NAME_LEN];
139        disk_name[..name_len as usize].copy_from_slice(&buf[0x1B1..0x1B1 + name_len as usize]);
140
141        let last_modified = AmigaDate::new(
142            read_i32_be(buf, 0x1D8),
143            read_i32_be(buf, 0x1DC),
144            read_i32_be(buf, 0x1E0),
145        );
146
147        let extension = read_u32_be(buf, 0x1F8);
148
149        Ok(Self {
150            block_type,
151            hash_table_size,
152            checksum,
153            hash_table,
154            bm_flag,
155            bm_pages,
156            bm_ext,
157            creation_date,
158            name_len,
159            disk_name,
160            last_modified,
161            extension,
162            sec_type,
163        })
164    }
165
166    /// Get disk name as string slice.
167    #[inline]
168    pub fn name(&self) -> &[u8] {
169        &self.disk_name[..self.name_len as usize]
170    }
171
172    /// Check if bitmap is valid.
173    #[inline]
174    pub const fn bitmap_valid(&self) -> bool {
175        self.bm_flag == BM_VALID
176    }
177}
178
179/// Parsed entry block (file header or directory).
180#[derive(Debug, Clone)]
181pub struct EntryBlock {
182    /// Block type (should be T_HEADER).
183    pub block_type: i32,
184    /// This block's sector number.
185    pub header_key: u32,
186    /// High sequence (number of data blocks in this header for files).
187    pub high_seq: i32,
188    /// First data block (files only).
189    pub first_data: u32,
190    /// Checksum.
191    pub checksum: u32,
192    /// Hash table (directories) or data block pointers (files).
193    pub hash_table: [u32; HASH_TABLE_SIZE],
194    /// Access flags.
195    pub access: u32,
196    /// File size in bytes (files only).
197    pub byte_size: u32,
198    /// Comment length.
199    pub comment_len: u8,
200    /// Comment (up to 79 chars).
201    pub comment: [u8; MAX_COMMENT_LEN],
202    /// Last modification date.
203    pub date: AmigaDate,
204    /// Name length.
205    pub name_len: u8,
206    /// Entry name (up to 30 chars).
207    pub name: [u8; MAX_NAME_LEN],
208    /// Real entry (for hard links).
209    pub real_entry: u32,
210    /// Next link in chain.
211    pub next_link: u32,
212    /// Next entry with same hash.
213    pub next_same_hash: u32,
214    /// Parent directory block.
215    pub parent: u32,
216    /// Extension block (file ext or dir cache).
217    pub extension: u32,
218    /// Secondary type.
219    pub sec_type: i32,
220}
221
222impl EntryBlock {
223    /// Parse entry block from raw data.
224    pub fn parse(buf: &[u8; BLOCK_SIZE]) -> Result<Self> {
225        let block_type = read_i32_be(buf, 0);
226        if block_type != T_HEADER {
227            return Err(AffsError::InvalidBlockType);
228        }
229
230        let checksum = read_u32_be(buf, 20);
231        let calculated = normal_sum(buf, 20);
232        if checksum != calculated {
233            return Err(AffsError::ChecksumMismatch);
234        }
235
236        let header_key = read_u32_be(buf, 4);
237        let high_seq = read_i32_be(buf, 8);
238        let first_data = read_u32_be(buf, 16);
239
240        let mut hash_table = [0u32; HASH_TABLE_SIZE];
241        for (i, entry) in hash_table.iter_mut().enumerate() {
242            *entry = read_u32_be(buf, 24 + i * 4);
243        }
244
245        let access = read_u32_be(buf, 0x140);
246        let byte_size = read_u32_be(buf, 0x144);
247
248        let comment_len = buf[0x148].min(MAX_COMMENT_LEN as u8);
249        let mut comment = [0u8; MAX_COMMENT_LEN];
250        comment[..comment_len as usize].copy_from_slice(&buf[0x149..0x149 + comment_len as usize]);
251
252        let date = AmigaDate::new(
253            read_i32_be(buf, 0x1A4),
254            read_i32_be(buf, 0x1A8),
255            read_i32_be(buf, 0x1AC),
256        );
257
258        let name_len = buf[0x1B0].min(MAX_NAME_LEN as u8);
259        let mut name = [0u8; MAX_NAME_LEN];
260        name[..name_len as usize].copy_from_slice(&buf[0x1B1..0x1B1 + name_len as usize]);
261
262        let real_entry = read_u32_be(buf, 0x1D4);
263        let next_link = read_u32_be(buf, 0x1D8);
264        let next_same_hash = read_u32_be(buf, 0x1F0);
265        let parent = read_u32_be(buf, 0x1F4);
266        let extension = read_u32_be(buf, 0x1F8);
267        let sec_type = read_i32_be(buf, 0x1FC);
268
269        Ok(Self {
270            block_type,
271            header_key,
272            high_seq,
273            first_data,
274            checksum,
275            hash_table,
276            access,
277            byte_size,
278            comment_len,
279            comment,
280            date,
281            name_len,
282            name,
283            real_entry,
284            next_link,
285            next_same_hash,
286            parent,
287            extension,
288            sec_type,
289        })
290    }
291
292    /// Get entry name as byte slice.
293    #[inline]
294    pub fn name(&self) -> &[u8] {
295        &self.name[..self.name_len as usize]
296    }
297
298    /// Get comment as byte slice.
299    #[inline]
300    pub fn comment(&self) -> &[u8] {
301        &self.comment[..self.comment_len as usize]
302    }
303
304    /// Get entry type.
305    #[inline]
306    pub fn entry_type(&self) -> Option<EntryType> {
307        EntryType::from_sec_type(self.sec_type)
308    }
309
310    /// Check if this is a directory.
311    #[inline]
312    pub const fn is_dir(&self) -> bool {
313        self.sec_type == ST_DIR || self.sec_type == ST_LDIR
314    }
315
316    /// Check if this is a file.
317    #[inline]
318    pub const fn is_file(&self) -> bool {
319        self.sec_type == ST_FILE || self.sec_type == ST_LFILE
320    }
321
322    /// Get data block pointer at index (for files).
323    /// Index 0 is the first data block.
324    #[inline]
325    pub const fn data_block(&self, index: usize) -> u32 {
326        if index < MAX_DATABLK {
327            // Data blocks are stored in reverse order
328            self.hash_table[MAX_DATABLK - 1 - index]
329        } else {
330            0
331        }
332    }
333}
334
335/// Parsed file extension block.
336#[derive(Debug, Clone)]
337pub struct FileExtBlock {
338    /// Block type (should be T_LIST).
339    pub block_type: i32,
340    /// This block's sector number.
341    pub header_key: u32,
342    /// High sequence (number of data blocks in this ext block).
343    pub high_seq: i32,
344    /// Checksum.
345    pub checksum: u32,
346    /// Data block pointers.
347    pub data_blocks: [u32; MAX_DATABLK],
348    /// Parent (file header block).
349    pub parent: u32,
350    /// Next extension block.
351    pub extension: u32,
352    /// Secondary type (should be ST_FILE).
353    pub sec_type: i32,
354}
355
356impl FileExtBlock {
357    /// Parse file extension block from raw data.
358    pub fn parse(buf: &[u8; BLOCK_SIZE]) -> Result<Self> {
359        let block_type = read_i32_be(buf, 0);
360        if block_type != T_LIST {
361            return Err(AffsError::InvalidBlockType);
362        }
363
364        let checksum = read_u32_be(buf, 20);
365        let calculated = normal_sum(buf, 20);
366        if checksum != calculated {
367            return Err(AffsError::ChecksumMismatch);
368        }
369
370        let header_key = read_u32_be(buf, 4);
371        let high_seq = read_i32_be(buf, 8);
372
373        let mut data_blocks = [0u32; MAX_DATABLK];
374        for (i, block) in data_blocks.iter_mut().enumerate() {
375            *block = read_u32_be(buf, 24 + i * 4);
376        }
377
378        let parent = read_u32_be(buf, 0x1F4);
379        let extension = read_u32_be(buf, 0x1F8);
380        let sec_type = read_i32_be(buf, 0x1FC);
381
382        Ok(Self {
383            block_type,
384            header_key,
385            high_seq,
386            checksum,
387            data_blocks,
388            parent,
389            extension,
390            sec_type,
391        })
392    }
393
394    /// Get data block pointer at index.
395    #[inline]
396    pub const fn data_block(&self, index: usize) -> u32 {
397        if index < MAX_DATABLK {
398            // Data blocks are stored in reverse order
399            self.data_blocks[MAX_DATABLK - 1 - index]
400        } else {
401            0
402        }
403    }
404}
405
406/// Parsed OFS data block header.
407#[derive(Debug, Clone, Copy)]
408pub struct OfsDataBlock {
409    /// Block type (should be T_DATA).
410    pub block_type: i32,
411    /// File header block pointer.
412    pub header_key: u32,
413    /// Sequence number (1-based).
414    pub seq_num: u32,
415    /// Data size in this block.
416    pub data_size: u32,
417    /// Next data block.
418    pub next_data: u32,
419    /// Checksum.
420    pub checksum: u32,
421}
422
423impl OfsDataBlock {
424    /// OFS data block header size.
425    pub const HEADER_SIZE: usize = 24;
426
427    /// Parse OFS data block header from raw data.
428    pub fn parse(buf: &[u8; BLOCK_SIZE]) -> Result<Self> {
429        let block_type = read_i32_be(buf, 0);
430        if block_type != T_DATA {
431            return Err(AffsError::InvalidBlockType);
432        }
433
434        let checksum = read_u32_be(buf, 20);
435        let calculated = normal_sum(buf, 20);
436        if checksum != calculated {
437            return Err(AffsError::ChecksumMismatch);
438        }
439
440        Ok(Self {
441            block_type,
442            header_key: read_u32_be(buf, 4),
443            seq_num: read_u32_be(buf, 8),
444            data_size: read_u32_be(buf, 12),
445            next_data: read_u32_be(buf, 16),
446            checksum,
447        })
448    }
449
450    /// Get data portion of the block.
451    #[inline]
452    pub fn data(buf: &[u8; BLOCK_SIZE]) -> &[u8] {
453        &buf[Self::HEADER_SIZE..]
454    }
455}
456
457/// Compute hash value for a name.
458///
459/// This implements the Amiga filename hashing algorithm.
460#[inline]
461pub fn hash_name(name: &[u8], intl: bool) -> usize {
462    let mut hash = name.len() as u32;
463    for &c in name {
464        let upper = if intl {
465            intl_to_upper(c)
466        } else {
467            c.to_ascii_uppercase()
468        };
469        hash = (hash.wrapping_mul(13).wrapping_add(upper as u32)) & 0x7FF;
470    }
471    (hash % HASH_TABLE_SIZE as u32) as usize
472}
473
474/// Convert character to uppercase with international support.
475#[inline]
476const fn intl_to_upper(c: u8) -> u8 {
477    if (c >= b'a' && c <= b'z') || (c >= 224 && c <= 254 && c != 247) {
478        c - (b'a' - b'A')
479    } else {
480        c
481    }
482}
483
484/// Compare two names for equality (case-insensitive).
485#[inline]
486pub fn names_equal(a: &[u8], b: &[u8], intl: bool) -> bool {
487    if a.len() != b.len() {
488        return false;
489    }
490    for (&ca, &cb) in a.iter().zip(b.iter()) {
491        let ua = if intl {
492            intl_to_upper(ca)
493        } else {
494            ca.to_ascii_uppercase()
495        };
496        let ub = if intl {
497            intl_to_upper(cb)
498        } else {
499            cb.to_ascii_uppercase()
500        };
501        if ua != ub {
502            return false;
503        }
504    }
505    true
506}
507
508#[cfg(test)]
509mod tests {
510    use super::*;
511
512    #[test]
513    fn test_hash_name() {
514        // These are known hash values from the AFFS spec
515        assert!(hash_name(b"test", false) < HASH_TABLE_SIZE);
516        assert!(hash_name(b"", false) == 0);
517    }
518
519    #[test]
520    fn test_intl_to_upper() {
521        assert_eq!(intl_to_upper(b'a'), b'A');
522        assert_eq!(intl_to_upper(b'z'), b'Z');
523        assert_eq!(intl_to_upper(b'A'), b'A');
524        assert_eq!(intl_to_upper(224), 192); // à -> À
525    }
526
527    #[test]
528    fn test_names_equal() {
529        assert!(names_equal(b"Test", b"test", false));
530        assert!(names_equal(b"TEST", b"test", false));
531        assert!(!names_equal(b"Test", b"test2", false));
532    }
533}