keos_project5/ffs/disk_layout.rs
1//! **on-disk layout** of the file system's metadata structures.
2//!
3//! This module defines the **on-disk layout** of the file system's core
4//! metadata structures. These types represent raw disk-resident data such as
5//! the superblock, block/inode allocation bitmaps, and inode table. Each struct
6//! in this module is tightly packed and designed to match the exact binary
7//! layout used by the file system when persisting and loading from disk.
8//!
9//! This module defines [`MetaData`] trait, and support [`MetaData::load`]
10//! method, allowing them to be generically loaded from a logical block address
11//! (LBA).
12//!
13//! ## Examples of accessing metadata.
14//! ```
15//! use crate::ffs::disk_layout::{MetaData, BlockBitmap};
16//!
17//! let block_bitmap = BlockBitmap::load(ffs, lba)?; // read BlockPointsTo<BlockBitMap> from lba.
18//! { // READ
19//! let guard = block_bitmap.read(); // reading metadata does not requires transaction.
20//! assert!(guard.is_allocated(0));
21//! }
22//! { // WRITE
23//! let tx = ffs.open_transaction(); // Open a new transaction. Only a single transaction can be existed.
24//! let mut guard = block_bitmap.write(&tx); // writing metadata requires transaction.
25//! guard.try_allocate(1);
26//! guard.submit(); // Submit the modified change to the transaction.
27//! }
28//! ```
29use crate::ffs::{
30 FastFileSystemInner, InodeNumber, JournalIO, LogicalBlockAddress, access_control::MetaData,
31};
32use alloc::boxed::Box;
33use core::fmt::Debug;
34use keos::{KernelError, fs::Disk};
35
36/// A struct for denying implementing [`MetaData`] from outside of this module.
37#[doc(hidden)]
38pub struct Private {
39 _p: (),
40}
41
42/// On-disk representation of the superblock of the Fast File System (FFS).
43///
44/// The superblock contains essential metadata about the filesystem,
45/// including the total number of blocks and inodes, as well as the
46/// size of the journal.
47#[repr(C)]
48#[derive(Clone, Copy)]
49pub struct SuperBlock {
50 /// File system magic: "KeOSFFS\0".
51 pub magic: [u8; 8],
52 /// Total number of blocks in the filesystem.
53 pub block_count: u64,
54 /// In-used count of blocks in the filesystem.
55 pub block_count_inused: u64,
56 /// Total number of inodes in the filesystem.
57 pub inode_count: u64,
58 /// In-used count of inodes in the filesystem.
59 pub inode_count_inused: u64,
60 /// A indicator that this filesystem have journaling feature.
61 pub has_journal: u64,
62 /// Padding to align to Block size.
63 pub _pad: [u8; 4096 - core::mem::size_of::<u64>() * 5 - 8],
64}
65
66impl Default for SuperBlock {
67 fn default() -> Self {
68 Self {
69 magic: [0; 8],
70 block_count: 0,
71 block_count_inused: 0,
72 inode_count: 0,
73 inode_count_inused: 0,
74 has_journal: 0,
75 _pad: [0; 4096 - 48],
76 }
77 }
78}
79
80impl Debug for SuperBlock {
81 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
82 f.debug_struct("SuperBlock")
83 .field("magic", &self.magic)
84 .field("block_count", &self.block_count)
85 .field("block_count_inused", &self.block_count_inused)
86 .field("inode_count", &self.inode_count)
87 .field("inode_count_used", &self.inode_count_inused)
88 .field("has_journal", &(self.has_journal != 0))
89 .finish()
90 }
91}
92
93impl MetaData for SuperBlock {
94 const P: Private = Private { _p: () };
95 fn load(
96 _ffs: &FastFileSystemInner,
97 _lba: LogicalBlockAddress,
98 ) -> Result<super::access_control::BlockPointsTo<Self>, KernelError> {
99 unreachable!()
100 }
101}
102const_assert!(core::mem::size_of::<SuperBlock>() == 4096);
103
104/// Represents the on-disk block allocation bitmap for the file system.
105///
106/// Each bit in the [`BlockBitmap`] corresponds to a single block on disk.
107/// A bit value of `1` indicates that the block is in use, while `0`
108/// means the block is free and available for allocation.
109#[repr(C, packed)]
110pub struct BlockBitmap {
111 bits: [u64; 4096 / 8],
112}
113
114impl Default for BlockBitmap {
115 fn default() -> Self {
116 BlockBitmap {
117 bits: [0; 4096 / 8],
118 }
119 }
120}
121
122impl BlockBitmap {
123 /// Checks whether a block at the given position is allocated.
124 ///
125 /// # Parameters
126 /// - `pos`: The index of the block to check.
127 ///
128 /// # Returns
129 /// - `true` if the block is currently marked as allocated (bit is 1).
130 /// - `false` if the block is free (bit is 0).
131 ///
132 /// This method is used to determine the allocation status of a block
133 /// in the file system's block bitmap.
134 pub fn is_allocated(&self, pos: usize) -> bool {
135 let (pos, off) = (pos / 64, pos % 64);
136 self.bits[pos] & (1 << off) != 0
137 }
138
139 /// Attempts to allocate a block at the given position.
140 ///
141 /// # Parameters
142 /// - `pos`: The index of the block to allocate.
143 ///
144 /// # Returns
145 /// - `true` if the block was previously free and is now marked as
146 /// allocated.
147 /// - `false` if the block was already allocated (no change).
148 ///
149 /// This method is used during block allocation to claim a free block.
150 /// If the block is already allocated, it fails without modifying the
151 /// bitmap.
152 pub fn try_allocate(&mut self, pos: usize) -> bool {
153 let (pos, off) = (pos / 64, pos % 64);
154 if self.bits[pos] & (1 << off) == 0 {
155 self.bits[pos] |= 1 << off;
156 true
157 } else {
158 false
159 }
160 }
161
162 /// deallocate a block at the given position.
163 ///
164 /// # Parameters
165 /// - `pos`: The index of the block to deallocate.
166 ///
167 /// # Returns
168 /// - `true` if the block was previously free and is now marked as
169 /// allocated.
170 /// - `false` if the block was already allocated (no change).
171 ///
172 /// This method is used during block allocation to claim a free block.
173 /// If the block is already allocated, it fails without modifying the
174 /// bitmap.
175 pub fn deallocate(&mut self, pos: usize) -> bool {
176 let (pos, off) = (pos / 64, pos % 64);
177 if self.bits[pos] & (1 << off) != 0 {
178 self.bits[pos] &= !(1 << off);
179 true
180 } else {
181 false
182 }
183 }
184}
185
186impl MetaData for BlockBitmap {
187 const P: Private = Private { _p: () };
188}
189const_assert!(core::mem::size_of::<BlockBitmap>() == 4096);
190
191/// Represents the on-disk inode allocation bitmap for the file system.
192///
193/// Each bit in the [`InodeBitmap`] corresponds to a single inode on disk.
194/// A bit value of `1` indicates that the inode is in use, while `0`
195/// means the inode is free and available for allocation.
196#[repr(C, packed)]
197pub struct InodeBitmap {
198 bits: [u64; 4096 / 8],
199}
200
201impl Default for InodeBitmap {
202 fn default() -> Self {
203 InodeBitmap {
204 bits: [0; 4096 / 8],
205 }
206 }
207}
208
209impl InodeBitmap {
210 /// Checks whether a inode at the given position is allocated.
211 ///
212 /// # Parameters
213 /// - `pos`: The index of the inode to check.
214 ///
215 /// # Returns
216 /// - `true` if the inode is currently marked as allocated (bit is 1).
217 /// - `false` if the inode is free (bit is 0).
218 ///
219 /// This method is used to determine the allocation status of a inode
220 /// in the file system's inode bitmap.
221 pub fn is_allocated(&self, pos: usize) -> bool {
222 let (pos, off) = (pos / 64, pos % 64);
223 self.bits[pos] & (1 << off) != 0
224 }
225
226 /// Attempts to allocate a inode at the given position.
227 ///
228 /// # Parameters
229 /// - `pos`: The index of the inode to allocate.
230 ///
231 /// # Returns
232 /// - `true` if the inode was previously free and is now marked as
233 /// allocated.
234 /// - `false` if the inode was already allocated (no change).
235 ///
236 /// This method is used during inode allocation to claim a free inode.
237 /// If the inode is already allocated, it fails without modifying the
238 /// bitmap.
239 pub fn try_allocate(&mut self, pos: usize) -> bool {
240 let (pos, off) = (pos / 64, pos % 64);
241 if self.bits[pos] & (1 << off) == 0 {
242 self.bits[pos] |= 1 << off;
243 true
244 } else {
245 false
246 }
247 }
248
249 pub fn deallocate(&mut self, pos: usize) -> bool {
250 let (pos, off) = (pos / 64, pos % 64);
251 if self.bits[pos] & (1 << off) != 0 {
252 self.bits[pos] &= !(1 << off);
253 true
254 } else {
255 false
256 }
257 }
258}
259
260impl MetaData for InodeBitmap {
261 const P: Private = Private { _p: () };
262}
263const_assert!(core::mem::size_of::<InodeBitmap>() == 4096);
264
265/// Represent a single inode within a inode array.
266#[derive(Debug)]
267#[repr(C, packed)]
268pub struct Inode {
269 /// File system magic: "KeOSFFSI".
270 pub magic: [u8; 8],
271 /// The unique inode number assigned to this file or directory.
272 pub ino: Option<InodeNumber>,
273 /// The type of the file (e.g., regular file, directory, symbolic link).
274 ///
275 /// Uses a `u32` to store values corresponding to [`FileType`].
276 ///
277 /// [`FileType`]: crate::ffs::types::FileType
278 pub ftype: u32,
279 /// The total size of the file in bytes.
280 pub size: u64,
281 /// The number of links alive in the file system.
282 pub link_count: u64,
283 /// Directly mapped data blocks.
284 ///
285 /// These 12 blocks store the first portions of a file's data, allowing
286 /// for efficient access to small files without requiring indirect blocks.
287 pub dblocks: [Option<LogicalBlockAddress>; 12],
288 /// An indirect block, which contains pointers to additional data
289 /// blocks.
290 ///
291 /// This extends the file size capability beyond direct blocks by storing
292 /// an array of logical block addresses in a separate block.
293 pub iblock: Option<LogicalBlockAddress>,
294 /// A doubly indirect block, which contains pointers to indirect
295 /// blocks.
296 ///
297 /// This allows for even larger file sizes by introducing an extra level
298 /// of indirection.
299 pub diblock: Option<LogicalBlockAddress>,
300 /// A padding to align to the power of two.
301 pub _pad: [u8; 112],
302}
303
304impl Default for Inode {
305 fn default() -> Self {
306 Self {
307 magic: [0; 8],
308 ino: None,
309 ftype: 0,
310 size: 0,
311 link_count: 0,
312 dblocks: [None; 12],
313 iblock: None,
314 diblock: None,
315 _pad: [0; 112],
316 }
317 }
318}
319const_assert!(core::mem::size_of::<Inode>() == 256);
320
321/// Represents a fixed-size array of inodes loaded from a block.
322///
323/// Each [`InodeArray`] holds a collection of [`Inode`] structures that are
324/// packed into a single 4KB block. The number of inodes it contains is computed
325/// based on the size of the [`Inode`] type.
326#[derive(Default)]
327#[repr(C, packed)]
328pub struct InodeArray {
329 inodes: [Inode; 4096 / core::mem::size_of::<Inode>()],
330}
331
332impl core::ops::Deref for InodeArray {
333 type Target = [Inode; 4096 / core::mem::size_of::<Inode>()];
334 fn deref(&self) -> &Self::Target {
335 &self.inodes
336 }
337}
338
339impl core::ops::DerefMut for InodeArray {
340 fn deref_mut(&mut self) -> &mut Self::Target {
341 &mut self.inodes
342 }
343}
344
345impl MetaData for InodeArray {
346 const P: Private = Private { _p: () };
347}
348const_assert!(core::mem::size_of::<InodeArray>() == 4096);
349
350/// Represents an indirect block in the filesystem.
351///
352/// An indirect block is used to extend the number of data blocks that an inode
353/// can reference. Instead of storing data directly, it contains a list of
354/// logical block addresses (LBAs), each pointing to a separate data block on
355/// disk.
356///
357/// This structure allows files to grow beyond the direct block limit imposed by
358/// the inode structure.
359///
360/// # Usage
361/// Typically used as part of indirect, or double-indirectblock addressing
362/// schemes to support large file sizes.
363#[repr(C)]
364pub struct IndirectBlock {
365 lbas: [Option<LogicalBlockAddress>; 512],
366}
367
368impl Default for IndirectBlock {
369 fn default() -> Self {
370 Self { lbas: [None; 512] }
371 }
372}
373
374impl core::ops::Deref for IndirectBlock {
375 type Target = [Option<LogicalBlockAddress>; 512];
376 fn deref(&self) -> &Self::Target {
377 &self.lbas
378 }
379}
380
381impl core::ops::DerefMut for IndirectBlock {
382 fn deref_mut(&mut self) -> &mut Self::Target {
383 &mut self.lbas
384 }
385}
386
387impl MetaData for IndirectBlock {
388 const P: Private = Private { _p: () };
389}
390
391const_assert!(core::mem::size_of::<IndirectBlock>() == 4096);
392
393/// Represent a single directory entry within a directory block.
394///
395/// Each entry stores metadata for necessary to locate a file or subdirectory.
396#[repr(C)]
397#[derive(Clone, Copy, Debug)]
398pub struct DirectoryBlockEntry {
399 /// The inode associated with this directory entry.
400 /// - `Some(inode)`: a valid file or directory.
401 /// - `None`: an unused or deleted entry.
402 pub inode: Option<InodeNumber>,
403 /// The length of the file or directory name stored in `name`.
404 /// This indicates how many bytes in the `name` array are valid.
405 pub name_len: u8,
406 /// The name of the file or directory.
407 /// Only the first `name_len` bytes are meaningful.
408 pub name: [u8; 251],
409}
410
411impl Default for DirectoryBlockEntry {
412 fn default() -> Self {
413 Self {
414 inode: None,
415 name_len: 0,
416 name: [0; 251],
417 }
418 }
419}
420
421impl DirectoryBlockEntry {
422 /// Constructs a new directory entry from an inode number and name.
423 ///
424 /// Returns `None` if the name is too long to fit in the directory entry.
425 ///
426 /// # Arguments
427 /// - `ino`: The inode number associated with the entry.
428 /// - `name`: The name of the file or directory.
429 ///
430 /// # Returns
431 /// - `Some(Self)`: A valid directory entry.
432 /// - `None`: If the name is too long to fit.
433 pub fn from_ino_name(ino: InodeNumber, name: &str) -> Option<Self> {
434 let name_len = name.len();
435 if name_len <= 251 {
436 let mut out = DirectoryBlockEntry {
437 inode: Some(ino),
438 name_len: name_len as u8,
439 name: [0; 251],
440 };
441 out.name[..name_len].copy_from_slice(name.as_bytes());
442 Some(out)
443 } else {
444 None
445 }
446 }
447
448 /// Returns the name of the directory entry as a string slice.
449 ///
450 /// # Returns
451 /// - `Some(&str)`: If the name is valid UTF-8.
452 /// - `None`: If the name contains invalid UTF-8 bytes.
453 pub fn name(&self) -> Option<&str> {
454 self.inode
455 .and_then(|_| core::str::from_utf8(&self.name[..self.name_len as usize]).ok())
456 }
457}
458
459const_assert!(core::mem::size_of::<DirectoryBlockEntry>() == 256);
460
461/// Represents a block that contains multiple directory entries.
462///
463/// A directory is composed of one or more of these blocks, each
464/// containing a fixed-size array of directory entries.
465#[repr(C)]
466pub struct DirectoryBlock {
467 entries: [DirectoryBlockEntry; 4096 / core::mem::size_of::<DirectoryBlockEntry>()],
468}
469
470impl Default for DirectoryBlock {
471 fn default() -> Self {
472 Self {
473 entries: [DirectoryBlockEntry::default();
474 4096 / core::mem::size_of::<DirectoryBlockEntry>()],
475 }
476 }
477}
478
479impl core::ops::Deref for DirectoryBlock {
480 type Target = [DirectoryBlockEntry; 4096 / core::mem::size_of::<DirectoryBlockEntry>()];
481 fn deref(&self) -> &Self::Target {
482 &self.entries
483 }
484}
485
486impl core::ops::DerefMut for DirectoryBlock {
487 fn deref_mut(&mut self) -> &mut Self::Target {
488 &mut self.entries
489 }
490}
491
492impl MetaData for DirectoryBlock {
493 const P: Private = Private { _p: () };
494}
495
496const_assert!(core::mem::size_of::<DirectoryBlock>() == 4096);
497
498/// Represents the on-disk metadata for the journal superblock.
499#[repr(C, packed)]
500pub struct JournalSb {
501 /// Journal magic: "KeOSJOUR".
502 pub magic: [u8; 8],
503 /// Indicate journal has been commited.
504 pub commited: u64,
505 /// Transaction id.
506 pub tx_id: u64,
507 /// Padding to fill a full block (4096 bytes).
508 _pad: [u8; 4096 - 24],
509}
510
511impl Default for JournalSb {
512 fn default() -> Self {
513 Self {
514 magic: [0; 8],
515 commited: 0,
516 tx_id: 0,
517 _pad: [0; 4096 - 24],
518 }
519 }
520}
521
522impl JournalSb {
523 /// Loads the journal superblock from disk.
524 ///
525 /// # Arguments
526 /// - `disk`: The underlying disk interface.
527 /// - `lba`: The logical block address where the journal superblock is
528 /// located.
529 ///
530 /// # Returns
531 /// - `Ok(Box<Self>)`: A parsed journal superblock.
532 /// - `Err(KernelError)`: If the block could not be read or parsed.
533 pub fn from_disk(disk: &Disk, lba: LogicalBlockAddress) -> Result<Box<Self>, KernelError> {
534 let mut b = Box::new(JournalSb::default());
535 {
536 let inner =
537 unsafe { core::slice::from_raw_parts_mut(&mut *b as *mut _ as *mut u8, 4096) };
538 for i in 0..8 {
539 disk.read(
540 lba.into_sector() + i,
541 inner[512 * i..512 * (i + 1)].as_mut_array().unwrap(),
542 )?;
543 }
544 }
545 Ok(b)
546 }
547
548 /// Writes the current journal superblock to disk.
549 ///
550 /// This updates the on-disk metadata for the journal ring buffer.
551 ///
552 /// # Arguments
553 /// - `io`: The I/O interface to the journal.
554 /// - `ffs`: Reference to the file system's metadata layout.
555 ///
556 /// # Returns
557 /// - `Ok(())`: If the write succeeds.
558 /// - `Err(KernelError)`: If the write fails.
559 pub fn writeback(&self, io: &JournalIO, ffs: &FastFileSystemInner) -> Result<(), KernelError> {
560 let lba = ffs.journal().start;
561 {
562 let inner = unsafe { core::slice::from_raw_parts(self as *const _ as *const u8, 4096) };
563 io.write_metadata_block(lba, inner.as_array().unwrap())?;
564 }
565 Ok(())
566 }
567}
568
569const_assert!(core::mem::size_of::<JournalSb>() == 4096);
570
571/// Represents a journal transaction header used to track modified blocks.
572///
573/// This structure is used during transaction commit to list all
574/// blocks that are affected by the transaction.
575pub struct JournalTxBegin {
576 /// Transaction id.
577 pub tx_id: u64,
578 /// Array of logical block addresses involved in the transaction.
579 /// - `Some(lba)`: a block to be committed.
580 /// - `None`: an unused slot.
581 pub lbas: [Option<LogicalBlockAddress>; 511],
582}
583
584impl JournalTxBegin {
585 /// Creates a new, empty journal `TxBegin` block.
586 pub fn new(tx_id: u64) -> Box<Self> {
587 Box::new(Self {
588 tx_id,
589 lbas: [None; 511],
590 })
591 }
592
593 /// Loads a journal `TxBegin` block from disk at the specified LBA.
594 ///
595 /// # Arguments
596 /// - `io`: Interface for reading blocks from disk.
597 /// - `lba`: Logical block address of the journal transaction block to load.
598 ///
599 /// # Returns
600 /// - `Ok(Box<Self>)`: The loaded journal transaction block.
601 /// - `Err(KernelError)`: If the block could not be read or parsed.
602 pub fn from_io(io: &JournalIO, lba: LogicalBlockAddress) -> Result<Box<Self>, KernelError> {
603 let mut b = Self::new(0);
604 {
605 let inner =
606 unsafe { core::slice::from_raw_parts_mut(&mut *b as *mut _ as *mut u8, 4096) };
607 io.read_journal(lba, inner.as_mut_array().unwrap())?;
608 }
609 Ok(b)
610 }
611
612 /// Converts this transaction into a 4096-byte block suitable for writing to
613 /// disk.
614 ///
615 /// This is typically used during transaction commit.
616 pub fn into_block(self: Box<Self>) -> Box<[u8; 4096]> {
617 unsafe { Box::from_raw(Box::into_raw(self) as *mut [u8; 4096]) }
618 }
619}
620
621const_assert!(core::mem::size_of::<JournalTxBegin>() == 4096);
622
623/// Represents a journal transaction is ended.
624///
625/// This structure is used during transaction commit to mark the end of
626/// transaction.
627pub struct JournalTxEnd {
628 /// Transaction id.
629 pub tx_id: u64,
630 _pad: [u8; 4088],
631}
632
633impl JournalTxEnd {
634 /// Creates a new, empty journal `TxEnd` block.
635 pub fn new(tx_id: u64) -> Box<Self> {
636 Box::new(Self {
637 tx_id,
638 _pad: [0; 4088],
639 })
640 }
641
642 /// Loads a journal `TxEnd` block from disk at the specified LBA.
643 ///
644 /// # Arguments
645 /// - `io`: Interface for reading blocks from disk.
646 /// - `lba`: Logical block address of the journal transaction block to load.
647 ///
648 /// # Returns
649 /// - `Ok(Box<Self>)`: The loaded journal transaction block.
650 /// - `Err(KernelError)`: If the block could not be read or parsed.
651 pub fn from_io(io: &JournalIO, lba: LogicalBlockAddress) -> Result<Box<Self>, KernelError> {
652 let mut b = Self::new(0);
653 {
654 let inner =
655 unsafe { core::slice::from_raw_parts_mut(&mut *b as *mut _ as *mut u8, 4096) };
656 io.read_journal(lba, inner.as_mut_array().unwrap())?;
657 }
658 Ok(b)
659 }
660
661 /// Converts this transaction into a 4096-byte block suitable for writing to
662 /// disk.
663 ///
664 /// This is typically used during transaction commit.
665 pub fn into_block(self: Box<Self>) -> Box<[u8; 4096]> {
666 unsafe { Box::from_raw(Box::into_raw(self) as *mut [u8; 4096]) }
667 }
668}
669
670const_assert!(core::mem::size_of::<JournalTxEnd>() == 4096);