Skip to main content

strat9_kernel/boot/
fat32_loader.rs

1//! FAT32 module loader for reading userspace ELFs from boot partition.
2//!
3//! Self-contained FAT32 parser with no external filesystem dependencies.
4//! Reads the BPB, follows cluster chains, parses directory entries (8.3 + LFN),
5//! and loads ELF modules into memory.
6//!
7//! # Safety considerations
8//! All `unsafe` blocks are confined to ELF memory copy and are documented.
9//! The parser uses no global mutable state : all buffers are stack-local.
10
11use super::block_device::BlockDevice;
12
13// ---------------------------------------------------------------------------
14// Constants
15// ---------------------------------------------------------------------------
16
17const SECTOR_SIZE: usize = 512;
18const FAT32_EOC: u32 = 0x0FFF_FFF8;
19const DIR_ENTRY_SIZE: usize = 32;
20
21const ATTR_READ_ONLY: u8 = 0x01;
22const ATTR_HIDDEN: u8 = 0x02;
23const ATTR_SYSTEM: u8 = 0x04;
24const ATTR_VOLUME_ID: u8 = 0x08;
25const ATTR_DIRECTORY: u8 = 0x10;
26const ATTR_ARCHIVE: u8 = 0x20;
27const ATTR_LFN: u8 = 0x0F;
28
29/// Maximum cluster chain length to prevent infinite loops on corrupted FAT.
30const MAX_CLUSTER_CHAIN: u32 = 1_048_576;
31
32/// Maximum number of path components.
33const MAX_PATH_DEPTH: usize = 16;
34
35// ---------------------------------------------------------------------------
36// BPB (BIOS Parameter Block)
37// ---------------------------------------------------------------------------
38
39#[derive(Debug, Clone)]
40struct Bpb {
41    bytes_per_sector: u16,
42    sectors_per_cluster: u8,
43    reserved_sectors: u16,
44    num_fats: u8,
45    sectors_per_fat_32: u32,
46    root_cluster: u32,
47    total_sectors_32: u32,
48}
49
50impl Bpb {
51    /// Parse a BPB from a 512-byte boot sector buffer.
52    ///
53    /// Validates:
54    /// - Boot signature 0x55AA at offset 0x1FE
55    /// - FAT32 signature "FAT32 " at offset 0x52
56    /// - Non-zero bytes_per_sector and sectors_per_cluster
57    /// - bytes_per_sector is 512 (only 512 supported)
58    fn from_bytes(buf: &[u8; SECTOR_SIZE]) -> Option<Self> {
59        // Boot signature
60        if buf[0x1FE] != 0x55 || buf[0x1FF] != 0xAA {
61            return None;
62        }
63
64        // FAT32 signature at offset 0x52
65        if &buf[0x52..0x58] != b"FAT32 " {
66            return None;
67        }
68
69        let bytes_per_sector = u16::from_le_bytes([buf[0x0B], buf[0x0C]]);
70        let sectors_per_cluster = buf[0x0D];
71        let reserved_sectors = u16::from_le_bytes([buf[0x0E], buf[0x0F]]);
72        let num_fats = buf[0x10];
73        let sectors_per_fat_32 = u32::from_le_bytes([buf[0x24], buf[0x25], buf[0x26], buf[0x27]]);
74        let root_cluster = u32::from_le_bytes([buf[0x2C], buf[0x2D], buf[0x2E], buf[0x2F]]);
75        let total_sectors_32 = u32::from_le_bytes([buf[0x20], buf[0x21], buf[0x22], buf[0x23]]);
76
77        if bytes_per_sector == 0 || sectors_per_cluster == 0 {
78            return None;
79        }
80        if bytes_per_sector != SECTOR_SIZE as u16 {
81            return None;
82        }
83        if num_fats == 0 || sectors_per_fat_32 == 0 {
84            return None;
85        }
86        if root_cluster < 2 {
87            return None;
88        }
89
90        Some(Bpb {
91            bytes_per_sector,
92            sectors_per_cluster,
93            reserved_sectors,
94            num_fats,
95            sectors_per_fat_32,
96            root_cluster,
97            total_sectors_32,
98        })
99    }
100
101    fn bytes_per_cluster(&self) -> u32 {
102        self.bytes_per_sector as u32 * self.sectors_per_cluster as u32
103    }
104
105    fn fat_start_sector(&self) -> u32 {
106        self.reserved_sectors as u32
107    }
108
109    fn data_start_sector(&self) -> u32 {
110        self.fat_start_sector() + self.sectors_per_fat_32 * self.num_fats as u32
111    }
112
113    /// Convert a cluster number to the LBA of its first sector.
114    /// Returns None if the cluster number is invalid.
115    fn cluster_to_lba(&self, cluster: u32) -> Option<u32> {
116        if cluster < 2 {
117            return None;
118        }
119        let offset = (cluster as u64 - 2) * self.sectors_per_cluster as u64;
120        let lba = self.data_start_sector() as u64 + offset;
121        if lba > u32::MAX as u64 {
122            return None;
123        }
124        Some(lba as u32)
125    }
126
127    /// Maximum valid cluster id for this volume.
128    fn max_cluster(&self) -> u32 {
129        let data_sectors = self
130            .total_sectors_32
131            .saturating_sub(self.data_start_sector());
132        let clusters = data_sectors / self.sectors_per_cluster as u32;
133        2 + clusters
134    }
135}
136
137// ---------------------------------------------------------------------------
138// Directory entry types
139// ---------------------------------------------------------------------------
140
141#[derive(Debug, Clone)]
142pub struct DirEntry {
143    name: DirEntryName,
144    attr: u8,
145    cluster: u32,
146    size: u32,
147}
148
149impl DirEntry {
150    fn is_dir(&self) -> bool {
151        self.attr & ATTR_DIRECTORY != 0
152    }
153
154    fn is_file(&self) -> bool {
155        self.attr & ATTR_DIRECTORY == 0
156    }
157}
158
159#[derive(Debug, Clone)]
160enum DirEntryName {
161    Short([u8; 11]),
162    Long(heapless::String<255>),
163}
164
165/// Small stack-allocated buffer for displaying a short file name.
166struct ShortNameDisplay {
167    buf: [u8; 12],
168    len: usize,
169}
170
171impl ShortNameDisplay {
172    fn from_sfn(sfn: &[u8; 11]) -> Self {
173        let mut display = ShortNameDisplay {
174            buf: [0u8; 12],
175            len: 0,
176        };
177        let mut pos = 0;
178
179        // Base name (chars 0-7), strip trailing spaces
180        let mut i = 0;
181        while i < 8 && sfn[i] != b' ' && sfn[i] != 0 {
182            display.buf[pos] = sfn[i].to_ascii_lowercase();
183            pos += 1;
184            i += 1;
185        }
186
187        // Extension (chars 8-10)
188        if sfn[8] != b' ' && sfn[8] != 0 {
189            display.buf[pos] = b'.';
190            pos += 1;
191            let mut i = 8;
192            while i < 11 && sfn[i] != b' ' && sfn[i] != 0 {
193                display.buf[pos] = sfn[i].to_ascii_lowercase();
194                pos += 1;
195                i += 1;
196            }
197        }
198
199        display.len = pos;
200        display
201    }
202
203    fn as_str(&self) -> &str {
204        core::str::from_utf8(&self.buf[..self.len]).unwrap_or("???")
205    }
206}
207
208impl DirEntryName {
209    fn display_name(&self) -> ShortNameDisplay {
210        match self {
211            DirEntryName::Long(s) => {
212                // For LFN, we need a different approach. Return a stack struct.
213                // Since ShortNameDisplay is for SFN only, we handle Long separately.
214                // We use the same struct but with the first 12 chars of the LFN.
215                let mut d = ShortNameDisplay {
216                    buf: [0u8; 12],
217                    len: 0,
218                };
219                let bytes = s.as_bytes();
220                let len = bytes.len().min(12);
221                d.buf[..len].copy_from_slice(&bytes[..len]);
222                d.len = len;
223                d
224            }
225            DirEntryName::Short(sfn) => ShortNameDisplay::from_sfn(sfn),
226        }
227    }
228
229    fn matches_short(&self, upper_name: &[u8; 11]) -> bool {
230        match self {
231            DirEntryName::Short(sfn) => sfn == upper_name,
232            DirEntryName::Long(_) => false,
233        }
234    }
235
236    fn as_lfn_str(&self) -> Option<&str> {
237        match self {
238            DirEntryName::Long(s) => Some(s.as_str()),
239            DirEntryName::Short(_) => None,
240        }
241    }
242}
243
244/// Build an 8.3 short name from a string for comparison.
245///
246/// Follows FAT32 spec:
247/// - Uppercase all characters
248/// - Ignore spaces
249/// - Truncate to 8+3
250/// - Names starting with 0xE5 are stored as 0x05
251fn make_sfn(name: &str) -> [u8; 11] {
252    let mut sfn = [b' '; 11];
253    let bytes = name.as_bytes();
254
255    // Find the last dot to split name/extension
256    // But only if it's not the first character (FAT treats ".foo" as extensionless)
257    let dot_pos = if bytes.len() > 1 && bytes[0] != b'.' {
258        bytes[1..].iter().rposition(|&b| b == b'.').map(|p| p + 1)
259    } else {
260        None
261    };
262
263    let (base, ext) = if let Some(dp) = dot_pos {
264        (&bytes[..dp], &bytes[dp + 1..])
265    } else {
266        (bytes, &b""[..])
267    };
268
269    // Fill base name (up to 8 chars), skipping spaces
270    let mut pos = 0;
271    let mut i = 0;
272    while pos < 8 && i < base.len() {
273        let c = base[i];
274        if c != b' ' {
275            sfn[pos] = c.to_ascii_uppercase();
276            pos += 1;
277        }
278        i += 1;
279    }
280
281    // Fill extension (up to 3 chars), skipping spaces
282    pos = 8;
283    i = 0;
284    while pos < 11 && i < ext.len() {
285        let c = ext[i];
286        if c != b' ' {
287            sfn[pos] = c.to_ascii_uppercase();
288            pos += 1;
289        }
290        i += 1;
291    }
292
293    // Handle leading 0xE5 (deleted marker) => 0x05
294    if sfn[0] == 0xE5 {
295        sfn[0] = 0x05;
296    }
297
298    sfn
299}
300
301// ---------------------------------------------------------------------------
302// Cluster chain iterator (with cycle detection)
303// ---------------------------------------------------------------------------
304
305/// Iterates over clusters in a chain, with cycle detection.
306struct ClusterChainIter<'a, B: BlockDevice> {
307    fs: &'a mut FatFs<'a, B>,
308    next_cluster: Option<u32>,
309    iterations: u32,
310}
311
312impl<'a, B: BlockDevice> ClusterChainIter<'a, B> {
313    fn new(fs: &'a mut FatFs<'a, B>, start_cluster: u32) -> Self {
314        Self {
315            fs,
316            next_cluster: if start_cluster >= 2 {
317                Some(start_cluster)
318            } else {
319                None
320            },
321            iterations: 0,
322        }
323    }
324}
325
326impl<'a, B: BlockDevice> Iterator for ClusterChainIter<'a, B> {
327    type Item = u32;
328
329    fn next(&mut self) -> Option<u32> {
330        let current = self.next_cluster?;
331        self.iterations += 1;
332        if self.iterations > MAX_CLUSTER_CHAIN {
333            crate::serial_println!("[fat32] Cluster chain exceeds maximum length (cycle?)");
334            self.next_cluster = None;
335            return None;
336        }
337
338        match self.fs.next_cluster(current) {
339            Some(next) if next >= 2 => {
340                self.next_cluster = Some(next);
341                Some(current)
342            }
343            Some(_) => {
344                self.next_cluster = None;
345                Some(current)
346            }
347            None => {
348                self.next_cluster = None;
349                None
350            }
351        }
352    }
353}
354
355// ---------------------------------------------------------------------------
356// FatFs : main filesystem object
357// ---------------------------------------------------------------------------
358
359pub struct FatFs<'a, B: BlockDevice> {
360    block_dev: &'a mut B,
361    bpb: Bpb,
362}
363
364impl<'a, B: BlockDevice> FatFs<'a, B> {
365    /// Mount a FAT32 filesystem from a block device.
366    pub fn new(block_dev: &'a mut B) -> Option<Self> {
367        let kernel_block_size = block_dev.block_size() as usize;
368        let mut sector_buf = [0u8; SECTOR_SIZE];
369        Self::read_sector_at(block_dev, 0, &mut sector_buf, kernel_block_size)?;
370
371        let bpb = Bpb::from_bytes(&sector_buf)?;
372
373        crate::serial_println!(
374            "[fat32] BPB: {} bytes/sector, {} sec/cluster, {} FATs, root_cluster={}",
375            bpb.bytes_per_sector,
376            bpb.sectors_per_cluster,
377            bpb.num_fats,
378            bpb.root_cluster,
379        );
380
381        Some(FatFs { block_dev, bpb })
382    }
383
384    /// Read a FAT32 sector from a block device at any block size.
385    fn read_sector_at(
386        dev: &mut B,
387        fat32_sector: u32,
388        buf: &mut [u8; SECTOR_SIZE],
389        kernel_block_size: usize,
390    ) -> Option<()> {
391        if kernel_block_size >= SECTOR_SIZE {
392            // Kernel block >= FAT sector: read one block, take first 512 bytes
393            let mut block_buf = [0u8; 4096];
394            dev.read_block(fat32_sector as u64, &mut block_buf[..kernel_block_size])
395                .ok()?;
396            let n = SECTOR_SIZE.min(kernel_block_size);
397            buf[..n].copy_from_slice(&block_buf[..n]);
398        } else {
399            // Kernel block < FAT sector: read multiple blocks
400            let blocks_per_sector = SECTOR_SIZE / kernel_block_size;
401            let base_lba = fat32_sector as u64 * blocks_per_sector as u64;
402            let mut block_buf = [0u8; 4096];
403            for i in 0..blocks_per_sector {
404                dev.read_block(base_lba + i as u64, &mut block_buf[..kernel_block_size])
405                    .ok()?;
406                let dst = i * kernel_block_size;
407                buf[dst..dst + kernel_block_size].copy_from_slice(&block_buf[..kernel_block_size]);
408            }
409        }
410        Some(())
411    }
412
413    fn read_sector(&mut self, fat32_sector: u32, buf: &mut [u8; SECTOR_SIZE]) -> Option<()> {
414        let kbs = self.block_dev.block_size() as usize;
415        Self::read_sector_at(self.block_dev, fat32_sector, buf, kbs)
416    }
417
418    /// Read the next cluster number from the FAT table.
419    fn next_cluster(&mut self, cluster: u32) -> Option<u32> {
420        if cluster < 2 {
421            return None;
422        }
423
424        let fat_offset = cluster as usize * 4;
425        let fat_sector = self.bpb.fat_start_sector() + (fat_offset / SECTOR_SIZE) as u32;
426        let entry_offset = fat_offset % SECTOR_SIZE;
427
428        // Bounds check: ensure the 4-byte entry fits within the sector
429        if entry_offset + 4 > SECTOR_SIZE {
430            crate::serial_println!("[fat32] FAT entry crosses sector boundary (corrupt?)");
431            return None;
432        }
433
434        let mut sector_buf = [0u8; SECTOR_SIZE];
435        self.read_sector(fat_sector, &mut sector_buf)?;
436
437        let entry = u32::from_le_bytes([
438            sector_buf[entry_offset],
439            sector_buf[entry_offset + 1],
440            sector_buf[entry_offset + 2],
441            sector_buf[entry_offset + 3],
442        ]) & 0x0FFF_FFFF;
443
444        if entry >= FAT32_EOC {
445            Some(0)
446        } else {
447            Some(entry)
448        }
449    }
450
451    /// Read a full cluster chain into a heap-allocated buffer.
452    ///
453    /// Skips `clusters_to_skip` clusters at the start (for offset-based reads).
454    /// Only reads `max_bytes` after the skip, if provided.
455    fn read_chain(
456        &mut self,
457        start_cluster: u32,
458        clusters_to_skip: u32,
459        max_bytes: Option<usize>,
460    ) -> Option<alloc::vec::Vec<u8>> {
461        let spc = self.bpb.sectors_per_cluster as u32;
462        let mut data = alloc::vec::Vec::new();
463
464        // Pre-allocate based on max_bytes if known
465        if let Some(max) = max_bytes {
466            data.reserve(max);
467        }
468
469        let mut current = start_cluster;
470
471        // Skip clusters
472        for _ in 0..clusters_to_skip {
473            current = self.next_cluster(current)?;
474            if current < 2 {
475                return Some(data);
476            }
477        }
478
479        let mut iterations = 0u32;
480        while current >= 2 {
481            iterations += 1;
482            if iterations > MAX_CLUSTER_CHAIN {
483                crate::serial_println!("[fat32] Cluster chain too long (cycle?)");
484                return None;
485            }
486
487            let cluster_lba = self.bpb.cluster_to_lba(current)?;
488
489            for s in 0..spc {
490                let mut sector_buf = [0u8; SECTOR_SIZE];
491                self.read_sector(cluster_lba + s, &mut sector_buf)?;
492                data.extend_from_slice(&sector_buf);
493
494                if let Some(max) = max_bytes {
495                    if data.len() >= max {
496                        data.truncate(max);
497                        return Some(data);
498                    }
499                }
500            }
501
502            current = self.next_cluster(current)?;
503        }
504
505        Some(data)
506    }
507
508    /// Read a full cluster chain (from the start, no skip).
509    fn read_cluster_chain_alloc(&mut self, cluster: u32) -> Option<alloc::vec::Vec<u8>> {
510        self.read_chain(cluster, 0, None)
511    }
512
513    /// Read raw directory entries from a cluster chain.
514    fn read_dir_entries(&mut self, cluster: u32) -> Option<alloc::vec::Vec<DirEntry>> {
515        let dir_data = self.read_cluster_chain_alloc(cluster)?;
516        self.parse_dir_entries(&dir_data)
517    }
518
519    /// Parse raw directory data into `DirEntry` items.
520    ///
521    /// Handles:
522    /// - End-of-directory marker (0x00)
523    /// - Deleted entries (0xE5)
524    /// - LFN entries (attr == 0x0F)
525    /// - Volume ID entries (attr == 0x08)
526    fn parse_dir_entries(&self, data: &[u8]) -> Option<alloc::vec::Vec<DirEntry>> {
527        let mut entries = alloc::vec::Vec::new();
528        let mut lfn_chars: heapless::Vec<char, 255> = heapless::Vec::new();
529        let mut lfn_checksum: u8 = 0;
530        let mut i = 0;
531
532        while i + DIR_ENTRY_SIZE <= data.len() {
533            let raw = &data[i..i + DIR_ENTRY_SIZE];
534            i += DIR_ENTRY_SIZE;
535
536            // End of directory
537            if raw[0] == 0x00 {
538                break;
539            }
540
541            // Deleted entry : clear LFN state
542            if raw[0] == 0xE5 {
543                lfn_chars.clear();
544                continue;
545            }
546
547            let attr = raw[11];
548
549            // LFN entry
550            if attr == ATTR_LFN {
551                let seq = raw[0];
552                let is_last = (seq & 0x40) != 0;
553                let _index = seq & 0x3F;
554                let chksum = raw[13];
555
556                if is_last {
557                    lfn_chars.clear();
558                    lfn_checksum = chksum;
559                } else if lfn_checksum != chksum {
560                    // Checksum mismatch : broken LFN sequence, discard
561                    lfn_chars.clear();
562                    continue;
563                }
564
565                // LFN character positions in the 32-byte entry
566                // Chars 0-4:   offsets 1,3,5,7,9
567                // Chars 5-10:  offsets 14,16,18,20,22,24
568                // Chars 11-12: offsets 28,30
569                let positions = [1, 3, 5, 7, 9, 14, 16, 18, 20, 22, 24, 28, 30];
570                for &pos in &positions {
571                    let lo = raw[pos] as u16;
572                    let hi = raw[pos + 1] as u16;
573                    let ch16 = (hi << 8) | lo;
574                    if ch16 == 0x0000 || ch16 == 0xFFFF {
575                        continue;
576                    }
577                    if let Some(c) = char::from_u32(ch16 as u32) {
578                        // Push in order : LFN entries arrive last-to-first,
579                        // so we push each entry's chars in forward order,
580                        // then reverse the entire collected sequence at the end.
581                        let _ = lfn_chars.push(c);
582                    }
583                }
584                continue;
585            }
586
587            // Regular directory entry (file or subdirectory)
588            let cluster_hi = u16::from_le_bytes([raw[20], raw[21]]) as u32;
589            let cluster_lo = u16::from_le_bytes([raw[26], raw[27]]) as u32;
590            let cluster = (cluster_hi << 16) | cluster_lo;
591            let size = u32::from_le_bytes([raw[28], raw[29], raw[30], raw[31]]);
592
593            let name = if !lfn_chars.is_empty() {
594                // LFN entries arrive last-to-first, chars within each entry are forward.
595                // We pushed chars in forward order per entry, but entries are reverse.
596                // So reverse the whole collected sequence to get the correct name.
597                let mut lfn_str = heapless::String::<255>::new();
598                for &c in lfn_chars.iter().rev() {
599                    let _ = lfn_str.push(c);
600                }
601                lfn_chars.clear();
602                DirEntryName::Long(lfn_str)
603            } else {
604                let mut sfn = [0u8; 11];
605                sfn.copy_from_slice(&raw[0..11]);
606                DirEntryName::Short(sfn)
607            };
608
609            // Skip volume ID entries
610            if attr & ATTR_VOLUME_ID != 0 {
611                continue;
612            }
613
614            entries.push(DirEntry {
615                name,
616                attr,
617                cluster,
618                size,
619            });
620        }
621
622        Some(entries)
623    }
624
625    /// Resolve a path to a directory entry.
626    fn resolve_path(&mut self, path: &str) -> Option<DirEntry> {
627        let path = path.trim_start_matches('/');
628        if path.is_empty() {
629            return Some(DirEntry {
630                name: DirEntryName::Short(*b"/          "),
631                attr: ATTR_DIRECTORY,
632                cluster: self.bpb.root_cluster,
633                size: 0,
634            });
635        }
636
637        let mut current_cluster = self.bpb.root_cluster;
638
639        // Collect path components, limiting depth
640        let mut parts: heapless::Vec<&str, MAX_PATH_DEPTH> = heapless::Vec::new();
641        for part in path.split('/') {
642            if !part.is_empty() {
643                if parts.push(part).is_err() {
644                    crate::serial_println!("[fat32] Path too deep: {}", path);
645                    return None;
646                }
647            }
648        }
649
650        for (idx, part) in parts.iter().enumerate() {
651            let dir_entries = self.read_dir_entries(current_cluster)?;
652            let target_sfn = make_sfn(part);
653
654            let mut found = None;
655            for entry in &dir_entries {
656                if entry.name.matches_short(&target_sfn) {
657                    found = Some(entry.clone());
658                    break;
659                }
660                // LFN match: case-insensitive
661                if let Some(lfn) = entry.name.as_lfn_str() {
662                    if lfn.eq_ignore_ascii_case(part) {
663                        found = Some(entry.clone());
664                        break;
665                    }
666                }
667                // SFN display match (handles lowercase comparison)
668                let display = entry.name.display_name();
669                if display.as_str().eq_ignore_ascii_case(part) {
670                    found = Some(entry.clone());
671                    break;
672                }
673            }
674
675            let entry = found?;
676
677            if idx == parts.len() - 1 {
678                return Some(entry);
679            }
680
681            if !entry.is_dir() {
682                return None;
683            }
684            current_cluster = entry.cluster;
685        }
686
687        None
688    }
689
690    /// Read a file by path, returning its contents.
691    pub fn read_file(&mut self, path: &str) -> Option<alloc::vec::Vec<u8>> {
692        let entry = self.resolve_path(path)?;
693        if entry.is_dir() {
694            return None;
695        }
696        if entry.cluster == 0 && entry.size == 0 {
697            return Some(alloc::vec::Vec::new());
698        }
699
700        let data = self.read_cluster_chain_alloc(entry.cluster)?;
701        let file_size = entry.size as usize;
702
703        // Validate that we read enough data
704        if data.len() < file_size {
705            crate::serial_println!(
706                "[fat32] Warning: file {} truncated (read {} bytes, expected {})",
707                path,
708                data.len(),
709                file_size
710            );
711        }
712
713        let mut result = data;
714        result.truncate(file_size);
715        Some(result)
716    }
717
718    /// List directory contents by path.
719    pub fn list_dir(&mut self, path: &str) -> Option<alloc::vec::Vec<DirEntry>> {
720        let entry = self.resolve_path(path)?;
721        if !entry.is_dir() {
722            return None;
723        }
724        self.read_dir_entries(entry.cluster)
725    }
726}
727
728// ---------------------------------------------------------------------------
729// ELF validation
730// ---------------------------------------------------------------------------
731
732/// Validate that a buffer looks like a valid ELF header.
733///
734/// Checks:
735/// - Magic: 0x7F 'E' 'L' 'F'
736/// - Class: 32-bit (1) or 64-bit (2)
737/// - Data: little-endian (1)
738/// - Type: relocatable (1), executable (2), shared (3), or core (4)
739fn is_valid_elf(data: &[u8]) -> bool {
740    if data.len() < 16 {
741        return false;
742    }
743
744    // Magic
745    if &data[0..4] != b"\x7fELF" {
746        return false;
747    }
748
749    // Class: 1 = 32-bit, 2 = 64-bit
750    let class = data[4];
751    if class != 1 && class != 2 {
752        return false;
753    }
754
755    // Data encoding: 1 = little-endian, 2 = big-endian
756    let data_enc = data[5];
757    if data_enc != 1 && data_enc != 2 {
758        return false;
759    }
760
761    // ELF type: 1=REL, 2=EXEC, 3=DYN, 4=CORE
762    let elf_type = if class == 2 {
763        u16::from_le_bytes([data[16], data[17]])
764    } else {
765        u16::from_le_bytes([data[16], data[17]])
766    };
767    if elf_type == 0 || elf_type > 4 {
768        return false;
769    }
770
771    true
772}
773
774// ---------------------------------------------------------------------------
775// ELF loading & public API
776// ---------------------------------------------------------------------------
777
778#[derive(Debug, Clone, Copy)]
779pub struct ModuleInfo {
780    pub base: u64,
781    pub size: u64,
782}
783
784pub struct BootModules {
785    pub init: Option<ModuleInfo>,
786    pub console_admin: Option<ModuleInfo>,
787    pub strate_net: Option<ModuleInfo>,
788    pub strate_bus: Option<ModuleInfo>,
789    pub fs_ext4: Option<ModuleInfo>,
790    pub strate_fs_ramfs: Option<ModuleInfo>,
791    pub strate_wasm: Option<ModuleInfo>,
792    pub strate_webrtc: Option<ModuleInfo>,
793    pub dhcp_client: Option<ModuleInfo>,
794    pub ping: Option<ModuleInfo>,
795    pub telnetd: Option<ModuleInfo>,
796    pub udp_tool: Option<ModuleInfo>,
797    pub web_admin: Option<ModuleInfo>,
798}
799
800impl Default for BootModules {
801    fn default() -> Self {
802        Self {
803            init: None,
804            console_admin: None,
805            strate_net: None,
806            strate_bus: None,
807            fs_ext4: None,
808            strate_fs_ramfs: None,
809            strate_wasm: None,
810            strate_webrtc: None,
811            dhcp_client: None,
812            ping: None,
813            telnetd: None,
814            udp_tool: None,
815            web_admin: None,
816        }
817    }
818}
819
820const MODULE_DIR: &str = "/modules";
821
822const MODULE_NAMES: &[(&str, fn(&mut BootModules) -> &mut Option<ModuleInfo>)] = &[
823    ("init", |m| &mut m.init),
824    ("console_admin", |m| &mut m.console_admin),
825    ("strate_net", |m| &mut m.strate_net),
826    ("strate_bus", |m| &mut m.strate_bus),
827    ("fs_ext4", |m| &mut m.fs_ext4),
828    ("strate_fs_ramfs", |m| &mut m.strate_fs_ramfs),
829    ("strate_wasm", |m| &mut m.strate_wasm),
830    ("strate_webrtc", |m| &mut m.strate_webrtc),
831    ("dhcp_client", |m| &mut m.dhcp_client),
832    ("ping", |m| &mut m.ping),
833    ("telnetd", |m| &mut m.telnetd),
834    ("udp_tool", |m| &mut m.udp_tool),
835    ("web_admin", |m| &mut m.web_admin),
836];
837
838/// Load all boot modules from the FAT32 boot partition.
839pub fn load_all_modules<B: BlockDevice>(block_dev: &mut B) -> BootModules {
840    let mut modules = BootModules::default();
841
842    let mut fs = match FatFs::new(block_dev) {
843        Some(fs) => fs,
844        None => {
845            crate::serial_println!("[fat32] Failed to mount filesystem");
846            return modules;
847        }
848    };
849
850    crate::serial_println!("[fat32] FAT32 filesystem mounted");
851
852    let entries = match fs.list_dir(MODULE_DIR) {
853        Some(e) => e,
854        None => {
855            crate::serial_println!("[fat32] No {} directory", MODULE_DIR);
856            return modules;
857        }
858    };
859
860    for entry in entries {
861        if !entry.is_file() {
862            continue;
863        }
864
865        // Get display name : owns the buffer so no dangling reference
866        let display = entry.name.display_name();
867        let name_str = display.as_str();
868
869        let stem = name_str.strip_suffix(".elf").unwrap_or(name_str);
870
871        for &(mod_name, setter) in MODULE_NAMES {
872            if stem.eq_ignore_ascii_case(mod_name) {
873                let full_path: heapless::String<64> = {
874                    let mut s = heapless::String::new();
875                    let _ = s.push_str(MODULE_DIR);
876                    let _ = s.push('/');
877                    let _ = s.push_str(name_str);
878                    s
879                };
880
881                match load_elf_from_fat(&mut fs, &full_path, &entry) {
882                    Some(info) => {
883                        crate::serial_println!(
884                            "[fat32] Loaded {} at {:#x} ({} bytes)",
885                            full_path,
886                            info.base,
887                            info.size
888                        );
889                        *setter(&mut modules) = Some(info);
890                    }
891                    None => {
892                        crate::serial_println!("[fat32] Failed to load {}", full_path);
893                    }
894                }
895                break;
896            }
897        }
898    }
899
900    modules
901}
902
903/// Load a single module from FAT32.
904pub fn load_module<B: BlockDevice>(block_dev: &mut B, path: &str) -> Option<ModuleInfo> {
905    let mut fs = FatFs::new(block_dev)?;
906    let entry = fs.resolve_path(path)?;
907    load_elf_from_fat(&mut fs, path, &entry)
908}
909
910/// Load an ELF from a directory entry into allocated memory.
911///
912/// Validates the ELF header before loading. Returns None on any error.
913fn load_elf_from_fat<B: BlockDevice>(
914    fs: &mut FatFs<'_, B>,
915    path: &str,
916    entry: &DirEntry,
917) -> Option<ModuleInfo> {
918    if entry.size == 0 {
919        crate::serial_println!("[fat32] {} is empty", path);
920        return None;
921    }
922
923    // Read the file data
924    let file_data = fs.read_cluster_chain_alloc(entry.cluster)?;
925    let file_size = entry.size as usize;
926
927    // Validate that we read enough data
928    if file_data.len() < file_size {
929        crate::serial_println!(
930            "[fat32] {} truncated (read {} bytes, expected {})",
931            path,
932            file_size,
933            file_size
934        );
935        return None;
936    }
937
938    // Validate ELF header (class, endianness, type)
939    if !is_valid_elf(&file_data) {
940        crate::serial_println!("[fat32] {} is not a valid ELF", path);
941        return None;
942    }
943
944    // Allocate contiguous physical memory for the module
945    // SAFETY: Layout is non-zero (file_size > 0 checked above) and aligned to 4096.
946    // The allocation is for boot module loading : memory is not freed (kernel lifetime).
947    let layout = core::alloc::Layout::from_size_align(file_size, 4096).ok()?;
948    let ptr = unsafe { alloc::alloc::alloc(layout) };
949    if ptr.is_null() {
950        crate::serial_println!("[fat32] Alloc failed: {} bytes for {}", file_size, path);
951        return None;
952    }
953
954    // SAFETY: ptr is non-null (checked above), layout matches the allocation,
955    // and file_size bytes are available from file_data.
956    let buf = unsafe { core::slice::from_raw_parts_mut(ptr, file_size) };
957    buf.copy_from_slice(&file_data[..file_size]);
958
959    Some(ModuleInfo {
960        base: ptr as u64,
961        size: file_size as u64,
962    })
963}