use crate::entry::FileEntry;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AttrSource {
Explicit,
Replaced,
Parent,
HostFile,
Fallback,
}
impl AttrSource {
pub fn label(self) -> &'static str {
match self {
Self::Explicit => "explicit",
Self::Replaced => "from replaced file",
Self::Parent => "from parent dir",
Self::HostFile => "from host file",
Self::Fallback => "default",
}
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct AttrOverrides {
pub mode: Option<u32>,
pub uid: Option<u32>,
pub gid: Option<u32>,
pub unix_times: Option<crate::times::UnixTimes>,
}
impl AttrOverrides {
pub fn is_empty(&self) -> bool {
self.mode.is_none() && self.uid.is_none() && self.gid.is_none() && self.unix_times.is_none()
}
pub fn has_permissions(&self) -> bool {
self.mode.is_some() || self.uid.is_some() || self.gid.is_some()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResolvedAttrs {
pub mode: u32,
pub uid: u32,
pub gid: u32,
pub mode_from: AttrSource,
pub uid_from: AttrSource,
pub gid_from: AttrSource,
}
impl ResolvedAttrs {
pub fn describe(&self) -> String {
format!(
"mode {:04o} ({}), uid {} ({}), gid {} ({})",
self.mode,
self.mode_from.label(),
self.uid,
self.uid_from.label(),
self.gid,
self.gid_from.label(),
)
}
pub fn file_mode(&self) -> u32 {
0o100_000 | (self.mode & 0o7777)
}
pub fn dir_mode(&self) -> u32 {
0o040_000 | (self.mode & 0o7777)
}
pub fn is_all_fallback(&self) -> bool {
self.mode_from == AttrSource::Fallback
&& self.uid_from == AttrSource::Fallback
&& self.gid_from == AttrSource::Fallback
}
}
fn perms_of(entry: &FileEntry) -> Option<u32> {
entry.mode.map(|m| m & 0o7777)
}
pub fn resolve_attrs(
overrides: &AttrOverrides,
replacing: Option<&FileEntry>,
parent: Option<&FileEntry>,
host_mode: Option<u32>,
fallback_mode: u32,
) -> ResolvedAttrs {
let (mode, mode_from) = if let Some(m) = overrides.mode {
(m & 0o7777, AttrSource::Explicit)
} else if let Some(m) = replacing.and_then(perms_of) {
(m, AttrSource::Replaced)
} else if let Some(m) = host_mode {
(m & 0o7777, AttrSource::HostFile)
} else {
(fallback_mode & 0o7777, AttrSource::Fallback)
};
let (uid, uid_from) = resolve_id(
overrides.uid,
replacing.and_then(|e| e.uid),
parent.and_then(|e| e.uid),
);
let (gid, gid_from) = resolve_id(
overrides.gid,
replacing.and_then(|e| e.gid),
parent.and_then(|e| e.gid),
);
ResolvedAttrs {
mode,
uid,
gid,
mode_from,
uid_from,
gid_from,
}
}
pub fn add_execute_where_read(mode: u32) -> u32 {
let mut out = mode;
for shift in [6, 3, 0] {
if mode >> shift & 0o4 != 0 {
out |= 0o1 << shift;
}
}
out
}
pub fn inherited_xattrs(
fs: &mut dyn crate::filesystem::Filesystem,
replacing: Option<&FileEntry>,
) -> Vec<crate::xattr::Xattr> {
let Some(entry) = replacing else {
return Vec::new();
};
if !fs.supports_xattrs() {
return Vec::new();
}
fs.list_xattrs(entry).unwrap_or_default()
}
pub fn resolve_dir_attrs(
overrides: &AttrOverrides,
replacing: Option<&FileEntry>,
parent: Option<&FileEntry>,
) -> ResolvedAttrs {
let (mode, mode_from) = if let Some(m) = overrides.mode {
(m & 0o7777, AttrSource::Explicit)
} else if let Some(m) = replacing.and_then(perms_of) {
(add_execute_where_read(m), AttrSource::Replaced)
} else if let Some(m) = parent.and_then(perms_of) {
(add_execute_where_read(m), AttrSource::Parent)
} else {
(0o755, AttrSource::Fallback)
};
let (uid, uid_from) = resolve_id(
overrides.uid,
replacing.and_then(|e| e.uid),
parent.and_then(|e| e.uid),
);
let (gid, gid_from) = resolve_id(
overrides.gid,
replacing.and_then(|e| e.gid),
parent.and_then(|e| e.gid),
);
ResolvedAttrs {
mode,
uid,
gid,
mode_from,
uid_from,
gid_from,
}
}
fn resolve_id(
explicit: Option<u32>,
replaced: Option<u32>,
parent: Option<u32>,
) -> (u32, AttrSource) {
if let Some(v) = explicit {
(v, AttrSource::Explicit)
} else if let Some(v) = replaced {
(v, AttrSource::Replaced)
} else if let Some(v) = parent {
(v, AttrSource::Parent)
} else {
(0, AttrSource::Fallback)
}
}
#[derive(Debug, Clone, Default)]
pub struct PreservedMeta {
pub mode: Option<u32>,
pub uid: Option<u32>,
pub gid: Option<u32>,
pub os_type: Option<[u8; 4]>,
pub os_creator: Option<[u8; 4]>,
pub aux_type: Option<u16>,
pub dos_attributes: Option<u16>,
pub amiga_protection: Option<u32>,
pub amiga_comment: Option<String>,
pub amiga_dates: Option<(i32, i32, i32)>,
pub mac_dates: Option<(u32, u32, u32)>,
pub xattrs: Vec<crate::xattr::Xattr>,
}
impl PreservedMeta {
pub fn is_empty(&self) -> bool {
self.mode.is_none()
&& self.uid.is_none()
&& self.gid.is_none()
&& self.os_type.is_none()
&& self.os_creator.is_none()
&& self.aux_type.is_none()
&& self.dos_attributes.is_none()
&& self.amiga_protection.is_none()
&& self.amiga_comment.is_none()
&& self.amiga_dates.is_none()
&& self.mac_dates.is_none()
&& self.xattrs.is_empty()
}
pub fn summary(&self) -> String {
let mut parts: Vec<String> = Vec::new();
if let Some(m) = self.mode {
parts.push(format!("mode {:04o}", m & 0o7777));
}
if let (Some(u), Some(g)) = (self.uid, self.gid) {
parts.push(format!("owner {u}:{g}"));
}
if let (Some(t), Some(c)) = (self.os_type, self.os_creator) {
parts.push(format!("type/creator {}/{}", ascii4(&t), ascii4(&c)));
}
if let Some(a) = self.dos_attributes {
parts.push(format!("DOS attrs 0x{a:02x}"));
}
if self.amiga_protection.is_some() {
parts.push("Amiga protection".to_string());
}
if self.mac_dates.is_some() || self.amiga_dates.is_some() {
parts.push("dates".to_string());
}
if !self.xattrs.is_empty() {
parts.push(format!("{} xattr(s)", self.xattrs.len()));
}
if parts.is_empty() {
"nothing to preserve".to_string()
} else {
parts.join(", ")
}
}
pub fn apply_to_options(&self, opts: &mut crate::filesystem::CreateFileOptions) {
if opts.mode.is_none() {
opts.mode = self.mode;
}
if opts.uid.is_none() {
opts.uid = self.uid;
}
if opts.gid.is_none() {
opts.gid = self.gid;
}
if opts.type_code.is_none() && opts.os_type.is_none() {
opts.os_type = self.os_type;
}
if opts.creator_code.is_none() && opts.os_creator.is_none() {
opts.os_creator = self.os_creator;
}
if opts.aux_type.is_none() {
opts.aux_type = self.aux_type;
}
if opts.dos_attributes.is_none() {
opts.dos_attributes = self.dos_attributes;
}
if opts.amiga_protection.is_none() {
opts.amiga_protection = self.amiga_protection;
}
if opts.amiga_comment.is_none() {
opts.amiga_comment = self.amiga_comment.clone();
}
if opts.amiga_dates.is_none() {
opts.amiga_dates = self.amiga_dates;
}
if opts.xattrs.is_empty() {
opts.xattrs = self.xattrs.clone();
}
}
pub fn reapply_dates(
&self,
fs: &mut dyn crate::filesystem::EditableFilesystem,
entry: &FileEntry,
) -> bool {
let Some((create, modify, backup)) = self.mac_dates else {
return false;
};
fs.set_dates(entry, create, modify, backup).is_ok()
}
}
fn ascii4(v: &[u8; 4]) -> String {
v.iter()
.map(|&b| {
if (0x20..0x7f).contains(&b) {
b as char
} else {
'.'
}
})
.collect()
}
pub fn preserved_meta(
fs: &mut dyn crate::filesystem::Filesystem,
replacing: Option<&FileEntry>,
) -> PreservedMeta {
let Some(entry) = replacing else {
return PreservedMeta::default();
};
PreservedMeta {
mode: entry.mode,
uid: entry.uid,
gid: entry.gid,
os_type: entry.type_code,
os_creator: entry.creator_code,
aux_type: entry.aux_type,
dos_attributes: entry.dos_attributes,
amiga_protection: entry.amiga_protection,
amiga_comment: entry.amiga_comment.clone(),
amiga_dates: entry.amiga_date,
mac_dates: entry.mac_dates,
xattrs: inherited_xattrs(fs, Some(entry)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::entry::FileEntry;
fn entry_with(mode: u32, uid: u32, gid: u32) -> FileEntry {
let mut e = FileEntry::new_file("f".into(), "/f".into(), 0, 0);
e.mode = Some(mode);
e.uid = Some(uid);
e.gid = Some(gid);
e
}
fn bare_dir() -> FileEntry {
FileEntry::new_directory("d".into(), "/d".into(), 0)
}
#[test]
fn explicit_overrides_win_over_everything() {
let replaced = entry_with(0o100_600, 7, 8);
let parent = entry_with(0o040_755, 9, 10);
let got = resolve_attrs(
&AttrOverrides {
mode: Some(0o755),
uid: Some(1),
gid: Some(2),
unix_times: None,
},
Some(&replaced),
Some(&parent),
Some(0o644),
0o644,
);
assert_eq!((got.mode, got.uid, got.gid), (0o755, 1, 2));
assert_eq!(got.mode_from, AttrSource::Explicit);
assert_eq!(got.uid_from, AttrSource::Explicit);
assert_eq!(got.gid_from, AttrSource::Explicit);
}
#[test]
fn replacing_a_file_inherits_its_mode_and_ownership() {
let shadow = entry_with(0o100_600, 0, 42);
let parent = entry_with(0o040_755, 0, 0);
let got = resolve_attrs(
&AttrOverrides::default(),
Some(&shadow),
Some(&parent),
Some(0o644),
0o644,
);
assert_eq!(got.mode, 0o600, "replaced file's mode must win over host's");
assert_eq!(got.uid, 0);
assert_eq!(got.gid, 42);
assert_eq!(got.mode_from, AttrSource::Replaced);
assert_eq!(got.gid_from, AttrSource::Replaced);
}
#[test]
fn new_file_takes_parent_ownership_and_host_mode() {
let parent = entry_with(0o040_755, 0, 20);
let got = resolve_attrs(
&AttrOverrides::default(),
None,
Some(&parent),
Some(0o755),
0o644,
);
assert_eq!(got.mode, 0o755);
assert_eq!(got.mode_from, AttrSource::HostFile);
assert_eq!(got.uid, 0);
assert_eq!(got.gid, 20);
assert_eq!(got.gid_from, AttrSource::Parent);
}
#[test]
fn falls_back_when_nothing_carries_metadata() {
let parent = bare_dir();
let got = resolve_attrs(&AttrOverrides::default(), None, Some(&parent), None, 0o644);
assert_eq!((got.mode, got.uid, got.gid), (0o644, 0, 0));
assert!(got.is_all_fallback());
}
#[test]
fn inherited_mode_drops_the_type_bits() {
let replaced = entry_with(0o100_640, 0, 0);
let got = resolve_attrs(
&AttrOverrides::default(),
Some(&replaced),
None,
None,
0o644,
);
assert_eq!(got.mode, 0o640);
assert_eq!(got.mode & 0o170_000, 0, "type bits must not survive");
}
#[test]
fn inherited_mode_keeps_setuid_and_sticky() {
let replaced = entry_with(0o104_755, 0, 0);
let got = resolve_attrs(
&AttrOverrides::default(),
Some(&replaced),
None,
None,
0o644,
);
assert_eq!(got.mode, 0o4755);
}
#[test]
fn add_execute_where_read_never_widens_a_private_dir() {
assert_eq!(add_execute_where_read(0o700), 0o700);
assert_eq!(add_execute_where_read(0o600), 0o700, "owner r -> owner x");
assert_eq!(add_execute_where_read(0o640), 0o750);
assert_eq!(add_execute_where_read(0o755), 0o755);
assert_eq!(add_execute_where_read(0o000), 0o000);
assert_eq!(add_execute_where_read(0o1644), 0o1755);
}
#[test]
fn new_dir_inherits_parent_mode_rather_than_a_blanket_0755() {
let private = entry_with(0o040_700, 0, 0);
let got = resolve_dir_attrs(&AttrOverrides::default(), None, Some(&private));
assert_eq!(got.mode, 0o700, "a subdir of /root must not be widened");
assert_eq!(got.mode_from, AttrSource::Parent);
}
#[test]
fn new_dir_becomes_traversable_by_whoever_can_read_it() {
let odd = entry_with(0o040_644, 0, 0);
let got = resolve_dir_attrs(&AttrOverrides::default(), None, Some(&odd));
assert_eq!(got.mode, 0o755);
}
#[test]
fn explicit_dir_mode_is_not_second_guessed() {
let got = resolve_dir_attrs(
&AttrOverrides {
mode: Some(0o644),
..Default::default()
},
None,
None,
);
assert_eq!(got.mode, 0o644);
assert_eq!(got.mode_from, AttrSource::Explicit);
}
#[test]
fn overrides_apply_uniformly_across_a_batch() {
let ov = AttrOverrides {
mode: Some(0o640),
uid: Some(0),
gid: Some(0),
unix_times: None,
};
let parents = [entry_with(0o040_755, 99, 99), entry_with(0o040_700, 5, 5)];
for parent in &parents {
let got = resolve_attrs(&ov, None, Some(parent), Some(0o777), 0o644);
assert_eq!((got.mode, got.uid, got.gid), (0o640, 0, 0));
}
}
#[test]
fn mode_helpers_carry_the_type_bits() {
let got = resolve_attrs(
&AttrOverrides {
mode: Some(0o640),
..Default::default()
},
None,
None,
None,
0o644,
);
assert_eq!(got.file_mode(), 0o100_640);
assert_eq!(got.dir_mode(), 0o040_640);
assert_eq!(got.file_mode() & 0o170_000, 0o100_000);
}
#[test]
fn describe_names_every_source() {
let replaced = entry_with(0o100_600, 0, 42);
let parent = entry_with(0o040_755, 0, 20);
let got = resolve_attrs(
&AttrOverrides {
uid: Some(5),
..Default::default()
},
Some(&replaced),
Some(&parent),
None,
0o644,
);
let text = got.describe();
assert!(text.contains("mode 0600 (from replaced file)"), "{text}");
assert!(text.contains("uid 5 (explicit)"), "{text}");
assert!(text.contains("gid 42 (from replaced file)"), "{text}");
assert!(text.is_ascii(), "log text must stay ASCII: {text}");
}
}