use bitflags::bitflags;
bitflags! {
#[derive(Debug, Clone, Copy)]
pub struct Compat: u32 {
const DIR_PREALLOC = 0x0001;
const IMAGIC_INODES = 0x0002;
const HAS_JOURNAL = 0x0004;
const EXT_ATTR = 0x0008;
const RESIZE_INODE = 0x0010;
const DIR_INDEX = 0x0020; const LAZY_BG = 0x0040;
const SPARSE_SUPER2 = 0x0200;
const FAST_COMMIT = 0x0400;
const ORPHAN_FILE = 0x1000;
}
#[derive(Debug, Clone, Copy)]
pub struct Incompat: u32 {
const COMPRESSION = 0x00001;
const FILETYPE = 0x00002;
const RECOVER = 0x00004;
const JOURNAL_DEV = 0x00008;
const META_BG = 0x00010;
const EXTENTS = 0x00040;
const BIT64 = 0x00080;
const MMP = 0x00100;
const FLEX_BG = 0x00200;
const EA_INODE = 0x00400;
const DIRDATA = 0x01000;
const CSUM_SEED = 0x02000;
const LARGEDIR = 0x04000;
const INLINE_DATA = 0x08000;
const ENCRYPT = 0x10000;
const CASEFOLD = 0x20000;
}
#[derive(Debug, Clone, Copy)]
pub struct RoCompat: u32 {
const SPARSE_SUPER = 0x0001;
const LARGE_FILE = 0x0002;
const BTREE_DIR = 0x0004;
const HUGE_FILE = 0x0008;
const GDT_CSUM = 0x0010;
const DIR_NLINK = 0x0020;
const EXTRA_ISIZE = 0x0040;
const HAS_SNAPSHOT = 0x0080;
const QUOTA = 0x0100;
const BIGALLOC = 0x0200;
const METADATA_CSUM = 0x0400;
const REPLICA = 0x0800;
const READONLY = 0x1000;
const PROJECT = 0x2000;
const VERITY = 0x8000;
const ORPHAN_PRESENT = 0x10000;
}
}
pub const SUPPORTED_INCOMPAT: u32 = Incompat::FILETYPE.bits()
| Incompat::EXTENTS.bits()
| Incompat::BIT64.bits()
| Incompat::FLEX_BG.bits()
| Incompat::CSUM_SEED.bits()
| Incompat::RECOVER.bits() | Incompat::MMP.bits() | Incompat::INLINE_DATA.bits() | Incompat::LARGEDIR.bits()
| Incompat::EA_INODE.bits()
| Incompat::CASEFOLD.bits();
pub const SUPPORTED_RO_COMPAT: u32 = RoCompat::SPARSE_SUPER.bits()
| RoCompat::LARGE_FILE.bits()
| RoCompat::HUGE_FILE.bits()
| RoCompat::GDT_CSUM.bits()
| RoCompat::DIR_NLINK.bits()
| RoCompat::EXTRA_ISIZE.bits()
| RoCompat::QUOTA.bits()
| RoCompat::METADATA_CSUM.bits()
| RoCompat::PROJECT.bits()
| RoCompat::ORPHAN_PRESENT.bits();
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FsFlavor {
Ext2,
Ext3,
Ext4,
}
impl FsFlavor {
pub fn detect(feature_compat: u32, feature_incompat: u32) -> Self {
let has_extents = (feature_incompat & Incompat::EXTENTS.bits()) != 0;
let has_journal = (feature_compat & Compat::HAS_JOURNAL.bits()) != 0;
match (has_extents, has_journal) {
(true, _) => FsFlavor::Ext4,
(false, true) => FsFlavor::Ext3,
(false, false) => FsFlavor::Ext2,
}
}
pub fn uses_extents(&self) -> bool {
matches!(self, FsFlavor::Ext4)
}
pub fn has_journal(&self) -> bool {
matches!(self, FsFlavor::Ext3 | FsFlavor::Ext4)
}
pub fn name(&self) -> &'static str {
match self {
FsFlavor::Ext2 => "ext2",
FsFlavor::Ext3 => "ext3",
FsFlavor::Ext4 => "ext4",
}
}
pub(crate) fn geometry(&self) -> (u16, u16, bool, usize) {
let csum_enabled = matches!(self, FsFlavor::Ext4);
let inode_size: u16 = if csum_enabled { 256 } else { 128 };
let desc_size: u16 = if csum_enabled { 64 } else { 32 };
let dir_csum_tail: usize = if csum_enabled { 12 } else { 0 };
(inode_size, desc_size, csum_enabled, dir_csum_tail)
}
}
pub const READ_BREAKING_RO_COMPAT: u32 = RoCompat::BIGALLOC.bits();
pub fn check_mountable(feature_incompat: u32, feature_ro_compat: u32) -> crate::error::Result<()> {
let unsupported_incompat = feature_incompat & !SUPPORTED_INCOMPAT;
if unsupported_incompat != 0 {
return Err(crate::error::Error::UnsupportedIncompat(
unsupported_incompat,
));
}
let breaking = feature_ro_compat & READ_BREAKING_RO_COMPAT;
if breaking != 0 {
return Err(crate::error::Error::UnsupportedRoCompat(breaking));
}
Ok(())
}
#[cfg(test)]
mod mountability_tests {
use super::*;
#[test]
fn a_bigalloc_filesystem_is_refused() {
let err = check_mountable(0, RoCompat::BIGALLOC.bits())
.expect_err("bigalloc changes the allocation unit and must not be mounted blind");
match err {
crate::error::Error::UnsupportedRoCompat(bits) => {
assert_eq!(bits, RoCompat::BIGALLOC.bits());
}
other => panic!("expected UnsupportedRoCompat, got {other:?}"),
}
}
#[test]
fn the_other_ro_compat_bits_still_mount() {
let tolerated = SUPPORTED_RO_COMPAT & !READ_BREAKING_RO_COMPAT;
assert_ne!(tolerated, 0, "nothing to check — the masks are wrong");
for shift in 0..32 {
let bit = 1u32 << shift;
if tolerated & bit == 0 {
continue;
}
check_mountable(0, bit)
.unwrap_or_else(|e| panic!("ro_compat {bit:#x} should mount read-only, got {e:?}"));
}
}
#[test]
fn an_unknown_ro_compat_bit_still_mounts() {
check_mountable(0, 0x8000_0000).expect("unknown RO_COMPAT is safe on a read-only mount");
}
#[test]
fn the_supported_and_breaking_masks_are_disjoint() {
assert_eq!(
SUPPORTED_RO_COMPAT & READ_BREAKING_RO_COMPAT,
0,
"a bit cannot be both tolerated and refused"
);
}
#[test]
fn an_unsupported_incompat_bit_is_still_refused() {
assert!(check_mountable(0x8000_0000, 0).is_err());
}
}