⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/fs/
ext4.rs

1//! ext4 Read-Only Filesystem Implementation
2//!
3//! Supports reading files, directories, symlinks from ext4 formatted volumes.
4//! Handles both extent-based and legacy block-map inodes.
5//! Journal replay is not implemented (read-only mount).
6//!
7//! Struct fields and constants define the on-disk ext4 format per the kernel
8//! documentation. Unused fields/constants are retained for format completeness
9//! and future write support.
10#![allow(dead_code)]
11
12use alloc::{collections::BTreeMap, string::String, sync::Arc, vec, vec::Vec};
13use core::sync::atomic::{AtomicU64, Ordering};
14
15#[cfg(not(target_arch = "aarch64"))]
16use spin::RwLock;
17
18#[cfg(target_arch = "aarch64")]
19use super::bare_lock::RwLock;
20use super::{DirEntry, Filesystem, Metadata, NodeType, Permissions, VfsNode};
21use crate::error::{FsError, KernelError};
22
23/// ext4 superblock magic number
24const EXT4_SUPER_MAGIC: u16 = 0xEF53;
25
26/// Superblock offset from start of volume
27const SUPERBLOCK_OFFSET: usize = 1024;
28
29/// Inode flags
30const EXT4_EXTENTS_FL: u32 = 0x0008_0000;
31
32/// Inode file type flags (from i_mode)
33const S_IFMT: u16 = 0xF000;
34const S_IFREG: u16 = 0x8000;
35const S_IFDIR: u16 = 0x4000;
36const S_IFLNK: u16 = 0xA000;
37
38/// Directory entry file types
39const EXT4_FT_UNKNOWN: u8 = 0;
40const EXT4_FT_REG_FILE: u8 = 1;
41const EXT4_FT_DIR: u8 = 2;
42const EXT4_FT_SYMLINK: u8 = 7;
43
44/// Root directory inode number
45const EXT4_ROOT_INO: u32 = 2;
46
47/// Size of an ext4 extent header
48const EXT4_EXTENT_HEADER_SIZE: usize = 12;
49/// Size of an ext4 extent
50const EXT4_EXTENT_SIZE: usize = 12;
51/// Size of an ext4 extent index
52const EXT4_EXTENT_IDX_SIZE: usize = 12;
53
54/// Magic number for extent tree headers
55const EXT4_EXT_MAGIC: u16 = 0xF30A;
56
57static EXT4_NEXT_INODE: AtomicU64 = AtomicU64::new(1);
58
59/// ext4 superblock (relevant fields only)
60#[derive(Clone)]
61struct Ext4Superblock {
62    inodes_count: u32,
63    blocks_count_lo: u32,
64    free_blocks_count_lo: u32,
65    free_inodes_count: u32,
66    first_data_block: u32,
67    log_block_size: u32,
68    blocks_per_group: u32,
69    inodes_per_group: u32,
70    magic: u16,
71    inode_size: u16,
72    feature_incompat: u32,
73    feature_ro_compat: u32,
74    desc_size: u16,
75    blocks_count_hi: u32,
76}
77
78impl Ext4Superblock {
79    fn parse(data: &[u8]) -> Result<Self, KernelError> {
80        if data.len() < 256 {
81            return Err(KernelError::FsError(FsError::CorruptedData));
82        }
83
84        let magic = u16::from_le_bytes([data[0x38], data[0x39]]);
85        if magic != EXT4_SUPER_MAGIC {
86            return Err(KernelError::FsError(FsError::CorruptedData));
87        }
88
89        let desc_size_raw = u16::from_le_bytes([data[0xFE], data[0xFF]]);
90        // desc_size is 0 for old ext2/ext3 (means 32 bytes)
91        let desc_size = if desc_size_raw == 0 {
92            32
93        } else {
94            desc_size_raw
95        };
96
97        Ok(Self {
98            inodes_count: u32::from_le_bytes([data[0], data[1], data[2], data[3]]),
99            blocks_count_lo: u32::from_le_bytes([data[4], data[5], data[6], data[7]]),
100            free_blocks_count_lo: u32::from_le_bytes([
101                data[0x0C], data[0x0D], data[0x0E], data[0x0F],
102            ]),
103            free_inodes_count: u32::from_le_bytes([data[0x10], data[0x11], data[0x12], data[0x13]]),
104            first_data_block: u32::from_le_bytes([data[0x14], data[0x15], data[0x16], data[0x17]]),
105            log_block_size: u32::from_le_bytes([data[0x18], data[0x19], data[0x1A], data[0x1B]]),
106            blocks_per_group: u32::from_le_bytes([data[0x20], data[0x21], data[0x22], data[0x23]]),
107            inodes_per_group: u32::from_le_bytes([data[0x28], data[0x29], data[0x2A], data[0x2B]]),
108            magic,
109            inode_size: u16::from_le_bytes([data[0x58], data[0x59]]),
110            feature_incompat: u32::from_le_bytes([data[0x60], data[0x61], data[0x62], data[0x63]]),
111            feature_ro_compat: u32::from_le_bytes([data[0x64], data[0x65], data[0x66], data[0x67]]),
112            desc_size,
113            blocks_count_hi: u32::from_le_bytes([
114                data[0x150],
115                data[0x151],
116                data[0x152],
117                data[0x153],
118            ]),
119        })
120    }
121
122    fn block_size(&self) -> usize {
123        1024 << self.log_block_size
124    }
125
126    fn num_block_groups(&self) -> u32 {
127        self.blocks_count_lo.div_ceil(self.blocks_per_group)
128    }
129}
130
131/// Block group descriptor (relevant fields)
132#[derive(Clone)]
133struct BlockGroupDesc {
134    inode_table_lo: u32,
135    inode_table_hi: u32,
136}
137
138impl BlockGroupDesc {
139    fn parse(data: &[u8], desc_size: u16) -> Self {
140        let inode_table_lo = u32::from_le_bytes([data[8], data[9], data[10], data[11]]);
141        let inode_table_hi = if desc_size >= 64 && data.len() >= 44 {
142            u32::from_le_bytes([data[40], data[41], data[42], data[43]])
143        } else {
144            0
145        };
146        Self {
147            inode_table_lo,
148            inode_table_hi,
149        }
150    }
151
152    fn inode_table_block(&self) -> u64 {
153        self.inode_table_lo as u64 | ((self.inode_table_hi as u64) << 32)
154    }
155}
156
157/// On-disk inode (relevant fields)
158struct Ext4Inode {
159    mode: u16,
160    size_lo: u32,
161    size_hi: u32,
162    atime: u32,
163    mtime: u32,
164    flags: u32,
165    /// The 60-byte i_block area (direct/indirect blocks or extent tree)
166    block_data: [u8; 60],
167    links_count: u16,
168}
169
170impl Ext4Inode {
171    fn parse(data: &[u8]) -> Result<Self, KernelError> {
172        if data.len() < 128 {
173            return Err(KernelError::FsError(FsError::CorruptedData));
174        }
175
176        let mut block_data = [0u8; 60];
177        block_data.copy_from_slice(&data[0x28..0x64]);
178
179        Ok(Self {
180            mode: u16::from_le_bytes([data[0], data[1]]),
181            size_lo: u32::from_le_bytes([data[4], data[5], data[6], data[7]]),
182            size_hi: u32::from_le_bytes([data[0x6C], data[0x6D], data[0x6E], data[0x6F]]),
183            atime: u32::from_le_bytes([data[8], data[9], data[10], data[11]]),
184            mtime: u32::from_le_bytes([data[0x10], data[0x11], data[0x12], data[0x13]]),
185            flags: u32::from_le_bytes([data[0x20], data[0x21], data[0x22], data[0x23]]),
186            block_data,
187            links_count: u16::from_le_bytes([data[0x1A], data[0x1B]]),
188        })
189    }
190
191    fn size(&self) -> u64 {
192        self.size_lo as u64 | ((self.size_hi as u64) << 32)
193    }
194
195    fn is_dir(&self) -> bool {
196        (self.mode & S_IFMT) == S_IFDIR
197    }
198
199    fn is_file(&self) -> bool {
200        (self.mode & S_IFMT) == S_IFREG
201    }
202
203    fn is_symlink(&self) -> bool {
204        (self.mode & S_IFMT) == S_IFLNK
205    }
206
207    fn uses_extents(&self) -> bool {
208        (self.flags & EXT4_EXTENTS_FL) != 0
209    }
210
211    fn node_type(&self) -> NodeType {
212        if self.is_dir() {
213            NodeType::Directory
214        } else if self.is_symlink() {
215            NodeType::Symlink
216        } else {
217            NodeType::File
218        }
219    }
220
221    fn permissions(&self) -> Permissions {
222        let mode = self.mode;
223        Permissions {
224            owner_read: (mode & 0o400) != 0,
225            owner_write: (mode & 0o200) != 0,
226            owner_exec: (mode & 0o100) != 0,
227            group_read: (mode & 0o040) != 0,
228            group_write: (mode & 0o020) != 0,
229            group_exec: (mode & 0o010) != 0,
230            other_read: (mode & 0o004) != 0,
231            other_write: (mode & 0o002) != 0,
232            other_exec: (mode & 0o001) != 0,
233        }
234    }
235}
236
237/// ext4 extent header (12 bytes)
238struct ExtentHeader {
239    magic: u16,
240    entries: u16,
241    _max: u16,
242    depth: u16,
243}
244
245impl ExtentHeader {
246    fn parse(data: &[u8]) -> Result<Self, KernelError> {
247        if data.len() < EXT4_EXTENT_HEADER_SIZE {
248            return Err(KernelError::FsError(FsError::CorruptedData));
249        }
250        let magic = u16::from_le_bytes([data[0], data[1]]);
251        if magic != EXT4_EXT_MAGIC {
252            return Err(KernelError::FsError(FsError::CorruptedData));
253        }
254        Ok(Self {
255            magic,
256            entries: u16::from_le_bytes([data[2], data[3]]),
257            _max: u16::from_le_bytes([data[4], data[5]]),
258            depth: u16::from_le_bytes([data[6], data[7]]),
259        })
260    }
261}
262
263/// ext4 extent leaf (12 bytes)
264struct Extent {
265    /// First file block covered by this extent
266    block: u32,
267    /// Number of blocks
268    len: u16,
269    /// High 16 bits of physical block
270    start_hi: u16,
271    /// Low 32 bits of physical block
272    start_lo: u32,
273}
274
275impl Extent {
276    fn parse(data: &[u8]) -> Self {
277        Self {
278            block: u32::from_le_bytes([data[0], data[1], data[2], data[3]]),
279            len: u16::from_le_bytes([data[4], data[5]]),
280            start_hi: u16::from_le_bytes([data[6], data[7]]),
281            start_lo: u32::from_le_bytes([data[8], data[9], data[10], data[11]]),
282        }
283    }
284
285    fn physical_block(&self) -> u64 {
286        self.start_lo as u64 | ((self.start_hi as u64) << 32)
287    }
288
289    /// Actual length (high bit set means uninitialized)
290    fn actual_len(&self) -> u32 {
291        (self.len & 0x7FFF) as u32
292    }
293}
294
295/// ext4 extent index (12 bytes) -- for internal tree nodes
296struct ExtentIdx {
297    /// File block covered by subtree
298    block: u32,
299    /// Physical block of child node
300    leaf_lo: u32,
301    leaf_hi: u16,
302}
303
304impl ExtentIdx {
305    fn parse(data: &[u8]) -> Self {
306        Self {
307            block: u32::from_le_bytes([data[0], data[1], data[2], data[3]]),
308            leaf_lo: u32::from_le_bytes([data[4], data[5], data[6], data[7]]),
309            leaf_hi: u16::from_le_bytes([data[8], data[9]]),
310        }
311    }
312
313    fn child_block(&self) -> u64 {
314        self.leaf_lo as u64 | ((self.leaf_hi as u64) << 32)
315    }
316}
317
318/// Shared ext4 state holding the image data and metadata
319struct Ext4State {
320    sb: Ext4Superblock,
321    group_descs: Vec<BlockGroupDesc>,
322    /// Full image data (read-only)
323    data: Vec<u8>,
324}
325
326impl Ext4State {
327    /// Read a block from the image
328    fn read_block(&self, block: u64) -> Option<&[u8]> {
329        let bs = self.sb.block_size();
330        let offset = block as usize * bs;
331        let end = offset + bs;
332        if end <= self.data.len() {
333            Some(&self.data[offset..end])
334        } else {
335            None
336        }
337    }
338
339    /// Read an inode by number (1-indexed)
340    fn read_inode(&self, ino: u32) -> Result<Ext4Inode, KernelError> {
341        if ino == 0 || ino > self.sb.inodes_count {
342            return Err(KernelError::FsError(FsError::NotFound));
343        }
344
345        let group = ((ino - 1) / self.sb.inodes_per_group) as usize;
346        let index = ((ino - 1) % self.sb.inodes_per_group) as usize;
347
348        if group >= self.group_descs.len() {
349            return Err(KernelError::FsError(FsError::CorruptedData));
350        }
351
352        let table_block = self.group_descs[group].inode_table_block();
353        let bs = self.sb.block_size();
354        let inode_size = self.sb.inode_size as usize;
355        let offset = table_block as usize * bs + index * inode_size;
356
357        if offset + inode_size > self.data.len() {
358            return Err(KernelError::FsError(FsError::CorruptedData));
359        }
360
361        Ext4Inode::parse(&self.data[offset..offset + inode_size])
362    }
363
364    /// Read file data using the extent tree or block map
365    fn read_inode_data(&self, inode: &Ext4Inode, max_size: u64) -> Vec<u8> {
366        let size = core::cmp::min(inode.size(), max_size);
367        if size == 0 {
368            return Vec::new();
369        }
370
371        // For small symlinks, data is stored inline in i_block
372        if inode.is_symlink() && size < 60 {
373            return inode.block_data[..size as usize].to_vec();
374        }
375
376        if inode.uses_extents() {
377            self.read_extent_data(&inode.block_data, size)
378        } else {
379            self.read_block_map_data(&inode.block_data, size)
380        }
381    }
382
383    /// Read data via extent tree
384    fn read_extent_data(&self, block_data: &[u8; 60], size: u64) -> Vec<u8> {
385        let mut result = vec![0u8; size as usize];
386        let bs = self.sb.block_size();
387
388        // Parse extent tree from the inode's i_block area
389        self.walk_extent_tree(block_data, &mut result, bs);
390
391        result
392    }
393
394    /// Walk the extent tree recursively
395    fn walk_extent_tree(&self, tree_data: &[u8], result: &mut [u8], bs: usize) {
396        let header = match ExtentHeader::parse(tree_data) {
397            Ok(h) => h,
398            Err(_) => return,
399        };
400
401        if header.magic != EXT4_EXT_MAGIC {
402            return;
403        }
404
405        let entries = header.entries as usize;
406
407        if header.depth == 0 {
408            // Leaf level: read extents directly
409            for i in 0..entries {
410                let off = EXT4_EXTENT_HEADER_SIZE + i * EXT4_EXTENT_SIZE;
411                if off + EXT4_EXTENT_SIZE > tree_data.len() {
412                    break;
413                }
414                let extent = Extent::parse(&tree_data[off..]);
415                let file_offset = extent.block as usize * bs;
416                let phys_block = extent.physical_block();
417                let len = extent.actual_len() as usize;
418
419                for b in 0..len {
420                    let src_block = phys_block + b as u64;
421                    let dst_offset = file_offset + b * bs;
422                    if dst_offset >= result.len() {
423                        break;
424                    }
425
426                    if let Some(block_data) = self.read_block(src_block) {
427                        let copy_len = core::cmp::min(bs, result.len() - dst_offset);
428                        result[dst_offset..dst_offset + copy_len]
429                            .copy_from_slice(&block_data[..copy_len]);
430                    }
431                }
432            }
433        } else {
434            // Internal node: follow index entries
435            for i in 0..entries {
436                let off = EXT4_EXTENT_HEADER_SIZE + i * EXT4_EXTENT_SIZE;
437                if off + EXT4_EXTENT_SIZE > tree_data.len() {
438                    break;
439                }
440                let idx = ExtentIdx::parse(&tree_data[off..]);
441                if let Some(child_block) = self.read_block(idx.child_block()) {
442                    // Recurse into child node (use up to block_size bytes)
443                    let child_len = core::cmp::min(child_block.len(), bs);
444                    self.walk_extent_tree(&child_block[..child_len], result, bs);
445                }
446            }
447        }
448    }
449
450    /// Read data via legacy block map (direct + indirect blocks)
451    fn read_block_map_data(&self, block_data: &[u8; 60], size: u64) -> Vec<u8> {
452        let mut result = vec![0u8; size as usize];
453        let bs = self.sb.block_size();
454        let mut file_offset = 0usize;
455
456        // 12 direct block pointers at offsets 0..48
457        for i in 0..12 {
458            if file_offset >= result.len() {
459                return result;
460            }
461            let block_num = u32::from_le_bytes([
462                block_data[i * 4],
463                block_data[i * 4 + 1],
464                block_data[i * 4 + 2],
465                block_data[i * 4 + 3],
466            ]);
467            if block_num != 0 {
468                if let Some(bdata) = self.read_block(block_num as u64) {
469                    let copy_len = core::cmp::min(bs, result.len() - file_offset);
470                    result[file_offset..file_offset + copy_len].copy_from_slice(&bdata[..copy_len]);
471                }
472            }
473            file_offset += bs;
474        }
475
476        // Singly indirect block (offset 48)
477        let indirect = u32::from_le_bytes([
478            block_data[48],
479            block_data[49],
480            block_data[50],
481            block_data[51],
482        ]);
483        if indirect != 0 && file_offset < result.len() {
484            file_offset =
485                self.read_indirect_blocks(indirect as u64, &mut result, file_offset, bs, 0);
486        }
487
488        // Doubly indirect (offset 52)
489        let dindirect = u32::from_le_bytes([
490            block_data[52],
491            block_data[53],
492            block_data[54],
493            block_data[55],
494        ]);
495        if dindirect != 0 && file_offset < result.len() {
496            file_offset =
497                self.read_indirect_blocks(dindirect as u64, &mut result, file_offset, bs, 1);
498        }
499
500        // Triply indirect (offset 56)
501        let tindirect = u32::from_le_bytes([
502            block_data[56],
503            block_data[57],
504            block_data[58],
505            block_data[59],
506        ]);
507        if tindirect != 0 && file_offset < result.len() {
508            let _ = self.read_indirect_blocks(tindirect as u64, &mut result, file_offset, bs, 2);
509        }
510
511        result
512    }
513
514    /// Read through indirect block levels
515    fn read_indirect_blocks(
516        &self,
517        block: u64,
518        result: &mut [u8],
519        mut offset: usize,
520        bs: usize,
521        level: u32,
522    ) -> usize {
523        let bdata = match self.read_block(block) {
524            Some(d) => d,
525            None => return offset,
526        };
527
528        let ptrs_per_block = bs / 4;
529        for i in 0..ptrs_per_block {
530            if offset >= result.len() {
531                break;
532            }
533            let ptr = u32::from_le_bytes([
534                bdata[i * 4],
535                bdata[i * 4 + 1],
536                bdata[i * 4 + 2],
537                bdata[i * 4 + 3],
538            ]);
539            if ptr == 0 {
540                if level == 0 {
541                    offset += bs;
542                }
543                continue;
544            }
545
546            if level == 0 {
547                // Direct data block
548                if let Some(data_block) = self.read_block(ptr as u64) {
549                    let copy_len = core::cmp::min(bs, result.len() - offset);
550                    result[offset..offset + copy_len].copy_from_slice(&data_block[..copy_len]);
551                }
552                offset += bs;
553            } else {
554                // Recurse into next level
555                offset = self.read_indirect_blocks(ptr as u64, result, offset, bs, level - 1);
556            }
557        }
558        offset
559    }
560
561    /// Parse directory entries from raw data
562    fn parse_directory(&self, data: &[u8]) -> Vec<(String, u32, u8)> {
563        let mut entries = Vec::new();
564        let mut pos = 0;
565
566        while pos + 8 <= data.len() {
567            let inode =
568                u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
569            let rec_len = u16::from_le_bytes([data[pos + 4], data[pos + 5]]) as usize;
570            let name_len = data[pos + 6] as usize;
571            let file_type = data[pos + 7];
572
573            if rec_len == 0 {
574                break;
575            }
576
577            if inode != 0 && name_len > 0 && pos + 8 + name_len <= data.len() {
578                let name_bytes = &data[pos + 8..pos + 8 + name_len];
579                if let Ok(name) = core::str::from_utf8(name_bytes) {
580                    if name != "." && name != ".." {
581                        entries.push((String::from(name), inode, file_type));
582                    }
583                }
584            }
585
586            pos += rec_len;
587        }
588
589        entries
590    }
591}
592
593/// A cached ext4 directory/file/symlink node
594pub struct Ext4Node {
595    vfs_inode: u64,
596    ext4_ino: u32,
597    node_type: NodeType,
598    metadata: RwLock<Metadata>,
599    children: RwLock<BTreeMap<String, Arc<Ext4Node>>>,
600    fs_state: Arc<RwLock<Ext4State>>,
601    /// Symlink target (populated for symlinks)
602    link_target: RwLock<Option<String>>,
603}
604
605impl Ext4Node {
606    fn new(
607        ext4_ino: u32,
608        inode: &Ext4Inode,
609        _parent_inode: u64,
610        fs_state: Arc<RwLock<Ext4State>>,
611    ) -> Self {
612        let vfs_inode = EXT4_NEXT_INODE.fetch_add(1, Ordering::Relaxed);
613        Self {
614            vfs_inode,
615            ext4_ino,
616            node_type: inode.node_type(),
617            metadata: RwLock::new(Metadata {
618                inode: vfs_inode,
619                size: inode.size() as usize,
620                node_type: inode.node_type(),
621                permissions: inode.permissions(),
622                created: 0,
623                modified: inode.mtime as u64,
624                accessed: 0,
625                uid: 0,
626                gid: 0,
627            }),
628            children: RwLock::new(BTreeMap::new()),
629            fs_state,
630            link_target: RwLock::new(None),
631        }
632    }
633
634    /// Load children from disk if directory and not yet cached
635    fn ensure_children_loaded(&self) {
636        if self.node_type != NodeType::Directory {
637            return;
638        }
639
640        {
641            let children = self.children.read();
642            if !children.is_empty() {
643                return;
644            }
645        }
646
647        let entries = {
648            let state = self.fs_state.read();
649            let inode = match state.read_inode(self.ext4_ino) {
650                Ok(i) => i,
651                Err(_) => return,
652            };
653            let dir_data = state.read_inode_data(&inode, inode.size());
654            let parsed = state.parse_directory(&dir_data);
655
656            // Build child nodes while we still hold state
657            let mut result = Vec::new();
658            for (name, ino, ft) in parsed {
659                if let Ok(child_inode) = state.read_inode(ino) {
660                    result.push((name, ino, child_inode, ft));
661                }
662            }
663            result
664        };
665
666        let mut children = self.children.write();
667        for (name, ino, child_inode, _ft) in entries {
668            let child = Arc::new(Ext4Node::new(
669                ino,
670                &child_inode,
671                self.vfs_inode,
672                self.fs_state.clone(),
673            ));
674            children.insert(name, child);
675        }
676    }
677
678    /// Load symlink target
679    fn ensure_link_loaded(&self) {
680        if self.node_type != NodeType::Symlink {
681            return;
682        }
683
684        {
685            let link = self.link_target.read();
686            if link.is_some() {
687                return;
688            }
689        }
690
691        let target = {
692            let state = self.fs_state.read();
693            match state.read_inode(self.ext4_ino) {
694                Ok(inode) => {
695                    let data = state.read_inode_data(&inode, inode.size());
696                    String::from_utf8_lossy(&data).into_owned()
697                }
698                Err(_) => return,
699            }
700        };
701
702        let mut link = self.link_target.write();
703        *link = Some(target);
704    }
705}
706
707impl VfsNode for Ext4Node {
708    fn node_type(&self) -> NodeType {
709        self.node_type
710    }
711
712    fn read(&self, offset: usize, buffer: &mut [u8]) -> Result<usize, KernelError> {
713        if self.node_type != NodeType::File {
714            return Err(KernelError::FsError(FsError::NotAFile));
715        }
716
717        let meta = self.metadata.read();
718        if offset >= meta.size {
719            return Ok(0);
720        }
721
722        let file_data = {
723            let state = self.fs_state.read();
724            let inode = state.read_inode(self.ext4_ino)?;
725            state.read_inode_data(&inode, inode.size())
726        };
727
728        let bytes_to_read = core::cmp::min(buffer.len(), file_data.len().saturating_sub(offset));
729        buffer[..bytes_to_read].copy_from_slice(&file_data[offset..offset + bytes_to_read]);
730
731        Ok(bytes_to_read)
732    }
733
734    fn write(&self, _offset: usize, _data: &[u8]) -> Result<usize, KernelError> {
735        Err(KernelError::FsError(FsError::ReadOnly))
736    }
737
738    fn metadata(&self) -> Result<Metadata, KernelError> {
739        Ok(self.metadata.read().clone())
740    }
741
742    fn readdir(&self) -> Result<Vec<DirEntry>, KernelError> {
743        if self.node_type != NodeType::Directory {
744            return Err(KernelError::FsError(FsError::NotADirectory));
745        }
746
747        self.ensure_children_loaded();
748        let children = self.children.read();
749
750        let entries = children
751            .iter()
752            .map(|(name, node)| DirEntry {
753                name: name.clone(),
754                inode: node.vfs_inode,
755                node_type: node.node_type,
756            })
757            .collect();
758
759        Ok(entries)
760    }
761
762    fn lookup(&self, name: &str) -> Result<Arc<dyn VfsNode>, KernelError> {
763        if self.node_type != NodeType::Directory {
764            return Err(KernelError::FsError(FsError::NotADirectory));
765        }
766
767        self.ensure_children_loaded();
768        let children = self.children.read();
769        children
770            .get(name)
771            .map(|n| n.clone() as Arc<dyn VfsNode>)
772            .ok_or(KernelError::FsError(FsError::NotFound))
773    }
774
775    fn create(
776        &self,
777        _name: &str,
778        _permissions: Permissions,
779    ) -> Result<Arc<dyn VfsNode>, KernelError> {
780        Err(KernelError::FsError(FsError::ReadOnly))
781    }
782
783    fn mkdir(
784        &self,
785        _name: &str,
786        _permissions: Permissions,
787    ) -> Result<Arc<dyn VfsNode>, KernelError> {
788        Err(KernelError::FsError(FsError::ReadOnly))
789    }
790
791    fn unlink(&self, _name: &str) -> Result<(), KernelError> {
792        Err(KernelError::FsError(FsError::ReadOnly))
793    }
794
795    fn truncate(&self, _size: usize) -> Result<(), KernelError> {
796        Err(KernelError::FsError(FsError::ReadOnly))
797    }
798
799    fn readlink(&self) -> Result<String, KernelError> {
800        if self.node_type != NodeType::Symlink {
801            return Err(KernelError::FsError(FsError::NotASymlink));
802        }
803        self.ensure_link_loaded();
804        let link = self.link_target.read();
805        link.clone().ok_or(KernelError::FsError(FsError::IoError))
806    }
807
808    fn chmod(&self, _permissions: Permissions) -> Result<(), KernelError> {
809        Err(KernelError::FsError(FsError::ReadOnly))
810    }
811}
812
813/// ext4 filesystem (read-only)
814pub struct Ext4Fs {
815    root: Arc<Ext4Node>,
816    state: Arc<RwLock<Ext4State>>,
817}
818
819impl Ext4Fs {
820    /// Create an ext4 filesystem from raw image data (read-only).
821    pub fn from_image(image_data: &[u8]) -> Result<Self, KernelError> {
822        if image_data.len() < SUPERBLOCK_OFFSET + 256 {
823            return Err(KernelError::FsError(FsError::CorruptedData));
824        }
825
826        let sb = Ext4Superblock::parse(&image_data[SUPERBLOCK_OFFSET..])?;
827        let bs = sb.block_size();
828
829        // Parse block group descriptors
830        // They start at the block after the superblock
831        let gd_block = if bs == 1024 { 2 } else { 1 };
832        let gd_offset = gd_block * bs;
833        let num_groups = sb.num_block_groups() as usize;
834        let desc_size = sb.desc_size as usize;
835
836        let mut group_descs = Vec::with_capacity(num_groups);
837        for i in 0..num_groups {
838            let off = gd_offset + i * desc_size;
839            if off + desc_size > image_data.len() {
840                break;
841            }
842            group_descs.push(BlockGroupDesc::parse(
843                &image_data[off..off + desc_size],
844                sb.desc_size,
845            ));
846        }
847
848        let state = Arc::new(RwLock::new(Ext4State {
849            sb: sb.clone(),
850            group_descs,
851            data: image_data.to_vec(),
852        }));
853
854        // Read root inode (inode 2)
855        let root_inode = {
856            let st = state.read();
857            st.read_inode(EXT4_ROOT_INO)?
858        };
859
860        let root = Arc::new(Ext4Node::new(EXT4_ROOT_INO, &root_inode, 0, state.clone()));
861
862        Ok(Self { root, state })
863    }
864}
865
866impl Filesystem for Ext4Fs {
867    fn root(&self) -> Arc<dyn VfsNode> {
868        self.root.clone() as Arc<dyn VfsNode>
869    }
870
871    fn name(&self) -> &str {
872        "ext4"
873    }
874
875    fn is_readonly(&self) -> bool {
876        true
877    }
878
879    fn sync(&self) -> Result<(), KernelError> {
880        Ok(()) // Read-only, nothing to sync
881    }
882}
883
884#[cfg(test)]
885mod tests {
886    use super::*;
887
888    /// Create a minimal ext4 image for testing.
889    /// This creates a tiny ext4 volume with:
890    /// - 1KB block size (log_block_size = 0)
891    /// - 1 block group
892    /// - Root directory with one file "hello.txt" containing "Hello, ext4!\n"
893    fn make_test_ext4_image() -> Vec<u8> {
894        let block_size = 1024usize;
895        let total_blocks = 64u32;
896        let total_size = total_blocks as usize * block_size;
897        let mut img = vec![0u8; total_size];
898
899        // === Superblock at offset 1024 (block 1) ===
900        let sb_off = SUPERBLOCK_OFFSET;
901        let inodes_count = 16u32;
902        let blocks_per_group = total_blocks;
903        let inodes_per_group = inodes_count;
904        let inode_size = 128u16;
905
906        // s_inodes_count (0x00)
907        img[sb_off..sb_off + 4].copy_from_slice(&inodes_count.to_le_bytes());
908        // s_blocks_count_lo (0x04)
909        img[sb_off + 0x04..sb_off + 0x08].copy_from_slice(&total_blocks.to_le_bytes());
910        // s_first_data_block (0x14) - 1 for 1KB blocks
911        img[sb_off + 0x14..sb_off + 0x18].copy_from_slice(&1u32.to_le_bytes());
912        // s_log_block_size (0x18) - 0 means 1024
913        img[sb_off + 0x18..sb_off + 0x1C].copy_from_slice(&0u32.to_le_bytes());
914        // s_blocks_per_group (0x20)
915        img[sb_off + 0x20..sb_off + 0x24].copy_from_slice(&blocks_per_group.to_le_bytes());
916        // s_inodes_per_group (0x28)
917        img[sb_off + 0x28..sb_off + 0x2C].copy_from_slice(&inodes_per_group.to_le_bytes());
918        // s_magic (0x38)
919        img[sb_off + 0x38..sb_off + 0x3A].copy_from_slice(&EXT4_SUPER_MAGIC.to_le_bytes());
920        // s_inode_size (0x58)
921        img[sb_off + 0x58..sb_off + 0x5A].copy_from_slice(&inode_size.to_le_bytes());
922        // s_desc_size (0xFE) - 0 means 32 bytes (old format)
923        img[sb_off + 0xFE..sb_off + 0x100].copy_from_slice(&0u16.to_le_bytes());
924
925        // === Block Group Descriptor at block 2 (offset 2048) ===
926        let gd_off = 2 * block_size;
927        // bg_inode_table_lo (offset 8) - inode table at block 4
928        let inode_table_block = 4u32;
929        img[gd_off + 8..gd_off + 12].copy_from_slice(&inode_table_block.to_le_bytes());
930
931        // === Inode Table at block 4 (offset 4096) ===
932        let it_off = inode_table_block as usize * block_size;
933
934        // --- Root inode (inode 2, index 1) at it_off + 128 ---
935        let root_off = it_off + 1 * inode_size as usize;
936        // i_mode: directory (0x4000) + rwxr-xr-x (0o755) = 0x41ED
937        img[root_off..root_off + 2].copy_from_slice(&0x41EDu16.to_le_bytes());
938        // i_size_lo: directory data size (1 block)
939        img[root_off + 4..root_off + 8].copy_from_slice(&(block_size as u32).to_le_bytes());
940        // i_flags: no extents, use legacy block map
941        img[root_off + 0x20..root_off + 0x24].copy_from_slice(&0u32.to_le_bytes());
942        // i_block[0]: direct block 10 (directory data)
943        let root_data_block = 10u32;
944        img[root_off + 0x28..root_off + 0x2C].copy_from_slice(&root_data_block.to_le_bytes());
945
946        // --- File inode (inode 3, index 2) at it_off + 256 ---
947        let file_content = b"Hello, ext4!\n";
948        let file_off = it_off + 2 * inode_size as usize;
949        // i_mode: regular file (0x8000) + rw-r--r-- (0o644) = 0x81A4
950        img[file_off..file_off + 2].copy_from_slice(&0x81A4u16.to_le_bytes());
951        // i_size_lo
952        img[file_off + 4..file_off + 8].copy_from_slice(&(file_content.len() as u32).to_le_bytes());
953        // i_flags: no extents
954        img[file_off + 0x20..file_off + 0x24].copy_from_slice(&0u32.to_le_bytes());
955        // i_block[0]: direct block 11
956        let file_data_block = 11u32;
957        img[file_off + 0x28..file_off + 0x2C].copy_from_slice(&file_data_block.to_le_bytes());
958
959        // --- Symlink inode (inode 4, index 3) at it_off + 384 ---
960        let link_target = b"hello.txt";
961        let link_off = it_off + 3 * inode_size as usize;
962        // i_mode: symlink (0xA000) + rwxrwxrwx (0o777) = 0xA1FF
963        img[link_off..link_off + 2].copy_from_slice(&0xA1FFu16.to_le_bytes());
964        // i_size_lo
965        img[link_off + 4..link_off + 8].copy_from_slice(&(link_target.len() as u32).to_le_bytes());
966        // Inline symlink: target stored in i_block (< 60 bytes)
967        img[link_off + 0x28..link_off + 0x28 + link_target.len()].copy_from_slice(link_target);
968
969        // === Root directory data at block 10 ===
970        let dir_off = root_data_block as usize * block_size;
971
972        // Entry 1: "." -> inode 2
973        let mut pos = dir_off;
974        img[pos..pos + 4].copy_from_slice(&2u32.to_le_bytes()); // inode
975        img[pos + 4..pos + 6].copy_from_slice(&12u16.to_le_bytes()); // rec_len
976        img[pos + 6] = 1; // name_len
977        img[pos + 7] = EXT4_FT_DIR; // file_type
978        img[pos + 8] = b'.';
979        pos += 12;
980
981        // Entry 2: ".." -> inode 2
982        img[pos..pos + 4].copy_from_slice(&2u32.to_le_bytes());
983        img[pos + 4..pos + 6].copy_from_slice(&12u16.to_le_bytes());
984        img[pos + 6] = 2;
985        img[pos + 7] = EXT4_FT_DIR;
986        img[pos + 8] = b'.';
987        img[pos + 9] = b'.';
988        pos += 12;
989
990        // Entry 3: "hello.txt" -> inode 3
991        let name = b"hello.txt";
992        img[pos..pos + 4].copy_from_slice(&3u32.to_le_bytes());
993        img[pos + 4..pos + 6].copy_from_slice(&20u16.to_le_bytes()); // rec_len (aligned to 4)
994        img[pos + 6] = name.len() as u8;
995        img[pos + 7] = EXT4_FT_REG_FILE;
996        img[pos + 8..pos + 8 + name.len()].copy_from_slice(name);
997        pos += 20;
998
999        // Entry 4: "link" -> inode 4
1000        let link_name = b"link";
1001        // rec_len = rest of block
1002        let rec_len = (block_size - (pos - dir_off)) as u16;
1003        img[pos..pos + 4].copy_from_slice(&4u32.to_le_bytes());
1004        img[pos + 4..pos + 6].copy_from_slice(&rec_len.to_le_bytes());
1005        img[pos + 6] = link_name.len() as u8;
1006        img[pos + 7] = EXT4_FT_SYMLINK;
1007        img[pos + 8..pos + 8 + link_name.len()].copy_from_slice(link_name);
1008
1009        // === File data at block 11 ===
1010        let fdata_off = file_data_block as usize * block_size;
1011        img[fdata_off..fdata_off + file_content.len()].copy_from_slice(file_content);
1012
1013        img
1014    }
1015
1016    #[test]
1017    fn test_superblock_parse() {
1018        let img = make_test_ext4_image();
1019        let sb = Ext4Superblock::parse(&img[SUPERBLOCK_OFFSET..]).unwrap();
1020        assert_eq!(sb.magic, EXT4_SUPER_MAGIC);
1021        assert_eq!(sb.block_size(), 1024);
1022        assert_eq!(sb.inodes_count, 16);
1023        assert_eq!(sb.inode_size, 128);
1024    }
1025
1026    #[test]
1027    fn test_mount_and_list_root() {
1028        let img = make_test_ext4_image();
1029        let fs = Ext4Fs::from_image(&img).unwrap();
1030        assert_eq!(fs.name(), "ext4");
1031        assert!(fs.is_readonly());
1032
1033        let root = fs.root();
1034        assert_eq!(root.node_type(), NodeType::Directory);
1035
1036        let entries = root.readdir().unwrap();
1037        let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
1038        assert!(names.contains(&"hello.txt"));
1039        assert!(names.contains(&"link"));
1040    }
1041
1042    #[test]
1043    fn test_read_file() {
1044        let img = make_test_ext4_image();
1045        let fs = Ext4Fs::from_image(&img).unwrap();
1046        let root = fs.root();
1047
1048        let file = root.lookup("hello.txt").unwrap();
1049        assert_eq!(file.node_type(), NodeType::File);
1050
1051        let mut buf = [0u8; 64];
1052        let n = file.read(0, &mut buf).unwrap();
1053        assert_eq!(&buf[..n], b"Hello, ext4!\n");
1054    }
1055
1056    #[test]
1057    fn test_read_symlink() {
1058        let img = make_test_ext4_image();
1059        let fs = Ext4Fs::from_image(&img).unwrap();
1060        let root = fs.root();
1061
1062        let link = root.lookup("link").unwrap();
1063        assert_eq!(link.node_type(), NodeType::Symlink);
1064
1065        let target = link.readlink().unwrap();
1066        assert_eq!(target, "hello.txt");
1067    }
1068
1069    #[test]
1070    fn test_read_only_enforcement() {
1071        let img = make_test_ext4_image();
1072        let fs = Ext4Fs::from_image(&img).unwrap();
1073        let root = fs.root();
1074
1075        assert!(root.write(0, b"test").is_err());
1076        assert!(root.create("new", Permissions::default()).is_err());
1077        assert!(root.mkdir("dir", Permissions::default()).is_err());
1078        assert!(root.unlink("hello.txt").is_err());
1079        assert!(root.truncate(0).is_err());
1080    }
1081
1082    #[test]
1083    fn test_metadata() {
1084        let img = make_test_ext4_image();
1085        let fs = Ext4Fs::from_image(&img).unwrap();
1086        let root = fs.root();
1087
1088        let file = root.lookup("hello.txt").unwrap();
1089        let meta = file.metadata().unwrap();
1090        assert_eq!(meta.size, 13); // "Hello, ext4!\n"
1091        assert_eq!(meta.node_type, NodeType::File);
1092        assert!(meta.permissions.owner_read);
1093        assert!(meta.permissions.owner_write);
1094    }
1095
1096    #[test]
1097    fn test_invalid_magic() {
1098        let mut img = vec![0u8; 2048];
1099        // No valid superblock
1100        assert!(Ext4Fs::from_image(&img).is_err());
1101
1102        // Set wrong magic
1103        img[SUPERBLOCK_OFFSET + 0x38] = 0xFF;
1104        img[SUPERBLOCK_OFFSET + 0x39] = 0xFF;
1105        assert!(Ext4Fs::from_image(&img).is_err());
1106    }
1107
1108    #[test]
1109    fn test_file_not_found() {
1110        let img = make_test_ext4_image();
1111        let fs = Ext4Fs::from_image(&img).unwrap();
1112        let root = fs.root();
1113
1114        assert!(root.lookup("nonexistent").is_err());
1115    }
1116
1117    #[test]
1118    fn test_read_at_offset() {
1119        let img = make_test_ext4_image();
1120        let fs = Ext4Fs::from_image(&img).unwrap();
1121        let root = fs.root();
1122
1123        let file = root.lookup("hello.txt").unwrap();
1124        let mut buf = [0u8; 64];
1125        let n = file.read(7, &mut buf).unwrap();
1126        assert_eq!(&buf[..n], b"ext4!\n");
1127    }
1128
1129    #[test]
1130    fn test_read_past_end() {
1131        let img = make_test_ext4_image();
1132        let fs = Ext4Fs::from_image(&img).unwrap();
1133        let root = fs.root();
1134
1135        let file = root.lookup("hello.txt").unwrap();
1136        let mut buf = [0u8; 64];
1137        let n = file.read(100, &mut buf).unwrap();
1138        assert_eq!(n, 0);
1139    }
1140}