use fs_ext4::acl::{self, AclKind, AclTag, ACL_EXECUTE, ACL_READ, ACL_WRITE};
use fs_ext4::bgd;
use fs_ext4::block_io::{BlockDevice, FileDevice};
use fs_ext4::error::Result;
use fs_ext4::fs::Filesystem;
use fs_ext4::inode::Inode;
use fs_ext4::path;
use fs_ext4::xattr;
use std::path::Path;
use std::sync::Arc;
const TEST_IMAGE: &str = "test-disks/ext4-acl.img";
fn open_or_skip() -> Option<(Arc<dyn BlockDevice>, Filesystem)> {
if !Path::new(TEST_IMAGE).exists() {
eprintln!("skip: {TEST_IMAGE} not built; run test-disks/build-ext4-feature-images.sh acl");
return None;
}
let dev = Arc::new(FileDevice::open(TEST_IMAGE).expect("open acl image"));
let dev_dyn: Arc<dyn BlockDevice> = dev.clone();
let fs = Filesystem::mount(dev_dyn.clone()).expect("mount");
Some((dev_dyn, fs))
}
fn read_inode_raw_bytes(fs: &Filesystem, ino: u32) -> Vec<u8> {
let (block, offset) = bgd::locate_inode(&fs.sb, &fs.groups, ino).expect("locate");
let block_data = fs.read_block(block).expect("read block");
let inode_size = fs.sb.inode_size as usize;
let off = offset as usize;
block_data[off..off + inode_size].to_vec()
}
fn inode_reader(fs: &Filesystem) -> impl FnMut(u32) -> Result<Inode> + '_ {
move |ino: u32| -> Result<Inode> {
let (block, offset) = bgd::locate_inode(&fs.sb, &fs.groups, ino)?;
let block_data = fs.read_block(block)?;
let inode_size = fs.sb.inode_size as usize;
let off = offset as usize;
Inode::parse(&block_data[off..off + inode_size])
}
}
fn resolve(dev: &dyn BlockDevice, fs: &Filesystem, p: &str) -> u32 {
let mut reader = inode_reader(fs);
path::lookup(dev, &fs.sb, &mut reader, p).unwrap_or_else(|e| panic!("resolve {p}: {e}"))
}
fn read_parsed(fs: &Filesystem, ino: u32) -> Inode {
let raw = read_inode_raw_bytes(fs, ino);
Inode::parse(&raw).expect("parse")
}
#[test]
fn mode_only_file_has_three_short_entries() {
let Some((dev, fs)) = open_or_skip() else {
return;
};
let ino = resolve(dev.as_ref(), &fs, "/mode_only.txt");
let inode = read_parsed(&fs, ino);
let raw = read_inode_raw_bytes(&fs, ino);
let entries = acl::read(
dev.as_ref(),
&inode,
&raw,
fs.sb.inode_size,
fs.sb.block_size(),
AclKind::Access,
)
.expect("read acl");
let Some(entries) = entries else {
eprintln!("note: kernel stored mode-only ACL inline in st_mode (no xattr)");
return;
};
assert_eq!(entries.len(), 3, "expected 3 entries, got {entries:?}");
assert_eq!(entries[0].tag, AclTag::UserObj);
assert_eq!(entries[0].perm, ACL_READ | ACL_WRITE | ACL_EXECUTE);
assert_eq!(entries[1].tag, AclTag::GroupObj);
assert_eq!(entries[1].perm, ACL_READ | ACL_EXECUTE);
assert_eq!(entries[2].tag, AclTag::Other);
assert_eq!(entries[2].perm, ACL_READ);
}
#[test]
fn named_user_and_group_entries_present() {
let Some((dev, fs)) = open_or_skip() else {
return;
};
let ino = resolve(dev.as_ref(), &fs, "/named.txt");
let inode = read_parsed(&fs, ino);
let raw = read_inode_raw_bytes(&fs, ino);
let entries = acl::read(
dev.as_ref(),
&inode,
&raw,
fs.sb.inode_size,
fs.sb.block_size(),
AclKind::Access,
)
.expect("read acl")
.expect("/named.txt must have ACL xattr");
let user_1000 = entries
.iter()
.find(|e| e.tag == AclTag::User && e.id == Some(1000))
.expect("USER entry for uid 1000");
assert_eq!(user_1000.perm, ACL_READ | ACL_WRITE);
let group_2000 = entries
.iter()
.find(|e| e.tag == AclTag::Group && e.id == Some(2000))
.expect("GROUP entry for gid 2000");
assert_eq!(group_2000.perm, ACL_READ);
let mask = entries
.iter()
.find(|e| e.tag == AclTag::Mask)
.expect("MASK entry");
assert_eq!(mask.perm, ACL_READ | ACL_WRITE | ACL_EXECUTE);
}
#[test]
fn directory_has_access_and_default_acl() {
let Some((dev, fs)) = open_or_skip() else {
return;
};
let ino = resolve(dev.as_ref(), &fs, "/acl_dir");
let inode = read_parsed(&fs, ino);
let raw = read_inode_raw_bytes(&fs, ino);
let access = acl::read(
dev.as_ref(),
&inode,
&raw,
fs.sb.inode_size,
fs.sb.block_size(),
AclKind::Access,
)
.expect("read access acl");
if let Some(entries) = access {
assert!(entries.iter().any(|e| e.tag == AclTag::UserObj));
}
let default = acl::read(
dev.as_ref(),
&inode,
&raw,
fs.sb.inode_size,
fs.sb.block_size(),
AclKind::Default,
)
.expect("read default acl")
.expect("/acl_dir must have default ACL");
let user_obj = default
.iter()
.find(|e| e.tag == AclTag::UserObj)
.expect("default UserObj");
assert_eq!(user_obj.perm, ACL_READ | ACL_WRITE | ACL_EXECUTE);
let group_obj = default
.iter()
.find(|e| e.tag == AclTag::GroupObj)
.expect("default GroupObj");
assert_eq!(group_obj.perm, ACL_READ | ACL_EXECUTE);
let other = default
.iter()
.find(|e| e.tag == AclTag::Other)
.expect("default Other");
assert_eq!(other.perm, 0);
}
#[test]
fn plain_file_has_no_acl() {
let Some((dev, fs)) = open_or_skip() else {
return;
};
let ino = resolve(dev.as_ref(), &fs, "/plain.txt");
let inode = read_parsed(&fs, ino);
let raw = read_inode_raw_bytes(&fs, ino);
for kind in [AclKind::Access, AclKind::Default] {
let entries = acl::read(
dev.as_ref(),
&inode,
&raw,
fs.sb.inode_size,
fs.sb.block_size(),
kind,
)
.expect("read acl");
assert!(
entries.is_none(),
"/plain.txt {kind:?} should be absent: {entries:?}"
);
}
let all = xattr::read_all(
dev.as_ref(),
&inode,
&raw,
fs.sb.inode_size,
fs.sb.block_size(),
)
.expect("read xattrs");
assert!(
!all.iter().any(|e| e.name.starts_with("system.posix_acl_")),
"unexpected ACL xattr: {all:?}"
);
}