// minixfs.rs // Minix 3 Filesystem Implementation // Stephen Marz // 16 March 2020 use crate::{block, fs::{Descriptor, FileSystem, FsError, Stat}, kmem::{kfree, kmalloc, talloc, tfree}}; use alloc::string::String; use core::{mem::size_of, ptr::null_mut}; pub const MAGIC: u16 = 0x4d5a; pub const BLOCK_SIZE: u32 = 1024; pub const NUM_IPTRS: u32 = BLOCK_SIZE / 4; /// The superblock describes the file system on the disk. It gives /// us all the information we need to read the file system and navigate /// the file system, including where to find the inodes and zones (blocks). #[repr(C)] pub struct SuperBlock { pub ninodes: u32, pub pad0: u16, pub imap_blocks: u16, pub zmap_blocks: u16, pub first_data_zone: u16, pub log_zone_size: u16, pub pad1: u16, pub max_size: u32, pub zones: u32, pub magic: u16, pub pad2: u16, pub block_size: u16, pub disk_version: u8, } /// An inode stores the "meta-data" to a file. The mode stores the permissions /// AND type of file. This is how we differentiate a directory from a file. A file /// size is in here too, which tells us how many blocks we need to read. Finally, the /// zones array points to where we can find the blocks, which is where the data /// is contained for the file. #[repr(C)] #[derive(Copy, Clone)] pub struct Inode { pub mode: u16, pub nlinks: u16, pub uid: u16, pub gid: u16, pub size: u32, pub atime: u32, pub mtime: u32, pub ctime: u32, pub zones: [u32; 10], } /// Notice that an inode does not contain the name of a file. This is because /// more than one file name may refer to the same inode. These are called "hard links" /// Instead, a DirEntry essentially associates a file name with an inode as shown in /// the structure below. #[repr(C)] pub struct DirEntry { pub inode: u32, pub name: [u8; 60], } // We need a BlockBuffer that can automatically be created and destroyed // in the lifetime of our read and write functions. In C, this would entail // goto statements that "unravel" all of the allocations that we made. Take // a look at the read() function to see why I thought this way would be better. pub struct BlockBuffer { buffer: *mut u8, } impl BlockBuffer { pub fn new(sz: u32) -> Self { BlockBuffer { buffer: kmalloc(sz as usize), } } pub fn get_mut(&mut self) -> *mut u8 { self.buffer } pub fn get(&self) -> *const u8 { self.buffer } } impl Default for BlockBuffer { fn default() -> Self { BlockBuffer { buffer: kmalloc(1024), } } } impl Drop for BlockBuffer { fn drop(&mut self) { if !self.buffer.is_null() { kfree(self.buffer); self.buffer = null_mut(); } } } /// The MinixFileSystem implements the FileSystem trait for the VFS. pub struct MinixFileSystem; impl MinixFileSystem { /// Inodes are the meta-data of a file, including the mode (permissions and type) and /// the file's size. They are stored above the data zones, but to figure out where we /// need to go to get the inode, we first need the superblock, which is where we can /// find all of the information about the filesystem itself. pub fn get_inode(bdev: usize, inode_num: u32) -> Option { // When we read, everything needs to be a multiple of a sector (512 bytes) // So, we need to have memory available that's at least 512 bytes, even if // we only want 10 bytes or 32 bytes (size of an Inode). let mut buffer = BlockBuffer::new(512); // Here is a little memory trick. We have a reference and it will refer to the // top portion of our buffer. Since we won't be using the super block and inode // simultaneously, we can overlap the memory regions. let super_block = unsafe { &*(buffer.get_mut() as *mut SuperBlock) }; let inode = unsafe { &*(buffer.get_mut() as *mut Inode) }; // Read from the block device. The size is 1 sector (512 bytes) and our offset is past // the boot block (first 1024 bytes). This is where the superblock sits. let result = block::read(bdev, buffer.get_mut(), 512, 1024); for _ in 0..1000000 { } if result.is_ok() && super_block.magic == MAGIC { // If we get here, we successfully read what we think is the super block. // The math here is 2 - one for the boot block, one for the super block. Then we // have to skip the bitmaps blocks. We have a certain number of inode map blocks (imap) // and zone map blocks (zmap). // The inode comes to us as a NUMBER, not an index. So, we need to subtract 1. let inode_offset = (2 + super_block.imap_blocks + super_block.zmap_blocks) as usize * BLOCK_SIZE as usize + (inode_num as usize - 1) * size_of::(); // Now, we read the inode itself. let result = block::read(bdev, buffer.get_mut(), 512, inode_offset as u64); for _ in 0..1000000 { } if result.is_ok() { println!("Inode sizex = {} {:o}", inode.size, inode.mode); return Some(inode.clone()); } } // If we get here, some result wasn't OK. Either the super block // or the inode itself. None } } impl FileSystem for MinixFileSystem { fn init(_bdev: usize) -> bool { false } fn open(_path: &String) -> Result { Err(FsError::FileNotFound) } fn read(desc: &Descriptor, buffer: *mut u8, offset: u32, size: u32) -> u32 { let mut blocks_seen = 0u32; let offset_block = offset / BLOCK_SIZE; let offset_byte = offset % BLOCK_SIZE; let mut block_buffer = BlockBuffer::new(BLOCK_SIZE); let stats = Self::stat(desc); let inode_result = Self::get_inode(desc.blockdev, desc.node); if inode_result.is_none() { // The inode couldn't be read, for some reason. return 0; } let inode = inode_result.unwrap(); // First, the _size parameter (now in bytes_left) is the size of the buffer, not // necessarily the size of the file. If our buffer is bigger than the file, we're OK. // If our buffer is smaller than the file, then we can only read up to the buffer size. let mut bytes_left = if size > stats.size { stats.size } else { size }; let mut bytes_left = 0; let mut bytes_read = 0u32; // In Rust, our for loop automatically "declares" i from 0 to < 7. The syntax // 0..7 means 0 through to 7 but not including 7. If we want to include 7, we // would use the syntax 0..=7. for i in 0..7 { // There are 7 direct zones in the Minix 3 file system. So, we can just read them // one by one. Any zone that has the value 0 is skipped and we check the next // zones. This might happen as we start writing and truncating. // We really use this to keep track of when we need to actually start reading // But an if statement probably takes more time than just incrementing it. if offset_block <= blocks_seen { // If we get here, then our offset is within our window that we want to see. // We need to go to the direct pointer's index. That'll give us a block INDEX. // That makes it easy since all we have to do is multiply the block size // by whatever we get. If it's 0, we skip it and move on. let zone_num = inode.zones[i]; if zone_num == 0 { continue; } let zone_offset = zone_num * BLOCK_SIZE; println!("Zone #{} -> #{} -> {}", i, zone_num, zone_offset); if let Ok(_) = block::read(desc.blockdev, block_buffer.get_mut(), BLOCK_SIZE, zone_offset as u64) { for _ in 0..100000 {} println!("Offset = {:x}", unsafe {block_buffer.get_mut().add(32).read()}); } } blocks_seen += 1; } bytes_read } fn write(_desc: &Descriptor, _buffer: *const u8, _offset: u32, _size: u32) -> u32 { 0 } fn close(_desc: &mut Descriptor) {} fn stat(desc: &Descriptor) -> Stat { let inode_result = Self::get_inode(desc.blockdev, desc.node); // This could be a little dangerous, but the descriptor should be checked in open(). let inode = inode_result.unwrap(); Stat { mode: inode.mode, size: inode.size, uid: inode.uid, gid: inode.gid, } } }