1use anyhow::Result;
6use ini::Ini;
7use std::{collections::HashSet, path::PathBuf};
8
9use crate::log_debug;
10
11#[derive(Debug, Clone, Default, PartialEq)]
12pub struct IniCompare {
13 pub updated: Vec<(IniParameter, IniParameter)>,
14 pub added: Vec<IniParameter>,
15 pub deleted: Vec<IniParameter>,
16}
17
18impl IniCompare {
19 pub fn new() -> Self {
20 Self::default()
21 }
22}
23
24#[derive(Debug, Clone, Default, PartialEq, PartialOrd, Eq, Ord)]
25pub struct IniParameter {
26 section: Option<String>,
27 property: String,
28 value: String,
29}
30
31pub fn ini_compare(a: &PathBuf, b: &PathBuf) -> Result<IniCompare> {
33 let a_ini = Ini::load_from_file(a)?;
35
36 let b_ini = Ini::load_from_file(b)?;
38
39 let mut results = IniCompare::new();
41
42 let a_sections: HashSet<&str> = a_ini.iter().filter_map(|(s, _)| s).collect();
44 let b_sections: HashSet<&str> = b_ini.iter().filter_map(|(s, _)| s).collect();
45
46 let deleted_sections = a_sections.difference(&b_sections);
47 let added_sections = b_sections.difference(&a_sections);
48
49 for &added_section in added_sections {
50 if let Some((_, b_properties)) = b_ini
51 .iter()
52 .find(|&(b_section, _)| b_section == Some(added_section))
53 {
54 for b_property in b_properties.iter() {
55 results.added.push(IniParameter {
56 section: Some(added_section.to_string()),
57 property: b_property.0.to_string(),
58 value: b_property.1.to_string(),
59 })
60 }
61 }
62 }
63
64 for &deleted_section in deleted_sections {
65 if let Some((_, a_properties)) = a_ini
66 .iter()
67 .find(|&(a_section, _)| a_section == Some(deleted_section))
68 {
69 for a_property in a_properties.iter() {
70 results.deleted.push(IniParameter {
71 section: Some(deleted_section.to_string()),
72 property: a_property.0.to_string(),
73 value: a_property.1.to_string(),
74 })
75 }
76 }
77 }
78
79 for (a_section, a_properties) in a_ini.iter() {
81 if let Some((_, b_properties)) = b_ini.iter().find(|&(b_section, _)| a_section == b_section)
82 {
83 let a_keys: HashSet<&str> = a_properties.iter().map(|(key, _)| key).collect();
84 let b_keys: HashSet<&str> = b_properties.iter().map(|(key, _)| key).collect();
85
86 let added_keys: HashSet<&str> =
88 b_keys.difference(&a_keys).map(|x| x.to_owned()).collect();
89 let mut added: Vec<IniParameter> = added_keys
90 .iter()
91 .filter_map(|k| {
92 b_properties.get(k).map(|v| IniParameter {
93 section: a_section.map(|x| x.to_string()),
94 property: k.to_string(),
95 value: v.to_string(),
96 })
97 })
98 .collect();
99
100 let removed_keys: HashSet<&str> =
102 a_keys.difference(&b_keys).map(|x| x.to_owned()).collect();
103 let mut removed: Vec<IniParameter> = removed_keys
104 .iter()
105 .filter_map(|k| {
106 a_properties.get(k).map(|v| IniParameter {
107 section: a_section.map(|x| x.to_string()),
108 property: k.to_string(),
109 value: v.to_string(),
110 })
111 })
112 .collect();
113
114 let updated_keys: HashSet<&str> =
116 a_keys.intersection(&b_keys).map(|x| x.to_owned()).collect();
117
118 let mut updated: Vec<(IniParameter, IniParameter)> = vec![];
119
120 for key in updated_keys.iter() {
121 let a_value = a_properties.get(key);
122 let b_value = b_properties.get(key);
123
124 if a_value != b_value {
125 updated.push((
126 IniParameter {
127 section: a_section.map(|x| x.to_string()),
128 property: key.to_string(),
129 value: a_value.unwrap_or_default().to_string(),
130 },
131 IniParameter {
132 section: a_section.map(|x| x.to_string()),
133 property: key.to_string(),
134 value: b_value.unwrap_or_default().to_string(),
135 },
136 ));
137 }
138 }
139
140 results.added.append(&mut added);
142 results.deleted.append(&mut removed);
143 results.updated.append(&mut updated);
144 }
145 }
146
147 Ok(results)
148}
149
150pub fn ini_update(
153 new_ini_file: &PathBuf,
154 comparison: &IniCompare,
155 protected_properties: &[&str],
156) -> Result<()> {
157 let mut new_config = Ini::load_from_file(new_ini_file)?;
159
160 for updates in comparison.updated.iter() {
163 if protected_properties
164 .iter()
165 .any(|&x| x == updates.0.property)
166 {
167 continue;
168 }
169
170 log_debug!(
171 "[ini][update] SECTION: [{:?}] PROPERTY: [{:?}] VALUE: [{:?}] => [{:?}]",
172 updates.0.section,
173 updates.0.property,
174 updates.0.value,
175 updates.1.value,
176 );
177
178 new_config.set_to(
179 updates.0.section.clone(),
180 updates.0.property.clone(),
181 updates.1.value.clone(),
182 );
183 }
184
185 for deletions in comparison.deleted.iter() {
188 if protected_properties
189 .iter()
190 .any(|&x| x == deletions.property)
191 {
192 continue;
193 }
194
195 log_debug!(
196 "[ini][update] SECTION: [{:?}] PROPERTY: [{:?}] VALUE: [{:?}] => [DELETED]",
197 deletions.section,
198 deletions.property,
199 deletions.value,
200 );
201
202 new_config.delete_from(deletions.section.clone(), &deletions.property);
203 }
204
205 for additions in comparison.added.iter() {
206 log_debug!(
207 "[ini][update] SECTION: [{:?}] PROPERTY: [{:?}] VALUE: [{:?}] => [ADDED]",
208 additions.section,
209 additions.property,
210 additions.value,
211 );
212
213 new_config.set_to(
214 additions.section.clone(),
215 additions.property.clone(),
216 additions.value.clone(),
217 );
218 }
219
220 new_config.write_to_file(new_ini_file)?;
221
222 Ok(())
223}
224
225#[cfg(test)]
226mod tests {
227
228 use super::*;
229 use indoc::indoc;
230
231 use std::io::Write;
232 use tempfile::NamedTempFile;
233
234 #[test]
235 fn compare_test() {
236 let mut a_ini = NamedTempFile::new().expect("Failed to create temp file!");
237
238 let config = indoc! {r#"
239 [version]
240 ; format: <major>.<minor>. Example 1.2
241 config_file_version = 10.0
242
243 ; A list of compatible FPGA bitstreams.
244 compatible_fpga = 2.0.0
245
246 ; Possible values: A1, B2
247 sys_variant = B2
248
249 [log]
250 ; This parameter sets the minimum log level that will be printed.
251 ; 0 = Trace
252 ; 1 = Debug
253 ; 2 = Info
254 ; 3 = Warning
255 ; 4 = Error
256 ; 5 = Fatal
257 log_level = 0
258 test_removed = 3
259
260 [board_control]
261 ref_clk_select = INT
262
263 ; This setting deactivates dynamic fan speed control.
264 always_apply_full_fan_speed = 0
265
266 "#};
267
268 writeln!(a_ini, "{}", config).expect("Failed to write to temp file!");
269
270 let mut b_ini = NamedTempFile::new().expect("Failed to create temp file!");
271
272 let config = indoc! {r#"
273 [version]
274 ; format: <major>.<minor>. Example 1.2
275 config_file_version = 10.1
276
277 ; A list of compatible FPGA bitstreams.
278 ; If the bitstream loaded to the board is not found in the list below, the firmware will not operate.
279 ; The version string should follow "X.Y.Z" format. All versions should be separated with a space character.
280 ; Typically, this field should not be modified in a file provided in the release
281 ; package. Note that entering a version that is not compatible might lead to firmware crash.
282 compatible_fpga = 2.0.0
283
284 ; Possible values: A1, B2
285 ; Basing on this parameter value the FW configures the Tx Board and selects how many and which PA channels are to be used.
286 ; The behavior when any other parameter is selected is undefined.
287 sys_variant = UNDEFINED
288
289 [log]
290 ; This parameter sets the minimum log level that will be printed.
291 ; 0 = Trace
292 ; 1 = Debug
293 ; 2 = Info
294 ; 3 = Warning
295 ; 4 = Error
296 ; 5 = Fatal
297 log_level = 2
298
299 [power_control]
300 test_added = YEAH
301
302 [board_control]
303 ref_clk_select = INT
304
305 ; This setting deactivates dynamic fan speed control.
306 always_apply_full_fan_speed = 0
307
308"#};
309
310 writeln!(b_ini, "{}", config).expect("Failed to write to temp file!");
311
312 let mut results = ini_compare(
313 &a_ini.into_temp_path().to_path_buf(),
314 &b_ini.into_temp_path().to_path_buf(),
315 )
316 .unwrap();
317
318 println!("{:#?}", results);
319
320 let mut expected = IniCompare {
321 updated: [
322 (
323 IniParameter {
324 section: Some("version".to_string()),
325 property: "config_file_version".to_string(),
326 value: "10.0".to_string(),
327 },
328 IniParameter {
329 section: Some("version".to_string()),
330 property: "config_file_version".to_string(),
331 value: "10.1".to_string(),
332 },
333 ),
334 (
335 IniParameter {
336 section: Some("version".to_string()),
337 property: "sys_variant".to_string(),
338 value: "B2".to_string(),
339 },
340 IniParameter {
341 section: Some("version".to_string()),
342 property: "sys_variant".to_string(),
343 value: "UNDEFINED".to_string(),
344 },
345 ),
346 (
347 IniParameter {
348 section: Some("log".to_string()),
349 property: "log_level".to_string(),
350 value: "0".to_string(),
351 },
352 IniParameter {
353 section: Some("log".to_string()),
354 property: "log_level".to_string(),
355 value: "2".to_string(),
356 },
357 ),
358 ]
359 .to_vec(),
360 added: [IniParameter {
361 section: Some("power_control".to_string()),
362 property: "test_added".to_string(),
363 value: "YEAH".to_string(),
364 }]
365 .to_vec(),
366 deleted: [IniParameter {
367 section: Some("log".to_string()),
368 property: "test_removed".to_string(),
369 value: "3".to_string(),
370 }]
371 .to_vec(),
372 };
373
374 expected.added.sort_unstable();
376 expected.deleted.sort_unstable();
377 expected.updated.sort_unstable();
378
379 results.added.sort_unstable();
381 results.deleted.sort_unstable();
382 results.updated.sort_unstable();
383
384 assert_eq!(expected, results);
385 }
386
387 #[test]
388 fn update_test() {
389 let mut a_ini = NamedTempFile::new().expect("Failed to create temp file!");
390 let mut updated_ini = NamedTempFile::new().expect("Failed to create temp file!");
391
392 let config = indoc! {r#"
393 [version]
394 ; format: <major>.<minor>. Example 1.2
395 config_file_version = 10.0
396
397 ; A list of compatible FPGA bitstreams.
398 compatible_fpga = 2.0.0
399
400 ; Possible values: A1, B2
401 sys_variant = B2
402
403 [log]
404 ; This parameter sets the minimum log level that will be printed.
405 ; 0 = Trace
406 ; 1 = Debug
407 ; 2 = Info
408 ; 3 = Warning
409 ; 4 = Error
410 ; 5 = Fatal
411 log_level = 0
412 test_removed = 3
413
414 [board_control]
415 ref_clk_select = INT
416
417 ; This setting deactivates dynamic fan speed control.
418 always_apply_full_fan_speed = 0
419
420 "#};
421
422 writeln!(a_ini, "{}", config).expect("Failed to write to temp file!");
423 writeln!(updated_ini, "{}", config).expect("Failed to write to temp file!");
424
425 let mut b_ini = NamedTempFile::new().expect("Failed to create temp file!");
426
427 let config = indoc! {r#"
428 [version]
429 ; format: <major>.<minor>. Example 1.2
430 config_file_version = 10.1
431
432 ; A list of compatible FPGA bitstreams.
433 ; If the bitstream loaded to the board is not found in the list below, the firmware will not operate.
434 ; The version string should follow "X.Y.Z" format. All versions should be separated with a space character.
435 ; Typically, this field should not be modified in a file provided in the release
436 ; package. Note that entering a version that is not compatible might lead to firmware crash.
437 compatible_fpga = 2.0.0
438
439 ; Possible values: A1, B2
440 ; Basing on this parameter value the FW configures the Tx Board and selects how many and which PA channels are to be used.
441 ; The behavior when any other parameter is selected is undefined.
442 sys_variant = UNDEFINED
443
444 [log]
445 ; This parameter sets the minimum log level that will be printed.
446 ; 0 = Trace
447 ; 1 = Debug
448 ; 2 = Info
449 ; 3 = Warning
450 ; 4 = Error
451 ; 5 = Fatal
452 log_level = 2
453
454 [power_control]
455 test_added = YEAH
456
457 [board_control]
458 ref_clk_select = INT
459
460 ; This setting deactivates dynamic fan speed control.
461 always_apply_full_fan_speed = 0
462
463"#};
464
465 writeln!(b_ini, "{}", config).expect("Failed to write to temp file!");
466
467 let results = ini_compare(
468 &a_ini.into_temp_path().to_path_buf(),
469 &b_ini.into_temp_path().to_path_buf(),
470 )
471 .unwrap();
472
473 let mut new_ini = NamedTempFile::new().expect("Failed to create temp file!");
474
475 let update_ini_path = updated_ini.into_temp_path().keep().unwrap();
476
477 ini_update(&update_ini_path, &results, &vec![]).unwrap();
478
479 let expected_config = indoc! {r#"
480 [version]
481 ; format: <major>.<minor>. Example 1.2
482 config_file_version = 10.1
483
484 ; A list of compatible FPGA bitstreams.
485 compatible_fpga = 2.0.0
486
487 ; Possible values: A1, B2
488 sys_variant = UNDEFINED
489
490 [log]
491 ; This parameter sets the minimum log level that will be printed.
492 ; 0 = Trace
493 ; 1 = Debug
494 ; 2 = Info
495 ; 3 = Warning
496 ; 4 = Error
497 ; 5 = Fatal
498 log_level = 2
499
500 [power_control]
501 test_added = YEAH
502
503 [board_control]
504 ref_clk_select = INT
505
506 ; This setting deactivates dynamic fan speed control.
507 always_apply_full_fan_speed = 0
508
509"#};
510
511 writeln!(new_ini, "{}", expected_config).expect("Failed to write to temp file!");
512
513 let results =
514 ini_compare(&update_ini_path, &new_ini.into_temp_path().to_path_buf()).unwrap();
515
516 assert!(results.added.is_empty());
517 assert!(results.deleted.is_empty());
518 assert!(results.updated.is_empty());
519 }
520}