1use crate::cmp::{self, ObjType};
7use crate::{filecmp, preserve};
8use anyhow::{Context, anyhow};
9
10pub fn parse_metadata_cmp_settings(
11 settings: &str,
12) -> Result<filecmp::MetadataCmpSettings, anyhow::Error> {
13 let mut metadata_cmp_settings = filecmp::MetadataCmpSettings::default();
14 for setting in settings.split(',') {
15 match setting {
16 "uid" => metadata_cmp_settings.uid = true,
17 "gid" => metadata_cmp_settings.gid = true,
18 "mode" => metadata_cmp_settings.mode = true,
19 "size" => metadata_cmp_settings.size = true,
20 "mtime" => metadata_cmp_settings.mtime = true,
21 "ctime" => metadata_cmp_settings.ctime = true,
22 _ => {
23 return Err(anyhow!("Unknown metadata comparison setting: {}", setting));
24 }
25 }
26 }
27 Ok(metadata_cmp_settings)
28}
29
30struct TypeSettings {
32 user_and_time: preserve::UserAndTimeSettings,
33 mode_mask: Option<preserve::ModeMask>,
35 acl: bool,
36}
37
38fn parse_type_settings(settings: &str) -> Result<TypeSettings, anyhow::Error> {
39 let mut user_and_time = preserve::UserAndTimeSettings::default();
40 let mut mode_mask = None;
41 let mut acl = false;
42 for setting in settings.split(',') {
43 match setting {
44 "uid" => user_and_time.uid = true,
45 "gid" => user_and_time.gid = true,
46 "time" => user_and_time.time = true,
47 "acl" => acl = true,
48 _ => {
49 if let Ok(mask) = u32::from_str_radix(setting, 8) {
50 mode_mask = Some(mask);
51 } else {
52 return Err(anyhow!("Unknown preserve attribute specified: {}", setting));
53 }
54 }
55 }
56 }
57 Ok(TypeSettings {
58 user_and_time,
59 mode_mask,
60 acl,
61 })
62}
63
64fn parse_preset_settings(settings: &str) -> Result<Option<preserve::Settings>, anyhow::Error> {
70 const PRESET_MODIFIERS: &str = "acl";
72 let mut tokens = settings.split('+');
73 let mut preset = match tokens.next().unwrap_or_default() {
75 "all" => preserve::preserve_all(),
76 "none" => preserve::preserve_none(),
77 _ => return Ok(None),
78 };
79 for modifier in tokens {
80 match modifier {
81 "acl" => preset = preset.with_acl(true),
83 _ => {
84 return Err(anyhow!(
87 "Unknown preserve preset modifier: {modifier:?}, expected one of: {PRESET_MODIFIERS}"
88 ));
89 }
90 }
91 }
92 Ok(Some(preset))
93}
94
95fn check_acl_vs_mode_mask(settings: &preserve::Settings) -> Result<(), anyhow::Error> {
103 for (obj_type, acl, mode_mask) in [
104 ("file", settings.file.acl, settings.file.mode_mask),
105 ("directory", settings.dir.acl, settings.dir.mode_mask),
106 ] {
107 if acl && mode_mask & 0o777 != 0o777 {
108 return Err(anyhow!(
109 "Preserve attribute `acl` conflicts with mode mask {mode_mask:04o} for type \
110 `{obj_type}`: an ACL carries the rwx permission bits, so a mask that narrows them \
111 would contradict it. Use a mask whose low 3 digits are 777 (e.g. 0777 or 7777)."
112 ));
113 }
114 }
115 Ok(())
116}
117
118pub fn parse_preserve_settings(settings: &str) -> Result<preserve::Settings, anyhow::Error> {
124 let preserve_settings = match parse_preset_settings(settings)? {
125 Some(preset) => preset,
126 None => parse_per_type_settings(settings)?,
127 };
128 check_acl_vs_mode_mask(&preserve_settings)?;
129 Ok(preserve_settings)
130}
131
132fn parse_per_type_settings(settings: &str) -> Result<preserve::Settings, anyhow::Error> {
133 let mut preserve_settings = preserve::Settings::default();
134 for type_settings in settings.split_whitespace() {
135 if let Some((obj_type, obj_settings)) = type_settings.split_once(':') {
136 let parsed = parse_type_settings(obj_settings).context(format!(
137 "parsing preserve settings: {obj_settings}, type: {obj_type}"
138 ))?;
139 match obj_type {
140 "f" | "file" => {
141 preserve_settings.file = preserve::FileSettings::default();
142 preserve_settings.file.user_and_time = parsed.user_and_time;
143 preserve_settings.file.acl = parsed.acl;
144 if let Some(mode) = parsed.mode_mask {
145 preserve_settings.file.mode_mask = mode;
146 }
147 }
148 "d" | "dir" | "directory" => {
149 preserve_settings.dir = preserve::DirSettings::default();
150 preserve_settings.dir.user_and_time = parsed.user_and_time;
151 preserve_settings.dir.acl = parsed.acl;
152 if let Some(mode) = parsed.mode_mask {
153 preserve_settings.dir.mode_mask = mode;
154 }
155 }
156 "l" | "link" | "symlink" => {
157 if parsed.acl {
160 return Err(anyhow!(
161 "Preserve attribute `acl` is not valid for type `{obj_type}`: symlinks \
162 cannot carry ACLs (the kernel has no symlink ACL)"
163 ));
164 }
165 preserve_settings.symlink = preserve::SymlinkSettings::default();
166 preserve_settings.symlink.user_and_time = parsed.user_and_time;
167 }
168 _ => {
169 return Err(anyhow!("Unknown object type: {}", obj_type));
170 }
171 }
172 } else {
173 return Err(anyhow!("Invalid preserve settings: {}", settings));
174 }
175 }
176 Ok(preserve_settings)
177}
178
179pub fn validate_update_compare_vs_preserve(
182 update_compare: &filecmp::MetadataCmpSettings,
183 preserve: &preserve::Settings,
184) -> Result<(), String> {
185 let mut missing = Vec::new();
186 if update_compare.mtime && !preserve.file.user_and_time.time {
187 missing.push("mtime");
188 }
189 if update_compare.uid && !preserve.file.user_and_time.uid {
190 missing.push("uid");
191 }
192 if update_compare.gid && !preserve.file.user_and_time.gid {
193 missing.push("gid");
194 }
195 if update_compare.mode && preserve.file.mode_mask != 0o7777 {
197 missing.push("mode");
198 }
199 if missing.is_empty() {
200 Ok(())
201 } else {
202 Err(format!(
203 "--update compares [{}] but --preserve-settings does not preserve them. \
204 Use --allow-lossy-update to override or adjust --preserve-settings.",
205 missing.join(", ")
206 ))
207 }
208}
209
210pub fn parse_compare_settings(settings: &str) -> Result<cmp::ObjSettings, anyhow::Error> {
211 let mut cmp_settings = cmp::ObjSettings::default();
212 for type_settings in settings.split_whitespace() {
213 if let Some((obj_type, obj_settings)) = type_settings.split_once(':') {
214 let obj_cmp_settings = parse_metadata_cmp_settings(obj_settings).context(format!(
215 "parsing compare settings: {obj_settings}, type: {obj_type}"
216 ))?;
217 let obj_type = match obj_type {
218 "f" | "file" => ObjType::File,
219 "d" | "dir" | "directory" => ObjType::Dir,
220 "l" | "link" | "symlink" => ObjType::Symlink,
221 "o" | "other" => ObjType::Other,
222 _ => {
223 return Err(anyhow!("Unknown obj type: {}", obj_type));
224 }
225 };
226 cmp_settings[obj_type] = obj_cmp_settings;
227 } else {
228 return Err(anyhow!("Invalid compare settings: {}", settings));
229 }
230 }
231 Ok(cmp_settings)
232}
233
234#[cfg(test)]
235mod parse_preserve_settings_tests {
236 use super::*;
237 #[test]
238 fn preset_all_returns_preserve_all() {
239 let settings = parse_preserve_settings("all").unwrap();
240 let expected = preserve::preserve_all();
241 assert_eq!(settings.file.mode_mask, expected.file.mode_mask);
242 assert!(settings.file.user_and_time.uid);
243 assert!(settings.file.user_and_time.gid);
244 assert!(settings.file.user_and_time.time);
245 assert_eq!(settings.dir.mode_mask, expected.dir.mode_mask);
246 assert!(settings.dir.user_and_time.uid);
247 assert!(settings.dir.user_and_time.gid);
248 assert!(settings.dir.user_and_time.time);
249 assert!(settings.symlink.user_and_time.uid);
250 assert!(settings.symlink.user_and_time.gid);
251 assert!(settings.symlink.user_and_time.time);
252 }
253 #[test]
254 fn preset_none_returns_preserve_none() {
255 let settings = parse_preserve_settings("none").unwrap();
256 let expected = preserve::preserve_none();
257 assert_eq!(settings.file.mode_mask, expected.file.mode_mask);
258 assert!(!settings.file.user_and_time.uid);
259 assert!(!settings.file.user_and_time.gid);
260 assert!(!settings.file.user_and_time.time);
261 assert_eq!(settings.dir.mode_mask, expected.dir.mode_mask);
262 assert!(!settings.dir.user_and_time.uid);
263 assert!(!settings.dir.user_and_time.gid);
264 assert!(!settings.dir.user_and_time.time);
265 assert!(!settings.symlink.user_and_time.uid);
266 assert!(!settings.symlink.user_and_time.gid);
267 assert!(!settings.symlink.user_and_time.time);
268 }
269 #[test]
270 fn per_type_settings_still_work() {
271 let settings = parse_preserve_settings("f:uid,time,0777 d:gid").unwrap();
272 assert!(settings.file.user_and_time.uid);
273 assert!(settings.file.user_and_time.time);
274 assert!(!settings.file.user_and_time.gid);
275 assert_eq!(settings.file.mode_mask, 0o777);
276 assert!(!settings.dir.user_and_time.uid);
277 assert!(settings.dir.user_and_time.gid);
278 assert!(!settings.dir.user_and_time.time);
279 }
280 #[test]
281 fn invalid_settings_returns_error() {
282 assert!(parse_preserve_settings("invalid").is_err());
283 assert!(parse_preserve_settings("f:unknown_attr").is_err());
284 }
285 #[test]
286 fn preset_all_does_not_enable_acls() {
287 let settings = parse_preserve_settings("all").unwrap();
289 assert!(!settings.file.acl);
290 assert!(!settings.dir.acl);
291 }
292 #[test]
293 fn preset_all_plus_acl_enables_acls_on_files_and_dirs() {
294 let settings = parse_preserve_settings("all+acl").unwrap();
295 assert!(settings.file.acl);
296 assert!(settings.dir.acl);
297 assert!(settings.symlink.user_and_time.uid);
300 assert!(settings.symlink.user_and_time.gid);
301 assert!(settings.symlink.user_and_time.time);
302 let expected = preserve::preserve_all_with_acls();
304 assert_eq!(settings.file.mode_mask, expected.file.mode_mask);
305 assert_eq!(settings.dir.mode_mask, expected.dir.mode_mask);
306 assert!(settings.file.user_and_time.time);
307 assert!(settings.dir.user_and_time.time);
308 }
309 #[test]
310 fn preset_none_plus_acl_enables_only_acls() {
311 let settings = parse_preserve_settings("none+acl").unwrap();
313 assert!(settings.file.acl);
314 assert!(settings.dir.acl);
315 assert!(!settings.file.user_and_time.any());
316 assert!(!settings.dir.user_and_time.any());
317 assert!(!settings.symlink.user_and_time.any());
318 assert_eq!(
319 settings.file.mode_mask,
320 preserve::preserve_none().file.mode_mask
321 );
322 }
323 #[test]
324 fn repeated_preset_modifier_is_idempotent() {
325 let settings = parse_preserve_settings("all+acl+acl").unwrap();
326 assert!(settings.file.acl);
327 assert!(settings.dir.acl);
328 }
329 #[test]
330 fn unknown_preset_modifier_names_the_valid_set() {
331 let err = parse_preserve_settings("all+xattr")
332 .unwrap_err()
333 .to_string();
334 assert!(err.contains("xattr"), "{err}");
335 assert!(err.contains("acl"), "{err}");
336 assert!(parse_preserve_settings("none+bogus").is_err());
337 assert!(parse_preserve_settings("all+").is_err());
339 }
340 #[test]
341 fn plus_stays_an_ordinary_character_in_the_per_type_dsl() {
342 let settings = parse_preserve_settings("f:+777").unwrap();
345 assert_eq!(settings.file.mode_mask, 0o777);
346 assert!(!settings.file.acl);
347 let err = parse_preserve_settings("f:uid+d:gid")
350 .unwrap_err()
351 .to_string();
352 assert!(!err.contains("preset modifier"), "{err}");
353 }
354 #[test]
355 fn per_type_acl_attribute_parses() {
356 let settings =
357 parse_preserve_settings("f:uid,gid,time,acl,7777 d:uid,gid,time,acl,7777").unwrap();
358 assert!(settings.file.acl);
359 assert_eq!(settings.file.mode_mask, 0o7777);
360 assert!(settings.file.user_and_time.uid);
361 assert!(settings.dir.acl);
362 assert_eq!(settings.dir.mode_mask, 0o7777);
363 assert!(!settings.symlink.user_and_time.any());
365 }
366 #[test]
367 fn acl_on_symlinks_is_rejected() {
368 for spec in ["l:acl", "link:acl", "symlink:acl", "l:uid,acl"] {
369 let err = parse_preserve_settings(spec).unwrap_err().to_string();
370 assert!(err.contains("acl"), "{spec}: {err}");
371 assert!(err.contains("symlink"), "{spec}: {err}");
372 }
373 }
374 #[test]
375 fn acl_with_a_mask_that_narrows_rwx_is_rejected() {
376 for spec in [
377 "f:acl,0700", "f:acl,0770", "f:acl,7000", "d:acl,0755", "f:acl,0777 d:acl,0700",
382 ] {
383 let err = parse_preserve_settings(spec).unwrap_err().to_string();
384 assert!(err.contains("acl"), "{spec}: {err}");
385 assert!(err.contains("mode mask"), "{spec}: {err}");
386 }
387 }
388 #[test]
389 fn acl_with_a_mask_that_only_strips_special_bits_is_accepted() {
390 assert!(parse_preserve_settings("f:acl,7777").unwrap().file.acl);
392 assert!(parse_preserve_settings("f:acl,0777").unwrap().file.acl);
393 let bare = parse_preserve_settings("f:acl").unwrap();
395 assert!(bare.file.acl);
396 assert!(!bare.dir.acl);
397 assert_eq!(bare.file.mode_mask, 0o0777);
398 assert!(parse_preserve_settings("all+acl").is_ok());
400 assert!(parse_preserve_settings("none+acl").is_ok());
401 }
402 #[test]
403 fn per_type_acl_applies_only_to_the_named_type() {
404 let file_only = parse_preserve_settings("f:acl").unwrap();
407 assert!(file_only.file.acl);
408 assert!(!file_only.dir.acl);
409 let dir_only = parse_preserve_settings("d:acl").unwrap();
410 assert!(dir_only.dir.acl);
411 assert!(!dir_only.file.acl);
412 let mixed = parse_preserve_settings("f:acl d:uid,gid").unwrap();
414 assert!(mixed.file.acl);
415 assert!(!mixed.dir.acl);
416 }
417 #[test]
418 fn narrowing_mask_without_acl_is_still_accepted() {
419 assert_eq!(
421 parse_preserve_settings("f:uid,0700")
422 .unwrap()
423 .file
424 .mode_mask,
425 0o700
426 );
427 }
428}
429
430#[cfg(test)]
431mod validate_update_compare_vs_preserve_tests {
432 use super::*;
433 #[test]
434 fn detects_mtime_mismatch() {
435 let compare = filecmp::MetadataCmpSettings {
436 mtime: true,
437 ..Default::default()
438 };
439 let preserve = preserve::preserve_none();
440 let result = validate_update_compare_vs_preserve(&compare, &preserve);
441 assert!(result.is_err());
442 assert!(result.unwrap_err().contains("mtime"));
443 }
444 #[test]
445 fn detects_uid_mismatch() {
446 let compare = filecmp::MetadataCmpSettings {
447 uid: true,
448 ..Default::default()
449 };
450 let preserve = preserve::preserve_none();
451 let result = validate_update_compare_vs_preserve(&compare, &preserve);
452 assert!(result.is_err());
453 assert!(result.unwrap_err().contains("uid"));
454 }
455 #[test]
456 fn detects_gid_mismatch() {
457 let compare = filecmp::MetadataCmpSettings {
458 gid: true,
459 ..Default::default()
460 };
461 let preserve = preserve::preserve_none();
462 let result = validate_update_compare_vs_preserve(&compare, &preserve);
463 assert!(result.is_err());
464 assert!(result.unwrap_err().contains("gid"));
465 }
466 #[test]
467 fn detects_mode_mismatch() {
468 let compare = filecmp::MetadataCmpSettings {
469 mode: true,
470 ..Default::default()
471 };
472 let mut preserve = preserve::preserve_none();
473 preserve.file.mode_mask = 0;
474 let result = validate_update_compare_vs_preserve(&compare, &preserve);
475 assert!(result.is_err());
476 assert!(result.unwrap_err().contains("mode"));
477 }
478 #[test]
479 fn detects_multiple_mismatches() {
480 let compare = filecmp::MetadataCmpSettings {
481 mtime: true,
482 uid: true,
483 gid: true,
484 ..Default::default()
485 };
486 let preserve = preserve::preserve_none();
487 let result = validate_update_compare_vs_preserve(&compare, &preserve);
488 assert!(result.is_err());
489 let err = result.unwrap_err();
490 assert!(err.contains("mtime"));
491 assert!(err.contains("uid"));
492 assert!(err.contains("gid"));
493 }
494 #[test]
495 fn passes_when_preserve_covers_all_compared_attrs() {
496 let compare = filecmp::MetadataCmpSettings {
497 mtime: true,
498 uid: true,
499 gid: true,
500 mode: true,
501 size: true, ctime: true, };
504 let preserve = preserve::preserve_all();
505 let result = validate_update_compare_vs_preserve(&compare, &preserve);
506 assert!(result.is_ok());
507 }
508 #[test]
509 fn fails_with_partial_mode_mask_when_mode_compared() {
510 let compare = filecmp::MetadataCmpSettings {
513 mode: true,
514 ..Default::default()
515 };
516 let preserve = preserve::preserve_none();
517 let result = validate_update_compare_vs_preserve(&compare, &preserve);
518 assert!(result.is_err());
519 assert!(result.unwrap_err().contains("mode"));
520 }
521}