composefs/erofs/
format.rs

1//! EROFS on-disk format definitions and data structures.
2//!
3//! This module defines the binary layout of EROFS filesystem structures
4//! including superblocks, inodes, directory entries, and other metadata
5//! using safe zerocopy-based parsing.
6
7// This is currently implemented using zerocopy but the eventual plan is to do this with safe
8// transmutation.  As such: all of the structures are defined in terms of pure LE integer sizes, we
9// handle the conversion to enum values separately, and we avoid the TryFromBytes trait.
10
11use std::fmt;
12
13use zerocopy::{
14    little_endian::{U16, U32, U64},
15    FromBytes, Immutable, IntoBytes, KnownLayout,
16};
17
18/// Number of bits used for block size (12 = 4096 bytes)
19pub const BLOCK_BITS: u8 = 12;
20/// Size of a block in bytes (4096)
21pub const BLOCK_SIZE: u16 = 1 << BLOCK_BITS;
22
23/// Errors that can occur when parsing EROFS format structures
24#[derive(Debug)]
25pub enum FormatError {
26    /// The data layout field contains an invalid value
27    InvalidDataLayout,
28}
29
30/* Special handling for enums: FormatField and FileTypeField */
31// FormatField == InodeLayout | DataLayout
32/// Combined field encoding both inode layout and data layout in a single u16 value
33#[derive(Clone, Copy, FromBytes, Immutable, IntoBytes, KnownLayout, PartialEq)]
34pub struct FormatField(U16);
35
36impl Default for FormatField {
37    fn default() -> Self {
38        FormatField(0xffff.into())
39    }
40}
41
42impl fmt::Debug for FormatField {
43    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
44        write!(
45            f,
46            "{} = {:?} | {:?}",
47            self.0.get(),
48            InodeLayout::from(*self),
49            DataLayout::try_from(*self)
50        )
51    }
52}
53
54const INODE_LAYOUT_MASK: u16 = 0b00000001;
55const INODE_LAYOUT_COMPACT: u16 = 0;
56const INODE_LAYOUT_EXTENDED: u16 = 1;
57
58/// Inode layout format, determining the inode header size
59#[derive(Debug)]
60#[repr(u16)]
61pub enum InodeLayout {
62    /// Compact 32-byte inode header
63    Compact = INODE_LAYOUT_COMPACT,
64    /// Extended 64-byte inode header with additional fields
65    Extended = INODE_LAYOUT_EXTENDED,
66}
67
68impl From<FormatField> for InodeLayout {
69    fn from(value: FormatField) -> Self {
70        match value.0.get() & INODE_LAYOUT_MASK {
71            INODE_LAYOUT_COMPACT => InodeLayout::Compact,
72            INODE_LAYOUT_EXTENDED => InodeLayout::Extended,
73            _ => unreachable!(),
74        }
75    }
76}
77
78const INODE_DATALAYOUT_MASK: u16 = 0b00001110;
79const INODE_DATALAYOUT_FLAT_PLAIN: u16 = 0;
80const INODE_DATALAYOUT_FLAT_INLINE: u16 = 4;
81const INODE_DATALAYOUT_CHUNK_BASED: u16 = 8;
82
83/// Data layout method for file content storage
84#[derive(Debug)]
85#[repr(u16)]
86pub enum DataLayout {
87    /// File data stored in separate blocks
88    FlatPlain = 0,
89    /// File data stored inline within the inode
90    FlatInline = 4,
91    /// File data stored using chunk-based addressing
92    ChunkBased = 8,
93}
94
95impl TryFrom<FormatField> for DataLayout {
96    type Error = FormatError;
97
98    fn try_from(value: FormatField) -> Result<Self, FormatError> {
99        match value.0.get() & INODE_DATALAYOUT_MASK {
100            INODE_DATALAYOUT_FLAT_PLAIN => Ok(DataLayout::FlatPlain),
101            INODE_DATALAYOUT_FLAT_INLINE => Ok(DataLayout::FlatInline),
102            INODE_DATALAYOUT_CHUNK_BASED => Ok(DataLayout::ChunkBased),
103            // This is non-injective, but only occurs in error cases.
104            _ => Err(FormatError::InvalidDataLayout),
105        }
106    }
107}
108
109impl std::ops::BitOr<DataLayout> for InodeLayout {
110    type Output = FormatField;
111
112    // Convert InodeLayout | DataLayout into a format field
113    fn bitor(self, datalayout: DataLayout) -> FormatField {
114        FormatField((self as u16 | datalayout as u16).into())
115    }
116}
117
118/// File type mask for st_mode
119pub const S_IFMT: u16 = 0o170000;
120/// Regular file mode bit
121pub const S_IFREG: u16 = 0o100000;
122/// Character device mode bit
123pub const S_IFCHR: u16 = 0o020000;
124/// Directory mode bit
125pub const S_IFDIR: u16 = 0o040000;
126/// Block device mode bit
127pub const S_IFBLK: u16 = 0o060000;
128/// FIFO mode bit
129pub const S_IFIFO: u16 = 0o010000;
130/// Symbolic link mode bit
131pub const S_IFLNK: u16 = 0o120000;
132/// Socket mode bit
133pub const S_IFSOCK: u16 = 0o140000;
134
135// FileTypeField == FileType
136/// Unknown file type value
137pub const FILE_TYPE_UNKNOWN: u8 = 0;
138/// Regular file type value
139pub const FILE_TYPE_REGULAR_FILE: u8 = 1;
140/// Directory file type value
141pub const FILE_TYPE_DIRECTORY: u8 = 2;
142/// Character device file type value
143pub const FILE_TYPE_CHARACTER_DEVICE: u8 = 3;
144/// Block device file type value
145pub const FILE_TYPE_BLOCK_DEVICE: u8 = 4;
146/// FIFO file type value
147pub const FILE_TYPE_FIFO: u8 = 5;
148/// Socket file type value
149pub const FILE_TYPE_SOCKET: u8 = 6;
150/// Symbolic link file type value
151pub const FILE_TYPE_SYMLINK: u8 = 7;
152
153/// File type enumeration for directory entries
154#[derive(Clone, Copy, Debug)]
155#[repr(u8)]
156pub enum FileType {
157    /// Unknown or invalid file type
158    Unknown = FILE_TYPE_UNKNOWN,
159    /// Regular file
160    RegularFile = FILE_TYPE_REGULAR_FILE,
161    /// Directory
162    Directory = FILE_TYPE_DIRECTORY,
163    /// Character device
164    CharacterDevice = FILE_TYPE_CHARACTER_DEVICE,
165    /// Block device
166    BlockDevice = FILE_TYPE_BLOCK_DEVICE,
167    /// FIFO (named pipe)
168    Fifo = FILE_TYPE_FIFO,
169    /// Socket
170    Socket = FILE_TYPE_SOCKET,
171    /// Symbolic link
172    Symlink = FILE_TYPE_SYMLINK,
173}
174
175impl From<FileTypeField> for FileType {
176    fn from(value: FileTypeField) -> Self {
177        match value.0 {
178            FILE_TYPE_REGULAR_FILE => Self::RegularFile,
179            FILE_TYPE_DIRECTORY => Self::Directory,
180            FILE_TYPE_CHARACTER_DEVICE => Self::CharacterDevice,
181            FILE_TYPE_BLOCK_DEVICE => Self::BlockDevice,
182            FILE_TYPE_FIFO => Self::Fifo,
183            FILE_TYPE_SOCKET => Self::Socket,
184            FILE_TYPE_SYMLINK => Self::Symlink,
185            // This is non-injective, but only occurs in error cases.
186            _ => Self::Unknown,
187        }
188    }
189}
190
191impl From<FileType> for FileTypeField {
192    fn from(value: FileType) -> Self {
193        FileTypeField(value as u8)
194    }
195}
196
197impl std::ops::BitOr<u16> for FileType {
198    type Output = U16;
199
200    // Convert ifmt | permissions into a st_mode field
201    fn bitor(self, permissions: u16) -> U16 {
202        (match self {
203            Self::RegularFile => S_IFREG,
204            Self::CharacterDevice => S_IFCHR,
205            Self::Directory => S_IFDIR,
206            Self::BlockDevice => S_IFBLK,
207            Self::Fifo => S_IFIFO,
208            Self::Symlink => S_IFLNK,
209            Self::Socket => S_IFSOCK,
210            Self::Unknown => unreachable!(),
211        } | permissions)
212            .into()
213    }
214}
215
216/// Raw file type field as stored in directory entries
217#[derive(Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout, PartialEq)]
218pub struct FileTypeField(u8);
219
220impl fmt::Debug for FileTypeField {
221    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
222        fmt::Debug::fmt(&FileType::from(*self), f)
223    }
224}
225
226impl Default for FileTypeField {
227    fn default() -> Self {
228        FileTypeField(0xff)
229    }
230}
231
232/* ModeField */
233/// File mode field combining file type and permissions
234#[derive(Clone, Copy, Default, FromBytes, Immutable, IntoBytes, KnownLayout, PartialEq)]
235pub struct ModeField(pub U16);
236
237impl ModeField {
238    /// Checks if this mode field represents a directory
239    pub fn is_dir(self) -> bool {
240        self.0.get() & S_IFMT == S_IFDIR
241    }
242}
243
244impl fmt::Debug for ModeField {
245    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
246        let mode = self.0.get();
247        let fmt = match mode & S_IFMT {
248            S_IFREG => "regular file",
249            S_IFCHR => "chardev",
250            S_IFDIR => "directory",
251            S_IFBLK => "blockdev",
252            S_IFIFO => "fifo",
253            S_IFLNK => "symlink",
254            S_IFSOCK => "socket",
255            _ => "INVALID",
256        };
257
258        write!(f, "0{mode:06o} ({fmt})")
259    }
260}
261
262impl std::ops::BitOr<u32> for FileType {
263    type Output = ModeField;
264
265    fn bitor(self, permissions: u32) -> ModeField {
266        ModeField(self | (permissions as u16))
267    }
268}
269
270/* composefs Header */
271
272/// EROFS format version number
273pub const VERSION: U32 = U32::new(1);
274/// Composefs-specific version number
275pub const COMPOSEFS_VERSION: U32 = U32::new(2);
276/// Magic number identifying composefs images
277pub const COMPOSEFS_MAGIC: U32 = U32::new(0xd078629a);
278
279/// Flag indicating the presence of ACL data
280pub const COMPOSEFS_FLAGS_HAS_ACL: U32 = U32::new(1 << 0);
281
282/// Composefs-specific header preceding the standard EROFS superblock
283#[derive(Default, FromBytes, Immutable, IntoBytes, KnownLayout)]
284#[repr(C)]
285pub struct ComposefsHeader {
286    /// Magic number for identification
287    pub magic: U32,
288    /// EROFS format version
289    pub version: U32,
290    /// Composefs feature flags
291    pub flags: U32,
292    /// Composefs format version
293    pub composefs_version: U32,
294    /// Reserved for future use
295    pub unused: [U32; 4],
296}
297
298/* Superblock */
299
300/// EROFS version 1 magic number
301pub const MAGIC_V1: U32 = U32::new(0xE0F5E1E2);
302/// Feature flag for mtime support
303pub const FEATURE_COMPAT_MTIME: U32 = U32::new(2);
304/// Feature flag for xattr filtering support
305pub const FEATURE_COMPAT_XATTR_FILTER: U32 = U32::new(4);
306
307/// EROFS filesystem superblock structure
308#[derive(Default, FromBytes, Immutable, IntoBytes, KnownLayout)]
309#[repr(C)]
310pub struct Superblock {
311    // vertical whitespace every 16 bytes (hexdump-friendly)
312    /// EROFS magic number
313    pub magic: U32,
314    /// Filesystem checksum
315    pub checksum: U32,
316    /// Compatible feature flags
317    pub feature_compat: U32,
318    /// Block size in bits (log2 of block size)
319    pub blkszbits: u8,
320    /// Number of extended attribute slots
321    pub extslots: u8,
322    /// Root inode number
323    pub root_nid: U16,
324
325    /// Total number of inodes
326    pub inos: U64,
327    /// Build time in seconds since epoch
328    pub build_time: U64,
329
330    /// Build time nanoseconds component
331    pub build_time_nsec: U32,
332    /// Total number of blocks
333    pub blocks: U32,
334    /// Starting block address of metadata
335    pub meta_blkaddr: U32,
336    /// Starting block address of extended attributes
337    pub xattr_blkaddr: U32,
338
339    /// Filesystem UUID
340    pub uuid: [u8; 16],
341
342    /// Volume name
343    pub volume_name: [u8; 16],
344
345    /// Incompatible feature flags
346    pub feature_incompat: U32,
347    /// Available compression algorithms bitmap
348    pub available_compr_algs: U16,
349    /// Number of extra devices
350    pub extra_devices: U16,
351    /// Device slot offset
352    pub devt_slotoff: U16,
353    /// Directory block size in bits
354    pub dirblkbits: u8,
355    /// Number of xattr prefixes
356    pub xattr_prefix_count: u8,
357    /// Starting position of xattr prefix table
358    pub xattr_prefix_start: U32,
359
360    /// Packed inode number
361    pub packed_nid: U64,
362    /// Reserved for xattr filtering
363    pub xattr_filter_reserved: u8,
364    /// Reserved for future use
365    pub reserved2: [u8; 23],
366}
367
368/* Inodes */
369
370/// Compact 32-byte inode header for basic file metadata
371#[derive(Default, FromBytes, Immutable, IntoBytes, KnownLayout)]
372#[repr(C)]
373pub struct CompactInodeHeader {
374    /// Format field combining inode layout and data layout
375    pub format: FormatField,
376    /// Extended attribute inode count
377    pub xattr_icount: U16,
378    /// File mode (type and permissions)
379    pub mode: ModeField,
380    /// Number of hard links
381    pub nlink: U16,
382
383    /// File size in bytes
384    pub size: U32,
385    /// Reserved field
386    pub reserved: U32,
387
388    /// Union field (block address, device number, etc.)
389    pub u: U32,
390    /// Inode number for 32-bit stat compatibility
391    pub ino: U32, // only used for 32-bit stat compatibility
392
393    /// User ID
394    pub uid: U16,
395    /// Group ID
396    pub gid: U16,
397    /// Reserved field
398    pub reserved2: [u8; 4],
399}
400
401/// Extended 64-byte inode header with additional metadata fields
402#[derive(Default, FromBytes, Immutable, IntoBytes, KnownLayout)]
403#[repr(C)]
404pub struct ExtendedInodeHeader {
405    /// Format field combining inode layout and data layout
406    pub format: FormatField,
407    /// Extended attribute inode count
408    pub xattr_icount: U16,
409    /// File mode (type and permissions)
410    pub mode: ModeField,
411    /// Reserved field
412    pub reserved: U16,
413    /// File size in bytes
414    pub size: U64,
415
416    /// Union field (block address, device number, etc.)
417    pub u: U32,
418    /// Inode number for 32-bit stat compatibility
419    pub ino: U32, // only used for 32-bit stat compatibility
420    /// User ID
421    pub uid: U32,
422    /// Group ID
423    pub gid: U32,
424
425    /// Modification time in seconds since epoch
426    pub mtime: U64,
427
428    /// Modification time nanoseconds component
429    pub mtime_nsec: U32,
430    /// Number of hard links
431    pub nlink: U32,
432
433    /// Reserved field
434    pub reserved2: [u8; 16],
435}
436
437/// Header for inode extended attributes section
438#[derive(Debug, Default, FromBytes, Immutable, IntoBytes, KnownLayout)]
439#[repr(C)]
440pub struct InodeXAttrHeader {
441    /// Name filter hash for quick xattr lookups
442    pub name_filter: U32,
443    /// Number of shared xattr references
444    pub shared_count: u8,
445    /// Reserved field
446    pub reserved: [u8; 7],
447}
448
449/* Extended attributes */
450/// Seed value for xattr name filter hash calculation
451pub const XATTR_FILTER_SEED: u32 = 0x25BBE08F;
452
453/// Header for an extended attribute entry
454#[derive(Debug, FromBytes, Immutable, IntoBytes, KnownLayout)]
455#[repr(C)]
456pub struct XAttrHeader {
457    /// Length of the attribute name suffix
458    pub name_len: u8,
459    /// Index into the xattr prefix table
460    pub name_index: u8,
461    /// Size of the attribute value
462    pub value_size: U16,
463}
464
465/// Standard xattr name prefixes indexed by name_index
466pub const XATTR_PREFIXES: [&[u8]; 7] = [
467    b"",
468    b"user.",
469    b"system.posix_acl_access",
470    b"system.posix_acl_default",
471    b"trusted.",
472    b"lustre.",
473    b"security.",
474];
475
476/* Directories */
477
478/// Header for a directory entry
479#[derive(Debug, Default, FromBytes, Immutable, IntoBytes, KnownLayout)]
480#[repr(C)]
481pub struct DirectoryEntryHeader {
482    /// Inode number of the entry
483    pub inode_offset: U64,
484    /// Offset to the entry name within the directory block
485    pub name_offset: U16,
486    /// File type of the entry
487    pub file_type: FileTypeField,
488    /// Reserved field
489    pub reserved: u8,
490}