utils_box/
config.rs

1//! # Configuration utilities
2//! A toolbox of small utilities to retrieve,write and compare INI-style configuration files.
3//! Useful for initial configuration of binaries and to perform updates in existing installations.
4
5use 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
31/// Compare two ini files and return the differences against the first one
32pub fn ini_compare(a: &PathBuf, b: &PathBuf) -> Result<IniCompare> {
33    // Open File A and Extract Information
34    let a_ini = Ini::load_from_file(a)?;
35
36    // Open File B and Extract Information
37    let b_ini = Ini::load_from_file(b)?;
38
39    // Initialize empty results
40    let mut results = IniCompare::new();
41
42    // Find removed and added sections
43    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    // Perform comparison section-by-section
80    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            // Get additions
87            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            // Get removals
101            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            // Get keys that remained
115            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            // Push current section results into global results
141            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
150/// Update file using the comparison results
151/// Do not modify protected properties
152pub fn ini_update(
153    new_ini_file: &PathBuf,
154    comparison: &IniCompare,
155    protected_properties: &[&str],
156) -> Result<()> {
157    // Open File A and Extract Information
158    let mut new_config = Ini::load_from_file(new_ini_file)?;
159
160    // Protected properties will keep the old values
161    // EVERYTHING ELSE will be replaced with the new value
162    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    // Protected properties will keep the old values
186    // EVERYTHING ELSE will be deleted
187    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        // Sort the results to avoid issues with vector equality checks
375        expected.added.sort_unstable();
376        expected.deleted.sort_unstable();
377        expected.updated.sort_unstable();
378
379        // Sort the results to avoid issues with vector equality checks
380        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}