use crate::cmp::{self, ObjType};
use crate::{filecmp, preserve};
use anyhow::{Context, anyhow};
pub fn parse_metadata_cmp_settings(
settings: &str,
) -> Result<filecmp::MetadataCmpSettings, anyhow::Error> {
let mut metadata_cmp_settings = filecmp::MetadataCmpSettings::default();
for setting in settings.split(',') {
match setting {
"uid" => metadata_cmp_settings.uid = true,
"gid" => metadata_cmp_settings.gid = true,
"mode" => metadata_cmp_settings.mode = true,
"size" => metadata_cmp_settings.size = true,
"mtime" => metadata_cmp_settings.mtime = true,
"ctime" => metadata_cmp_settings.ctime = true,
_ => {
return Err(anyhow!("Unknown metadata comparison setting: {}", setting));
}
}
}
Ok(metadata_cmp_settings)
}
struct TypeSettings {
user_and_time: preserve::UserAndTimeSettings,
mode_mask: Option<preserve::ModeMask>,
acl: bool,
}
fn parse_type_settings(settings: &str) -> Result<TypeSettings, anyhow::Error> {
let mut user_and_time = preserve::UserAndTimeSettings::default();
let mut mode_mask = None;
let mut acl = false;
for setting in settings.split(',') {
match setting {
"uid" => user_and_time.uid = true,
"gid" => user_and_time.gid = true,
"time" => user_and_time.time = true,
"acl" => acl = true,
_ => {
if let Ok(mask) = u32::from_str_radix(setting, 8) {
mode_mask = Some(mask);
} else {
return Err(anyhow!("Unknown preserve attribute specified: {}", setting));
}
}
}
}
Ok(TypeSettings {
user_and_time,
mode_mask,
acl,
})
}
fn parse_preset_settings(settings: &str) -> Result<Option<preserve::Settings>, anyhow::Error> {
const PRESET_MODIFIERS: &str = "acl";
let mut tokens = settings.split('+');
let mut preset = match tokens.next().unwrap_or_default() {
"all" => preserve::preserve_all(),
"none" => preserve::preserve_none(),
_ => return Ok(None),
};
for modifier in tokens {
match modifier {
"acl" => preset = preset.with_acl(true),
_ => {
return Err(anyhow!(
"Unknown preserve preset modifier: {modifier:?}, expected one of: {PRESET_MODIFIERS}"
));
}
}
}
Ok(Some(preset))
}
fn check_acl_vs_mode_mask(settings: &preserve::Settings) -> Result<(), anyhow::Error> {
for (obj_type, acl, mode_mask) in [
("file", settings.file.acl, settings.file.mode_mask),
("directory", settings.dir.acl, settings.dir.mode_mask),
] {
if acl && mode_mask & 0o777 != 0o777 {
return Err(anyhow!(
"Preserve attribute `acl` conflicts with mode mask {mode_mask:04o} for type \
`{obj_type}`: an ACL carries the rwx permission bits, so a mask that narrows them \
would contradict it. Use a mask whose low 3 digits are 777 (e.g. 0777 or 7777)."
));
}
}
Ok(())
}
pub fn parse_preserve_settings(settings: &str) -> Result<preserve::Settings, anyhow::Error> {
let preserve_settings = match parse_preset_settings(settings)? {
Some(preset) => preset,
None => parse_per_type_settings(settings)?,
};
check_acl_vs_mode_mask(&preserve_settings)?;
Ok(preserve_settings)
}
fn parse_per_type_settings(settings: &str) -> Result<preserve::Settings, anyhow::Error> {
let mut preserve_settings = preserve::Settings::default();
for type_settings in settings.split_whitespace() {
if let Some((obj_type, obj_settings)) = type_settings.split_once(':') {
let parsed = parse_type_settings(obj_settings).context(format!(
"parsing preserve settings: {obj_settings}, type: {obj_type}"
))?;
match obj_type {
"f" | "file" => {
preserve_settings.file = preserve::FileSettings::default();
preserve_settings.file.user_and_time = parsed.user_and_time;
preserve_settings.file.acl = parsed.acl;
if let Some(mode) = parsed.mode_mask {
preserve_settings.file.mode_mask = mode;
}
}
"d" | "dir" | "directory" => {
preserve_settings.dir = preserve::DirSettings::default();
preserve_settings.dir.user_and_time = parsed.user_and_time;
preserve_settings.dir.acl = parsed.acl;
if let Some(mode) = parsed.mode_mask {
preserve_settings.dir.mode_mask = mode;
}
}
"l" | "link" | "symlink" => {
if parsed.acl {
return Err(anyhow!(
"Preserve attribute `acl` is not valid for type `{obj_type}`: symlinks \
cannot carry ACLs (the kernel has no symlink ACL)"
));
}
preserve_settings.symlink = preserve::SymlinkSettings::default();
preserve_settings.symlink.user_and_time = parsed.user_and_time;
}
_ => {
return Err(anyhow!("Unknown object type: {}", obj_type));
}
}
} else {
return Err(anyhow!("Invalid preserve settings: {}", settings));
}
}
Ok(preserve_settings)
}
pub fn validate_update_compare_vs_preserve(
update_compare: &filecmp::MetadataCmpSettings,
preserve: &preserve::Settings,
) -> Result<(), String> {
let mut missing = Vec::new();
if update_compare.mtime && !preserve.file.user_and_time.time {
missing.push("mtime");
}
if update_compare.uid && !preserve.file.user_and_time.uid {
missing.push("uid");
}
if update_compare.gid && !preserve.file.user_and_time.gid {
missing.push("gid");
}
if update_compare.mode && preserve.file.mode_mask != 0o7777 {
missing.push("mode");
}
if missing.is_empty() {
Ok(())
} else {
Err(format!(
"--update compares [{}] but --preserve-settings does not preserve them. \
Use --allow-lossy-update to override or adjust --preserve-settings.",
missing.join(", ")
))
}
}
pub fn parse_compare_settings(settings: &str) -> Result<cmp::ObjSettings, anyhow::Error> {
let mut cmp_settings = cmp::ObjSettings::default();
for type_settings in settings.split_whitespace() {
if let Some((obj_type, obj_settings)) = type_settings.split_once(':') {
let obj_cmp_settings = parse_metadata_cmp_settings(obj_settings).context(format!(
"parsing compare settings: {obj_settings}, type: {obj_type}"
))?;
let obj_type = match obj_type {
"f" | "file" => ObjType::File,
"d" | "dir" | "directory" => ObjType::Dir,
"l" | "link" | "symlink" => ObjType::Symlink,
"o" | "other" => ObjType::Other,
_ => {
return Err(anyhow!("Unknown obj type: {}", obj_type));
}
};
cmp_settings[obj_type] = obj_cmp_settings;
} else {
return Err(anyhow!("Invalid compare settings: {}", settings));
}
}
Ok(cmp_settings)
}
#[cfg(test)]
mod parse_preserve_settings_tests {
use super::*;
#[test]
fn preset_all_returns_preserve_all() {
let settings = parse_preserve_settings("all").unwrap();
let expected = preserve::preserve_all();
assert_eq!(settings.file.mode_mask, expected.file.mode_mask);
assert!(settings.file.user_and_time.uid);
assert!(settings.file.user_and_time.gid);
assert!(settings.file.user_and_time.time);
assert_eq!(settings.dir.mode_mask, expected.dir.mode_mask);
assert!(settings.dir.user_and_time.uid);
assert!(settings.dir.user_and_time.gid);
assert!(settings.dir.user_and_time.time);
assert!(settings.symlink.user_and_time.uid);
assert!(settings.symlink.user_and_time.gid);
assert!(settings.symlink.user_and_time.time);
}
#[test]
fn preset_none_returns_preserve_none() {
let settings = parse_preserve_settings("none").unwrap();
let expected = preserve::preserve_none();
assert_eq!(settings.file.mode_mask, expected.file.mode_mask);
assert!(!settings.file.user_and_time.uid);
assert!(!settings.file.user_and_time.gid);
assert!(!settings.file.user_and_time.time);
assert_eq!(settings.dir.mode_mask, expected.dir.mode_mask);
assert!(!settings.dir.user_and_time.uid);
assert!(!settings.dir.user_and_time.gid);
assert!(!settings.dir.user_and_time.time);
assert!(!settings.symlink.user_and_time.uid);
assert!(!settings.symlink.user_and_time.gid);
assert!(!settings.symlink.user_and_time.time);
}
#[test]
fn per_type_settings_still_work() {
let settings = parse_preserve_settings("f:uid,time,0777 d:gid").unwrap();
assert!(settings.file.user_and_time.uid);
assert!(settings.file.user_and_time.time);
assert!(!settings.file.user_and_time.gid);
assert_eq!(settings.file.mode_mask, 0o777);
assert!(!settings.dir.user_and_time.uid);
assert!(settings.dir.user_and_time.gid);
assert!(!settings.dir.user_and_time.time);
}
#[test]
fn invalid_settings_returns_error() {
assert!(parse_preserve_settings("invalid").is_err());
assert!(parse_preserve_settings("f:unknown_attr").is_err());
}
#[test]
fn preset_all_does_not_enable_acls() {
let settings = parse_preserve_settings("all").unwrap();
assert!(!settings.file.acl);
assert!(!settings.dir.acl);
}
#[test]
fn preset_all_plus_acl_enables_acls_on_files_and_dirs() {
let settings = parse_preserve_settings("all+acl").unwrap();
assert!(settings.file.acl);
assert!(settings.dir.acl);
assert!(settings.symlink.user_and_time.uid);
assert!(settings.symlink.user_and_time.gid);
assert!(settings.symlink.user_and_time.time);
let expected = preserve::preserve_all_with_acls();
assert_eq!(settings.file.mode_mask, expected.file.mode_mask);
assert_eq!(settings.dir.mode_mask, expected.dir.mode_mask);
assert!(settings.file.user_and_time.time);
assert!(settings.dir.user_and_time.time);
}
#[test]
fn preset_none_plus_acl_enables_only_acls() {
let settings = parse_preserve_settings("none+acl").unwrap();
assert!(settings.file.acl);
assert!(settings.dir.acl);
assert!(!settings.file.user_and_time.any());
assert!(!settings.dir.user_and_time.any());
assert!(!settings.symlink.user_and_time.any());
assert_eq!(
settings.file.mode_mask,
preserve::preserve_none().file.mode_mask
);
}
#[test]
fn repeated_preset_modifier_is_idempotent() {
let settings = parse_preserve_settings("all+acl+acl").unwrap();
assert!(settings.file.acl);
assert!(settings.dir.acl);
}
#[test]
fn unknown_preset_modifier_names_the_valid_set() {
let err = parse_preserve_settings("all+xattr")
.unwrap_err()
.to_string();
assert!(err.contains("xattr"), "{err}");
assert!(err.contains("acl"), "{err}");
assert!(parse_preserve_settings("none+bogus").is_err());
assert!(parse_preserve_settings("all+").is_err());
}
#[test]
fn plus_stays_an_ordinary_character_in_the_per_type_dsl() {
let settings = parse_preserve_settings("f:+777").unwrap();
assert_eq!(settings.file.mode_mask, 0o777);
assert!(!settings.file.acl);
let err = parse_preserve_settings("f:uid+d:gid")
.unwrap_err()
.to_string();
assert!(!err.contains("preset modifier"), "{err}");
}
#[test]
fn per_type_acl_attribute_parses() {
let settings =
parse_preserve_settings("f:uid,gid,time,acl,7777 d:uid,gid,time,acl,7777").unwrap();
assert!(settings.file.acl);
assert_eq!(settings.file.mode_mask, 0o7777);
assert!(settings.file.user_and_time.uid);
assert!(settings.dir.acl);
assert_eq!(settings.dir.mode_mask, 0o7777);
assert!(!settings.symlink.user_and_time.any());
}
#[test]
fn acl_on_symlinks_is_rejected() {
for spec in ["l:acl", "link:acl", "symlink:acl", "l:uid,acl"] {
let err = parse_preserve_settings(spec).unwrap_err().to_string();
assert!(err.contains("acl"), "{spec}: {err}");
assert!(err.contains("symlink"), "{spec}: {err}");
}
}
#[test]
fn acl_with_a_mask_that_narrows_rwx_is_rejected() {
for spec in [
"f:acl,0700", "f:acl,0770", "f:acl,7000", "d:acl,0755", "f:acl,0777 d:acl,0700",
] {
let err = parse_preserve_settings(spec).unwrap_err().to_string();
assert!(err.contains("acl"), "{spec}: {err}");
assert!(err.contains("mode mask"), "{spec}: {err}");
}
}
#[test]
fn acl_with_a_mask_that_only_strips_special_bits_is_accepted() {
assert!(parse_preserve_settings("f:acl,7777").unwrap().file.acl);
assert!(parse_preserve_settings("f:acl,0777").unwrap().file.acl);
let bare = parse_preserve_settings("f:acl").unwrap();
assert!(bare.file.acl);
assert!(!bare.dir.acl);
assert_eq!(bare.file.mode_mask, 0o0777);
assert!(parse_preserve_settings("all+acl").is_ok());
assert!(parse_preserve_settings("none+acl").is_ok());
}
#[test]
fn per_type_acl_applies_only_to_the_named_type() {
let file_only = parse_preserve_settings("f:acl").unwrap();
assert!(file_only.file.acl);
assert!(!file_only.dir.acl);
let dir_only = parse_preserve_settings("d:acl").unwrap();
assert!(dir_only.dir.acl);
assert!(!dir_only.file.acl);
let mixed = parse_preserve_settings("f:acl d:uid,gid").unwrap();
assert!(mixed.file.acl);
assert!(!mixed.dir.acl);
}
#[test]
fn narrowing_mask_without_acl_is_still_accepted() {
assert_eq!(
parse_preserve_settings("f:uid,0700")
.unwrap()
.file
.mode_mask,
0o700
);
}
}
#[cfg(test)]
mod validate_update_compare_vs_preserve_tests {
use super::*;
#[test]
fn detects_mtime_mismatch() {
let compare = filecmp::MetadataCmpSettings {
mtime: true,
..Default::default()
};
let preserve = preserve::preserve_none();
let result = validate_update_compare_vs_preserve(&compare, &preserve);
assert!(result.is_err());
assert!(result.unwrap_err().contains("mtime"));
}
#[test]
fn detects_uid_mismatch() {
let compare = filecmp::MetadataCmpSettings {
uid: true,
..Default::default()
};
let preserve = preserve::preserve_none();
let result = validate_update_compare_vs_preserve(&compare, &preserve);
assert!(result.is_err());
assert!(result.unwrap_err().contains("uid"));
}
#[test]
fn detects_gid_mismatch() {
let compare = filecmp::MetadataCmpSettings {
gid: true,
..Default::default()
};
let preserve = preserve::preserve_none();
let result = validate_update_compare_vs_preserve(&compare, &preserve);
assert!(result.is_err());
assert!(result.unwrap_err().contains("gid"));
}
#[test]
fn detects_mode_mismatch() {
let compare = filecmp::MetadataCmpSettings {
mode: true,
..Default::default()
};
let mut preserve = preserve::preserve_none();
preserve.file.mode_mask = 0;
let result = validate_update_compare_vs_preserve(&compare, &preserve);
assert!(result.is_err());
assert!(result.unwrap_err().contains("mode"));
}
#[test]
fn detects_multiple_mismatches() {
let compare = filecmp::MetadataCmpSettings {
mtime: true,
uid: true,
gid: true,
..Default::default()
};
let preserve = preserve::preserve_none();
let result = validate_update_compare_vs_preserve(&compare, &preserve);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.contains("mtime"));
assert!(err.contains("uid"));
assert!(err.contains("gid"));
}
#[test]
fn passes_when_preserve_covers_all_compared_attrs() {
let compare = filecmp::MetadataCmpSettings {
mtime: true,
uid: true,
gid: true,
mode: true,
size: true, ctime: true, };
let preserve = preserve::preserve_all();
let result = validate_update_compare_vs_preserve(&compare, &preserve);
assert!(result.is_ok());
}
#[test]
fn fails_with_partial_mode_mask_when_mode_compared() {
let compare = filecmp::MetadataCmpSettings {
mode: true,
..Default::default()
};
let preserve = preserve::preserve_none();
let result = validate_update_compare_vs_preserve(&compare, &preserve);
assert!(result.is_err());
assert!(result.unwrap_err().contains("mode"));
}
}