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
30fn parse_type_settings(
31 settings: &str,
32) -> Result<(preserve::UserAndTimeSettings, Option<preserve::ModeMask>), anyhow::Error> {
33 let mut user_and_time = preserve::UserAndTimeSettings::default();
34 let mut mode_mask = None;
35 for setting in settings.split(',') {
36 match setting {
37 "uid" => user_and_time.uid = true,
38 "gid" => user_and_time.gid = true,
39 "time" => user_and_time.time = true,
40 _ => {
41 if let Ok(mask) = u32::from_str_radix(setting, 8) {
42 mode_mask = Some(mask);
43 } else {
44 return Err(anyhow!("Unknown preserve attribute specified: {}", setting));
45 }
46 }
47 }
48 }
49 Ok((user_and_time, mode_mask))
50}
51
52pub fn parse_preserve_settings(settings: &str) -> Result<preserve::Settings, anyhow::Error> {
53 match settings {
55 "all" => return Ok(preserve::preserve_all()),
56 "none" => return Ok(preserve::preserve_none()),
57 _ => {}
58 }
59 let mut preserve_settings = preserve::Settings::default();
60 for type_settings in settings.split_whitespace() {
61 if let Some((obj_type, obj_settings)) = type_settings.split_once(':') {
62 let (user_and_time_settings, mode_opt) = parse_type_settings(obj_settings).context(
63 format!("parsing preserve settings: {obj_settings}, type: {obj_type}"),
64 )?;
65 match obj_type {
66 "f" | "file" => {
67 preserve_settings.file = preserve::FileSettings::default();
68 preserve_settings.file.user_and_time = user_and_time_settings;
69 if let Some(mode) = mode_opt {
70 preserve_settings.file.mode_mask = mode;
71 }
72 }
73 "d" | "dir" | "directory" => {
74 preserve_settings.dir = preserve::DirSettings::default();
75 preserve_settings.dir.user_and_time = user_and_time_settings;
76 if let Some(mode) = mode_opt {
77 preserve_settings.dir.mode_mask = mode;
78 }
79 }
80 "l" | "link" | "symlink" => {
81 preserve_settings.symlink = preserve::SymlinkSettings::default();
82 preserve_settings.symlink.user_and_time = user_and_time_settings;
83 }
84 _ => {
85 return Err(anyhow!("Unknown object type: {}", obj_type));
86 }
87 }
88 } else {
89 return Err(anyhow!("Invalid preserve settings: {}", settings));
90 }
91 }
92 Ok(preserve_settings)
93}
94
95pub fn validate_update_compare_vs_preserve(
98 update_compare: &filecmp::MetadataCmpSettings,
99 preserve: &preserve::Settings,
100) -> Result<(), String> {
101 let mut missing = Vec::new();
102 if update_compare.mtime && !preserve.file.user_and_time.time {
103 missing.push("mtime");
104 }
105 if update_compare.uid && !preserve.file.user_and_time.uid {
106 missing.push("uid");
107 }
108 if update_compare.gid && !preserve.file.user_and_time.gid {
109 missing.push("gid");
110 }
111 if update_compare.mode && preserve.file.mode_mask != 0o7777 {
113 missing.push("mode");
114 }
115 if missing.is_empty() {
116 Ok(())
117 } else {
118 Err(format!(
119 "--update compares [{}] but --preserve-settings does not preserve them. \
120 Use --allow-lossy-update to override or adjust --preserve-settings.",
121 missing.join(", ")
122 ))
123 }
124}
125
126pub fn parse_compare_settings(settings: &str) -> Result<cmp::ObjSettings, anyhow::Error> {
127 let mut cmp_settings = cmp::ObjSettings::default();
128 for type_settings in settings.split_whitespace() {
129 if let Some((obj_type, obj_settings)) = type_settings.split_once(':') {
130 let obj_cmp_settings = parse_metadata_cmp_settings(obj_settings).context(format!(
131 "parsing compare settings: {obj_settings}, type: {obj_type}"
132 ))?;
133 let obj_type = match obj_type {
134 "f" | "file" => ObjType::File,
135 "d" | "dir" | "directory" => ObjType::Dir,
136 "l" | "link" | "symlink" => ObjType::Symlink,
137 "o" | "other" => ObjType::Other,
138 _ => {
139 return Err(anyhow!("Unknown obj type: {}", obj_type));
140 }
141 };
142 cmp_settings[obj_type] = obj_cmp_settings;
143 } else {
144 return Err(anyhow!("Invalid compare settings: {}", settings));
145 }
146 }
147 Ok(cmp_settings)
148}
149
150#[cfg(test)]
151mod parse_preserve_settings_tests {
152 use super::*;
153 #[test]
154 fn preset_all_returns_preserve_all() {
155 let settings = parse_preserve_settings("all").unwrap();
156 let expected = preserve::preserve_all();
157 assert_eq!(settings.file.mode_mask, expected.file.mode_mask);
158 assert!(settings.file.user_and_time.uid);
159 assert!(settings.file.user_and_time.gid);
160 assert!(settings.file.user_and_time.time);
161 assert_eq!(settings.dir.mode_mask, expected.dir.mode_mask);
162 assert!(settings.dir.user_and_time.uid);
163 assert!(settings.dir.user_and_time.gid);
164 assert!(settings.dir.user_and_time.time);
165 assert!(settings.symlink.user_and_time.uid);
166 assert!(settings.symlink.user_and_time.gid);
167 assert!(settings.symlink.user_and_time.time);
168 }
169 #[test]
170 fn preset_none_returns_preserve_none() {
171 let settings = parse_preserve_settings("none").unwrap();
172 let expected = preserve::preserve_none();
173 assert_eq!(settings.file.mode_mask, expected.file.mode_mask);
174 assert!(!settings.file.user_and_time.uid);
175 assert!(!settings.file.user_and_time.gid);
176 assert!(!settings.file.user_and_time.time);
177 assert_eq!(settings.dir.mode_mask, expected.dir.mode_mask);
178 assert!(!settings.dir.user_and_time.uid);
179 assert!(!settings.dir.user_and_time.gid);
180 assert!(!settings.dir.user_and_time.time);
181 assert!(!settings.symlink.user_and_time.uid);
182 assert!(!settings.symlink.user_and_time.gid);
183 assert!(!settings.symlink.user_and_time.time);
184 }
185 #[test]
186 fn per_type_settings_still_work() {
187 let settings = parse_preserve_settings("f:uid,time,0777 d:gid").unwrap();
188 assert!(settings.file.user_and_time.uid);
189 assert!(settings.file.user_and_time.time);
190 assert!(!settings.file.user_and_time.gid);
191 assert_eq!(settings.file.mode_mask, 0o777);
192 assert!(!settings.dir.user_and_time.uid);
193 assert!(settings.dir.user_and_time.gid);
194 assert!(!settings.dir.user_and_time.time);
195 }
196 #[test]
197 fn invalid_settings_returns_error() {
198 assert!(parse_preserve_settings("invalid").is_err());
199 assert!(parse_preserve_settings("f:unknown_attr").is_err());
200 }
201}
202
203#[cfg(test)]
204mod validate_update_compare_vs_preserve_tests {
205 use super::*;
206 #[test]
207 fn detects_mtime_mismatch() {
208 let compare = filecmp::MetadataCmpSettings {
209 mtime: true,
210 ..Default::default()
211 };
212 let preserve = preserve::preserve_none();
213 let result = validate_update_compare_vs_preserve(&compare, &preserve);
214 assert!(result.is_err());
215 assert!(result.unwrap_err().contains("mtime"));
216 }
217 #[test]
218 fn detects_uid_mismatch() {
219 let compare = filecmp::MetadataCmpSettings {
220 uid: true,
221 ..Default::default()
222 };
223 let preserve = preserve::preserve_none();
224 let result = validate_update_compare_vs_preserve(&compare, &preserve);
225 assert!(result.is_err());
226 assert!(result.unwrap_err().contains("uid"));
227 }
228 #[test]
229 fn detects_gid_mismatch() {
230 let compare = filecmp::MetadataCmpSettings {
231 gid: true,
232 ..Default::default()
233 };
234 let preserve = preserve::preserve_none();
235 let result = validate_update_compare_vs_preserve(&compare, &preserve);
236 assert!(result.is_err());
237 assert!(result.unwrap_err().contains("gid"));
238 }
239 #[test]
240 fn detects_mode_mismatch() {
241 let compare = filecmp::MetadataCmpSettings {
242 mode: true,
243 ..Default::default()
244 };
245 let mut preserve = preserve::preserve_none();
246 preserve.file.mode_mask = 0;
247 let result = validate_update_compare_vs_preserve(&compare, &preserve);
248 assert!(result.is_err());
249 assert!(result.unwrap_err().contains("mode"));
250 }
251 #[test]
252 fn detects_multiple_mismatches() {
253 let compare = filecmp::MetadataCmpSettings {
254 mtime: true,
255 uid: true,
256 gid: true,
257 ..Default::default()
258 };
259 let preserve = preserve::preserve_none();
260 let result = validate_update_compare_vs_preserve(&compare, &preserve);
261 assert!(result.is_err());
262 let err = result.unwrap_err();
263 assert!(err.contains("mtime"));
264 assert!(err.contains("uid"));
265 assert!(err.contains("gid"));
266 }
267 #[test]
268 fn passes_when_preserve_covers_all_compared_attrs() {
269 let compare = filecmp::MetadataCmpSettings {
270 mtime: true,
271 uid: true,
272 gid: true,
273 mode: true,
274 size: true, ctime: true, };
277 let preserve = preserve::preserve_all();
278 let result = validate_update_compare_vs_preserve(&compare, &preserve);
279 assert!(result.is_ok());
280 }
281 #[test]
282 fn fails_with_partial_mode_mask_when_mode_compared() {
283 let compare = filecmp::MetadataCmpSettings {
286 mode: true,
287 ..Default::default()
288 };
289 let preserve = preserve::preserve_none();
290 let result = validate_update_compare_vs_preserve(&compare, &preserve);
291 assert!(result.is_err());
292 assert!(result.unwrap_err().contains("mode"));
293 }
294}