keos_project5/ffs/
inode.rs

1//! ## Inode abstraction.
2//!
3//! In a Unix-like file system, every object in a file system is represented by
4//! a data structure known as an **inode**. An **inode** (index node) stores
5//! metadata about a file system object, including its size, permissions,
6//! timestamps, and pointers to the underlying data blocks.
7//!
8//! ### Inode Internals
9//! An `Inode` is the authoritative metadata structure for a file or
10//! directory. It stores essential information such as the object’s type, size,
11//! and pointers to data blocks, serving as the central handle for locating and
12//! managing file data.
13//!
14//! At its core, an inode functions as an **indexing structure**: it maps a
15//! [`FileBlockNumber`] (a block’s position relative to the file) to a
16//! [`LogicalBlockAddress`] (the actual block location on disk). This mapping
17//! enables the file system to translate file-relative accesses into physical
18//! disk operations, bridging the logical view of a file with the underlying
19//! storage layout.
20//!
21//! While the on-disk inode structure provides persistence,
22//! the kernel maintains in-memory inodes to speed up access and coordinate
23//! concurrent operations. To ensure this, KeOS provides the
24//! [`FastFileSystemInner::get_inode`] API to maintains **single, global view**
25//! of each inode inside the kernel.
26//!
27//! The function looks up an inode number ([`InodeNumber`]) and returns a
28//! [`TrackedInode`], which is a consistent view for a reference-counted,
29//! thread-safe wrapper around the in-memory inode. All kernel subsystems
30//! interact with inodes through this wrapper, ensuring proper synchronization.
31//!
32//! A [`TrackedInode`] provides two key capabilities:
33//!
34//! - **Read access** via [`TrackedInode::read`], which acquires a shared guard
35//!   for inspecting inode state (e.g., file size, permissions) without
36//!   modification.
37//! - **Write access** via [`TrackedInode::write_with`], which locks both the
38//!   in-memory and on-disk inode. Writes are performed inside a closure that
39//!   receives a [`TrackedInodeWriteGuard`]. Changes must be explicitly
40//!   finalized with [`TrackedInodeWriteGuard::submit`], ensuring that memory
41//!   and disk stay consistent.
42//!
43//! Together, `get_inode` and `TrackedInode` enforce a disciplined access model:
44//! there is exactly one in-memory representation of each inode, guarded by
45//! lock. This design keeps inode state consistent across memory and disk, even
46//! under concurrent file system activity.
47//!
48//! ### Inode Indexing in FFS
49//! An inode does not store file data directly. Instead, it contains pointers
50//! to **data blocks** that hold the file’s contents. To balance efficiency
51//! for small files with scalability for large files, FFS adopts a tiered
52//! indexing scheme as follow:
53//! ```text
54//!              ┌───────────────────────────┐
55//!              │         Inode             │
56//!              ├───────────────────────────┤
57//!              │ dblocks[0] → Data blk 0   │
58//!              │ dblocks[1] → Data blk 1   │
59//!              │ ...                       │
60//!              │ dblocks[11] → Data blk11  │
61//!              │                           │
62//!              │ iblock 0 ───────────────┐ │
63//!              │                         │ │
64//!              │ diblock ─────────────┐  │ │
65//!              └──────────────────────┬──┬─┘
66//!                                     │  │
67//!          ┌──────────────────────────┘  │
68//!   ┌──────▼───────┐                 ┌───▼──────────┐
69//!   │ Double ind.  │                 │ Indirect     │
70//!   ├──────────────┤                 ├──────────────┤
71//!   │ → iblock 1   │                 │ → Data blk12 │
72//!   │ → iblock 2   │─┐               │ → Data blk13 │
73//!   │ ..           │ │               │ ...          │
74//!   │ → iblock 512 │ │               │ → Data blk523│
75//!   └──────────────┘ │               └──────────────┘
76//!                    │
77//!     ┌──────────────┘
78//! ┌───▼──────────┐
79//! │ Indirect blk │
80//! ├──────────────┤
81//! │ → Data blk X │
82//! │ → Data blk Y │
83//! │ ...          │
84//! └──────────────┘
85//! ```
86//!
87//! - **Direct blocks (`dblocks`)** The first 12 pointers directly reference
88//!   data blocks. This makes access to small files very fast, as no additional
89//!   lookup is required. Small files can therefore be served entirely from
90//!   these direct entries.
91//
92//! - **Indirect block (`iblock`)** When the file grows beyond the direct
93//!   blocks, the inode refers to a single **indirect block**. This block is
94//!   itself an array of data block pointers, extending the maximum file size
95//!   significantly.
96//
97//! - **Double indirect block (`diblock`)** For even larger files, the inode
98//!   uses a **double indirection**. The inode points to a block that contains
99//!   pointers to *indirect blocks*, each of which then contains pointers to
100//!   data blocks. This extra level of indirection allows extremely large files
101//!   to be addressed.
102//!
103//! Together, these three levels form a hierarchical mapping from a
104//! [`FileBlockNumber`] (position within a file) to a
105//! [`LogicalBlockAddress`] (actual block on disk).
106//!
107//! Your task is to implement the two core file access functions based on the
108//! indexing structure: [`Inode::get`] and [`Inode::grow`].
109//!
110//! - [`Inode::get`] retrieves the disk location of a specific file block. It
111//!   traverses the inode’s indexing structure and returns the corresponding
112//!   disk block address, or `None` if the block has not been allocated.
113//!
114//! - [`Inode::grow`] ensures that the inode can cover a target file block
115//!   number. If needed, it allocates new blocks and updates the inode’s
116//!   indexing structure. All modifications are performed transactionally to
117//!   guarantee consistency.
118//!
119//! These functions use [`MetaData::load`] to access or create
120//! [`IndirectBlock`]s, and they update these blocks via the transaction API
121//! (using [`BlockPointsTo::write`] and
122//! [`BlockPointsToWriteGuard::submit`]). For reference on using these APIs, you
123//! may consult the documentation or implementation of the followings:
124//! - [`access_control`]
125//! - [`Directory::add_entry`]
126//!
127//! While implementing the requirements, you may encounter
128//! [`RunningTransaction`] struct. You do not need to understand it until the
129//! implementation of the journaling; for now, simply pass a reference to the
130//! required methods.
131//!
132//! ## Implementation Requirements
133//! You need to implement the followings:
134//! - [`Inode::get`]
135//! - [`Inode::grow`]
136//!
137//! After implement the functionalities, move on to the next [`section`].
138//!
139//! [`section`]: mod@crate::ffs::fs_objects
140//! [`IndirectBlock`]: crate::ffs::disk_layout::IndirectBlock
141use super::access_control::TrackedInodeWriteGuard;
142use crate::ffs::{
143    FastFileSystemInner, FileBlockNumber, InodeNumber, LogicalBlockAddress,
144    access_control::MetaData, disk_layout, journal::RunningTransaction, types::FileType,
145};
146#[cfg(doc)]
147use crate::ffs::{
148    access_control::{self, BlockPointsTo, BlockPointsToWriteGuard, TrackedInode},
149    fs_objects::Directory,
150};
151use keos::KernelError;
152#[cfg(doc)]
153use keos::fs::traits::Directory as _Directory;
154
155/// Represents an inode in memory, the metadata structure for a file or
156/// directory.
157///
158/// An inode stores essential information about a file, including its size,
159/// type, and the locations of its data blocks.
160#[derive(Debug)]
161pub struct Inode {
162    /// The unique inode number assigned to this file or directory.
163    pub ino: InodeNumber,
164    /// The type of the file (e.g., regular file, directory, symbolic link).
165    ///
166    /// Uses a `u32` to store values corresponding to [`FileType`].
167    pub ftype: FileType,
168    /// The total size of the file in bytes.
169    pub size: usize,
170    /// Number of links alive in the filesystem.
171    pub link_count: usize,
172    /// Directly mapped data blocks.
173    ///
174    /// These 12 blocks store the first portions of a file's data, allowing
175    /// for efficient access to small files without requiring indirect blocks.
176    pub dblocks: [Option<LogicalBlockAddress>; 12],
177    /// An indirect block, which contains pointers to additional data
178    /// blocks.
179    ///
180    /// This extends the file size capability beyond direct blocks by storing
181    /// an array of logical block addresses in a separate block.
182    pub iblock: Option<LogicalBlockAddress>,
183    /// A doubly indirect block, which contains pointers to indirect
184    /// blocks.
185    ///
186    /// This allows for even larger file sizes by introducing an extra level
187    /// of indirection.
188    pub diblock: Option<LogicalBlockAddress>,
189}
190
191impl Inode {
192    /// Constructs an in-memory [`Inode`] from its on-disk representation.
193    ///
194    /// # Arguments
195    /// - `inode`: A reference to the [`disk_layout::Inode`] structure loaded
196    ///   from disk.
197    ///
198    /// # Returns
199    /// - `Ok(Self)`: If the conversion succeeds.
200    /// - `Err(KernelError)`: If the inode contains invalid or inconsistent
201    ///   data.
202    ///
203    /// This function performs necessary validation and translation between the
204    /// raw on-disk layout and the structured, in-memory format used by the
205    /// filesystem.
206    pub(crate) fn from_disk_layout(inode: &disk_layout::Inode) -> Result<Self, KernelError> {
207        if &inode.magic != b"KeOSFFSI" {
208            return Err(KernelError::FilesystemCorrupted("Inode Magic Mismatch"));
209        }
210        Ok(Self {
211            ino: inode
212                .ino
213                .ok_or(KernelError::FilesystemCorrupted("Inode number is zero"))?,
214            ftype: FileType::try_from(inode.ftype)?,
215            size: inode.size as usize,
216            link_count: inode.link_count as usize,
217            dblocks: inode.dblocks,
218            iblock: inode.iblock,
219            diblock: inode.diblock,
220        })
221    }
222
223    /// Converts the in-memory [`Inode`] structure into its on-disk
224    /// representation.
225    ///
226    /// This is used when persisting an inode to disk, typically during
227    /// checkpointing or journal submission. The returned [`disk_layout::Inode`]
228    /// struct matches the format expected by the on-disk inode array.
229    pub fn into_disk_format(&self) -> disk_layout::Inode {
230        disk_layout::Inode {
231            magic: *b"KeOSFFSI",
232            ino: Some(self.ino),
233            ftype: match self.ftype {
234                FileType::RegularFile => 0,
235                FileType::Directory => 1,
236            },
237            size: self.size as u64,
238            link_count: self.link_count as u64,
239            dblocks: self.dblocks,
240            iblock: self.iblock,
241            diblock: self.diblock,
242            _pad: [0; 112],
243        }
244    }
245
246    /// Creates a new in-memory [`Inode`] instance.
247    ///
248    /// This function is used to initialize a fresh inode in memory before it is
249    /// ever written to disk. It sets the inode number and whether the inode
250    /// represents a directory.
251    ///
252    /// # Parameters
253    /// - `ino`: The inode number.
254    /// - `is_dir`: Whether this inode represents a directory (`true`) or a file
255    ///   (`false`).
256    ///
257    /// # Returns
258    /// A new [`Inode`] instance ready to be inserted into the inode table.
259    pub(crate) fn new(ino: InodeNumber, is_dir: bool) -> Self {
260        Self {
261            ino,
262            ftype: if is_dir {
263                FileType::Directory
264            } else {
265                FileType::RegularFile
266            },
267            size: 0,
268            link_count: 0,
269            dblocks: [None; 12],
270            iblock: None,
271            diblock: None,
272        }
273    }
274
275    /// Retrieves the logical block address (LBA) corresponding to a file block.
276    ///
277    /// # Arguments
278    /// - `ffs`: Reference to the file system.
279    /// - `fba`: [`FileBlockNumber`], relative to the beginning of the file.
280    ///
281    /// # Returns
282    /// - `Ok(lba)`: The logical block address where the specified file block is
283    ///   stored.
284    /// - `Err(KernelError)`: If the block is not allocated or the block number
285    ///   is out of bounds.
286    pub fn get(
287        &self,
288        ffs: &FastFileSystemInner,
289        fba: FileBlockNumber,
290    ) -> Result<Option<LogicalBlockAddress>, KernelError> {
291        if 0x1000 * fba.0 >= self.size {
292            return Ok(None);
293        }
294        todo!()
295    }
296
297    /// Grows the inode to include at least the given number of file blocks.
298    ///
299    /// # Arguments
300    /// - `ffs`: Reference to the file system.
301    /// - `until`: The target [`FileBlockNumber`] (inclusive) that the inode
302    ///   should grow to cover.
303    /// - `tx`: The running transaction used to log allocation changes.
304    ///
305    /// # Returns
306    /// - `Ok(())`: If the inode was successfully extended.
307    /// - `Err(KernelError)`: If allocation fails or the inode cannot be grown.
308    ///
309    /// This function ensures that all blocks up to `until` are allocated,
310    /// performing allocation of direct and indirect blocks as needed. The
311    /// transaction log is updated to support crash consistency.
312    pub fn grow(
313        &mut self,
314        ffs: &FastFileSystemInner,
315        until: FileBlockNumber,
316        tx: &RunningTransaction,
317    ) -> Result<(), KernelError> {
318        // Hint: use [`FastFileSystemInner::allocate_block`] to allocate an free block.
319        todo!()
320    }
321
322    /// Deallocate inner blocks and set the inode's size to zero.
323    ///
324    /// Note that submitting the InodeWriteGuard is the caller's responsibility.
325    pub fn zeroify(
326        ino: &mut TrackedInodeWriteGuard,
327        tx: &RunningTransaction,
328        ffs: &FastFileSystemInner,
329    ) {
330        let mut sb = ffs.sb.write(tx);
331        for fba in 0..(ino.size.div_ceil(0x1000)) {
332            let lba = ino.get(ffs, FileBlockNumber(fba)).unwrap().unwrap();
333
334            let (b_lba, offset) = lba.into_bitmap_lba_offset(ffs).unwrap();
335            let bitmap = disk_layout::BlockBitmap::load(ffs, b_lba).unwrap();
336
337            let mut guard = bitmap.write(tx);
338            assert!(guard.deallocate(offset));
339            guard.submit();
340
341            sb.block_count_inused -= 1;
342        }
343
344        sb.submit();
345        ino.size = 0;
346    }
347}