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}