// Copyright 2024 The ChromiumOS Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. //! Defines the inode structure. use std::mem::MaybeUninit; use std::os::unix::fs::MetadataExt; use anyhow::bail; use anyhow::Result; use enumn::N; use zerocopy::AsBytes; use zerocopy_derive::FromBytes; use zerocopy_derive::FromZeroes; use crate::arena::Arena; use crate::arena::BlockId; use crate::blockgroup::GroupMetaData; use crate::xattr::InlineXattrs; /// Types of inodes. #[derive(Debug, PartialEq, Eq, Clone, Copy, N)] pub enum InodeType { Fifo = 0x1, Char = 0x2, Directory = 0x4, Block = 0x6, Regular = 0x8, Symlink = 0xa, Socket = 0xc, } impl InodeType { /// Converts to a file type for directory entry. /// The value is defined in "Table 4.2. Defined Inode File Type Values" in the spec. pub fn into_dir_entry_file_type(self) -> u8 { match self { InodeType::Regular => 1, InodeType::Directory => 2, InodeType::Char => 3, InodeType::Block => 4, InodeType::Fifo => 5, InodeType::Socket => 6, InodeType::Symlink => 7, } } } // Represents an inode number. // This is 1-indexed. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub(crate) struct InodeNum(pub u32); impl InodeNum { pub fn new(inode: u32) -> Result { if inode == 0 { bail!("inode number is 1-indexed"); } Ok(Self(inode)) } // Returns index in the inode table. pub fn to_table_index(self) -> usize { // (num - 1) because inode is 1-indexed. self.0 as usize - 1 } } impl From for u32 { fn from(inode: InodeNum) -> Self { inode.0 } } impl From for usize { fn from(inode: InodeNum) -> Self { inode.0 as usize } } /// Size of the `block` field in Inode. const INODE_BLOCK_LEN: usize = 60; /// Represents 60-byte region for block in Inode. /// This region is used for various ways depending on the file type. /// For regular files and directories, it's used for storing 32-bit indices of blocks. /// /// This is a wrapper of `[u8; 60]` to implement `Default` manually. #[repr(C)] #[derive(Debug, Copy, Clone, FromZeroes, FromBytes, AsBytes)] pub(crate) struct InodeBlock(pub [u8; INODE_BLOCK_LEN]); impl Default for InodeBlock { fn default() -> Self { Self([0; INODE_BLOCK_LEN]) } } impl InodeBlock { // Each inode contains 12 direct pointers (0-11), one singly indirect pointer (12), one // doubly indirect block pointer (13), and one triply indirect pointer (14). pub const NUM_DIRECT_BLOCKS: usize = 12; const INDIRECT_BLOCK_TABLE_ID: usize = Self::NUM_DIRECT_BLOCKS; const DOUBLE_INDIRECT_BLOCK_TABLE_ID: usize = 13; /// Set a block id at the given index. pub fn set_block_id(&mut self, index: usize, block_id: &BlockId) -> Result<()> { let offset = index * std::mem::size_of::(); let bytes = block_id.as_bytes(); if self.0.len() < offset + bytes.len() { bail!("index out of bounds when setting block_id to InodeBlock: index={index}, block_id: {:?}", block_id); } self.0[offset..offset + bytes.len()].copy_from_slice(bytes); Ok(()) } /// Set an array of direct block IDs. pub fn set_direct_blocks_from( &mut self, start_idx: usize, block_ids: &[BlockId], ) -> Result<()> { let bytes = block_ids.as_bytes(); if bytes.len() + start_idx * 4 > self.0.len() { bail!( "length of direct blocks is {} bytes, but it must not exceed {}", bytes.len(), self.0.len() ); } self.0[start_idx * 4..(start_idx * 4 + bytes.len())].copy_from_slice(bytes); Ok(()) } /// Set an array of direct block IDs. pub fn set_direct_blocks(&mut self, block_ids: &[BlockId]) -> Result<()> { self.set_direct_blocks_from(0, block_ids) } /// Set a block id to be used as the indirect block table. pub fn set_indirect_block_table(&mut self, block_id: &BlockId) -> Result<()> { self.set_block_id(Self::INDIRECT_BLOCK_TABLE_ID, block_id) } /// Set a block id to be used as the double indirect block table. pub fn set_double_indirect_block_table(&mut self, block_id: &BlockId) -> Result<()> { self.set_block_id(Self::DOUBLE_INDIRECT_BLOCK_TABLE_ID, block_id) } /// Returns the max length of symbolic links that can be stored in the inode data. /// This length contains the trailing `\0`. pub const fn max_inline_symlink_len() -> usize { INODE_BLOCK_LEN } /// Stores a given string as an inlined symbolic link data. pub fn set_inline_symlink(&mut self, symlink: &str) -> Result<()> { let bytes = symlink.as_bytes(); if bytes.len() >= Self::max_inline_symlink_len() { bail!( "symlink '{symlink}' exceeds or equals tomax length: {} >= {}", bytes.len(), Self::max_inline_symlink_len() ); } self.0[..bytes.len()].copy_from_slice(bytes); Ok(()) } } /// The ext2 inode. /// /// The field names are based on [the specification](https://www.nongnu.org/ext2-doc/ext2.html#inode-table). #[repr(C)] #[derive(Debug, Copy, Clone, FromZeroes, FromBytes, AsBytes)] pub(crate) struct Inode { mode: u16, uid: u16, pub size: u32, atime: u32, ctime: u32, mtime: u32, _dtime: u32, gid: u16, pub links_count: u16, pub blocks: InodeBlocksCount, _flags: u32, _osd1: u32, pub block: InodeBlock, _generation: u32, _file_acl: u32, _dir_acl: u32, _faddr: u32, _fragment_num: u8, _fragment_size: u8, _reserved1: u16, uid_high: u16, gid_high: u16, _reserved2: u32, // 128-th byte // We don't use any inode metadata region beyond the basic 128 bytes. // However set `extra_size` to the minimum value to let Linux kernel know that there are // inline extended attribute data. The minimum possible is 4 bytes, so define extra_size // and add the next padding. pub extra_size: u16, _paddings: u16, // padding for 32-bit alignment } impl Default for Inode { fn default() -> Self { // SAFETY: zero-filled value is a valid value. let mut r: Self = unsafe { MaybeUninit::zeroed().assume_init() }; // Set extra size to 4 for `extra_size` and `paddings` fields. r.extra_size = 4; r } } /// Used in `Inode` to represent how many 512-byte blocks are used by a file. /// /// The block size '512' byte is fixed and not related to the actual block size of the file system. /// For more details, see notes for `i_blocks_lo` in the specification. #[repr(C)] #[derive(Default, Debug, Copy, Clone, FromZeroes, FromBytes, AsBytes)] pub struct InodeBlocksCount(u32); impl InodeBlocksCount { const INODE_BLOCKS_SIZE: u32 = 512; pub fn from_bytes_len(len: u32) -> Self { Self(len / Self::INODE_BLOCKS_SIZE) } pub fn add(&mut self, v: u32) { self.0 += v / Self::INODE_BLOCKS_SIZE; } } impl Inode { /// Size of the inode record in bytes. /// /// From ext2 revision 1, inode size larger than 128 bytes is supported. /// We use 256 byte here, which is the default value for ext4. /// /// Note that inode "record" size can be larger that inode "structure" size. /// The gap between the end of the inode structure and the end of the inode record can be used /// to store extended attributes. pub const INODE_RECORD_SIZE: usize = 256; /// Size of the region that inline extended attributes can be written. pub const XATTR_AREA_SIZE: usize = Inode::INODE_RECORD_SIZE - std::mem::size_of::(); pub fn new<'a>( arena: &'a Arena<'a>, group: &mut GroupMetaData, inode_num: InodeNum, typ: InodeType, size: u32, xattr: Option, ) -> Result<&'a mut Self> { const EXT2_S_IRUSR: u16 = 0x0100; // user read const EXT2_S_IXUSR: u16 = 0x0040; // user execute const EXT2_S_IRGRP: u16 = 0x0020; // group read const EXT2_S_IXGRP: u16 = 0x0008; // group execute const EXT2_S_IROTH: u16 = 0x0004; // others read const EXT2_S_IXOTH: u16 = 0x0001; // others execute let inode_offset = inode_num.to_table_index() * Inode::INODE_RECORD_SIZE; let inode = arena.allocate::(BlockId::from(group.group_desc.inode_table), inode_offset)?; // Give read and execute permissions let mode = ((typ as u16) << 12) | EXT2_S_IRUSR | EXT2_S_IXUSR | EXT2_S_IRGRP | EXT2_S_IXGRP | EXT2_S_IROTH | EXT2_S_IXOTH; let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH)? .as_secs() as u32; // SAFETY: geteuid never fail. let uid = unsafe { libc::geteuid() }; let uid_high = (uid >> 16) as u16; let uid_low = uid as u16; // SAFETY: getegid never fail. let gid = unsafe { libc::getegid() }; let gid_high = (gid >> 16) as u16; let gid_low = gid as u16; *inode = Self { mode, size, atime: now, ctime: now, mtime: now, uid: uid_low, gid: gid_low, uid_high, gid_high, ..Default::default() }; if let Some(xattr) = xattr { Self::add_xattr(arena, group, inode, inode_offset, xattr)?; } Ok(inode) } pub fn from_metadata<'a>( arena: &'a Arena<'a>, group: &mut GroupMetaData, inode_num: InodeNum, m: &std::fs::Metadata, size: u32, links_count: u16, blocks: InodeBlocksCount, block: InodeBlock, xattr: Option, ) -> Result<&'a mut Self> { let inodes_per_group = group.inode_bitmap.len(); // (inode_num - 1) because inode is 1-indexed. let inode_offset = ((usize::from(inode_num) - 1) % inodes_per_group) * Inode::INODE_RECORD_SIZE; let inode = arena.allocate::(BlockId::from(group.group_desc.inode_table), inode_offset)?; let mode = m.mode() as u16; let uid = m.uid(); let uid_high = (uid >> 16) as u16; let uid_low: u16 = uid as u16; let gid = m.gid(); let gid_high = (gid >> 16) as u16; let gid_low: u16 = gid as u16; let atime = m.atime() as u32; let ctime = m.ctime() as u32; let mtime = m.mtime() as u32; *inode = Inode { mode, uid: uid_low, gid: gid_low, size, atime, ctime, mtime, links_count, blocks, block, uid_high, gid_high, ..Default::default() }; if let Some(xattr) = xattr { Self::add_xattr(arena, group, inode, inode_offset, xattr)?; } Ok(inode) } fn add_xattr<'a>( arena: &'a Arena<'a>, group: &mut GroupMetaData, inode: &mut Inode, inode_offset: usize, xattr: InlineXattrs, ) -> Result<()> { let xattr_region = arena.allocate::<[u8; Inode::XATTR_AREA_SIZE]>( BlockId::from(group.group_desc.inode_table), inode_offset + std::mem::size_of::(), )?; if !xattr.entry_table.is_empty() { // Linux and debugfs uses extra_size to check if inline xattr is stored so we need to // set a positive value here. 4 (= sizeof(extra_size) + sizeof(_paddings)) // is the smallest value. inode.extra_size = 4; let InlineXattrs { entry_table, values, } = xattr; if entry_table.len() + values.len() > Inode::XATTR_AREA_SIZE { bail!("xattr size is too large for inline store: entry_table.len={}, values.len={}, inline region size={}", entry_table.len(), values.len(), Inode::XATTR_AREA_SIZE); } // `entry_table` should be aligned to the beginning of the region. xattr_region[..entry_table.len()].copy_from_slice(&entry_table); xattr_region[Inode::XATTR_AREA_SIZE - values.len()..].copy_from_slice(&values); } Ok(()) } pub fn update_metadata(&mut self, m: &std::fs::Metadata) { self.mode = m.mode() as u16; let uid: u32 = m.uid(); self.uid_high = (uid >> 16) as u16; self.uid = uid as u16; let gid = m.gid(); self.gid_high = (gid >> 16) as u16; self.gid = gid as u16; self.atime = m.atime() as u32; self.ctime = m.ctime() as u32; self.mtime = m.mtime() as u32; } pub fn typ(&self) -> Option { InodeType::n((self.mode >> 12) as u8) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_inode_size() { assert_eq!(std::mem::offset_of!(Inode, extra_size), 128); // Check that no implicit paddings is inserted after the padding field. assert_eq!( std::mem::offset_of!(Inode, _paddings) + std::mem::size_of::(), std::mem::size_of::() ); assert!(128 < std::mem::size_of::()); assert!(std::mem::size_of::() <= Inode::INODE_RECORD_SIZE); } }