[pbs-devel] [PATCH proxmox] close #4263: section-config: add comment support in section-config files

Gabriel Goller g.goller at proxmox.com
Fri Aug 25 12:55:49 CEST 2023


Added support for comments in section-config files. Comments start with
'#' and need to be over the whole line (f.e. no `attribute value # comment`).
We often parse, edit then rewrite the config to the file. In that case we
save the comments in a `HashMap` and associate them either to a section
or an attribute. When deleting sections, we also delete all the containing
comments, when deleting attributes, we don't delete any comments.

Signed-off-by: Gabriel Goller <g.goller at proxmox.com>
---
 proxmox-section-config/src/lib.rs | 259 +++++++++++++++++++++++++++++-
 1 file changed, 256 insertions(+), 3 deletions(-)

diff --git a/proxmox-section-config/src/lib.rs b/proxmox-section-config/src/lib.rs
index 4441df1..1e2378a 100644
--- a/proxmox-section-config/src/lib.rs
+++ b/proxmox-section-config/src/lib.rs
@@ -105,6 +105,7 @@ enum ParseState<'a> {
 pub struct SectionConfigData {
     pub sections: HashMap<String, (String, Value)>,
     pub order: Vec<String>,
+    pub comments: HashMap<String, Vec<String>>,
 }
 
 impl Default for SectionConfigData {
@@ -119,6 +120,7 @@ impl SectionConfigData {
         Self {
             sections: HashMap::new(),
             order: Vec::new(),
+            comments: HashMap::new(),
         }
     }
 
@@ -353,6 +355,11 @@ impl SectionConfig {
                         raw += "\n"
                     }
 
+                    if let Some(comments) = config.comments.get(section_id) {
+                        for c in comments {
+                            raw = format!("{}{}\n", raw, c);
+                        }
+                    }
                     raw += &(self.format_section_header)(type_name, section_id, section_config)?;
 
                     for (key, value) in section_config.as_object().unwrap() {
@@ -361,6 +368,13 @@ impl SectionConfig {
                                 continue; // skip writing out id properties, they are in the section header
                             }
                         }
+                        if let Some(comments) =
+                            config.comments.get(&format!("{}:{}", section_id, key))
+                        {
+                            for c in comments {
+                                raw = format!("{}{}\n", raw, c);
+                            }
+                        }
                         raw += &(self.format_section_content)(type_name, section_id, key, value)?;
                     }
                 }
@@ -373,9 +387,21 @@ impl SectionConfig {
                         raw += "\n"
                     }
 
+                    if let Some(comments) = config.comments.get(section_id) {
+                        for c in comments {
+                            raw = format!("{}{}\n", raw, c);
+                        }
+                    }
                     raw += &(self.format_section_header)(type_name, section_id, section_config)?;
 
                     for (key, value) in section_config.as_object().unwrap() {
+                        if let Some(comments) =
+                            config.comments.get(&format!("{}:{}", section_id, key))
+                        {
+                            for c in comments {
+                                raw = format!("{}{}\n", raw, c);
+                            }
+                        }
                         raw += &(self.format_section_content)(type_name, section_id, key, value)?;
                     }
                 }
@@ -383,6 +409,25 @@ impl SectionConfig {
                     bail!("unknown section type '{type_name}'");
                 }
             }
+            // Insert the comments with property not found
+            let keys: Vec<&String> = section_config
+                .as_object()
+                .unwrap()
+                .iter()
+                .map(|c| c.0)
+                .collect();
+
+            for c in &config.comments {
+                let split: Vec<&str> = c.0.split(':').collect();
+                if split[0] == section_id
+                    && split.len() > 1
+                    && !keys.contains(&&split[1].to_string())
+                {
+                    for comment in c.1 {
+                        raw = format!("{}{}\n", raw, comment);
+                    }
+                }
+            }
         }
 
         Ok(raw)
@@ -421,6 +466,7 @@ impl SectionConfig {
 
         try_block!({
             let mut result = SectionConfigData::new();
+            let mut dangling_comments: Vec<String> = Vec::new();
 
             try_block!({
                 for line in raw.lines() {
@@ -428,6 +474,10 @@ impl SectionConfig {
 
                     match state {
                         ParseState::BeforeHeader => {
+                            if line.trim().starts_with('#') {
+                                dangling_comments.push(line.to_string());
+                                continue;
+                            }
                             if line.trim().is_empty() {
                                 continue;
                             }
@@ -436,6 +486,13 @@ impl SectionConfig {
                                 (self.parse_section_header)(line)
                             {
                                 //println!("OKLINE: type: {} ID: {}", section_type, section_id);
+                                if !dangling_comments.is_empty() {
+                                    result.comments.insert(
+                                        section_id.clone().to_string(),
+                                        dangling_comments.clone(),
+                                    );
+                                    dangling_comments.clear();
+                                }
                                 if let Some(plugin) = self.plugins.get(&section_type) {
                                     let id_schema =
                                         plugin.get_id_schema().unwrap_or(self.id_schema);
@@ -461,8 +518,28 @@ impl SectionConfig {
                             }
                         }
                         ParseState::InsideSection(plugin, ref mut section_id, ref mut config) => {
+                            if line.trim().starts_with('#') {
+                                dangling_comments.push(line.to_string());
+                                continue;
+                            }
                             if line.trim().is_empty() {
                                 // finish section
+                                match result.comments.get(section_id) {
+                                    Some(e) => {
+                                        let mut comments = e.clone();
+                                        comments.append(&mut dangling_comments);
+                                        result.comments.insert(section_id.to_string(), comments);
+                                    }
+                                    None if !dangling_comments.is_empty() => {
+                                        result.comments.insert(
+                                            section_id.to_string(),
+                                            dangling_comments.clone(),
+                                        );
+                                    }
+                                    _ => (),
+                                };
+                                dangling_comments.clear();
+
                                 test_required_properties(
                                     config,
                                     plugin.properties,
@@ -479,6 +556,13 @@ impl SectionConfig {
                             }
                             if let Some((key, value)) = (self.parse_section_content)(line) {
                                 //println!("CONTENT: key: {} value: {}", key, value);
+                                if !dangling_comments.is_empty() {
+                                    result.comments.insert(
+                                        format!("{}:{}", section_id, key),
+                                        dangling_comments.clone(),
+                                    );
+                                    dangling_comments.clear();
+                                }
 
                                 let schema = plugin.properties.lookup(&key);
                                 let (is_array, prop_schema) = match schema {
@@ -522,6 +606,21 @@ impl SectionConfig {
                         ) => {
                             if line.trim().is_empty() {
                                 // finish section
+                                match result.comments.get(section_id) {
+                                    Some(e) => {
+                                        let mut comments = e.clone();
+                                        comments.append(&mut dangling_comments);
+                                        result.comments.insert(section_id.to_string(), comments);
+                                    }
+                                    None if !dangling_comments.is_empty() => {
+                                        result.comments.insert(
+                                            section_id.to_string(),
+                                            dangling_comments.clone(),
+                                        );
+                                    }
+                                    _ => (),
+                                };
+                                dangling_comments.clear();
                                 result.set_data(section_id, section_type, config.take())?;
                                 result.record_order(section_id);
 
@@ -529,6 +628,13 @@ impl SectionConfig {
                                 continue;
                             }
                             if let Some((key, value)) = (self.parse_section_content)(line) {
+                                if !dangling_comments.is_empty() {
+                                    result.comments.insert(
+                                        format!("{}:{}", section_id, key),
+                                        dangling_comments.clone(),
+                                    );
+                                    dangling_comments.clear();
+                                }
                                 match &mut config[&key] {
                                     Value::Null => config[key] = json!(value),
                                     // Assume it's an array schema in order to handle actual array
@@ -836,8 +942,8 @@ lvmthin: local-lvm2
 
     let res = config.parse(filename, raw);
     println!("RES: {:?}", res);
-    let raw = config.write(filename, &res.unwrap());
-    println!("CONFIG:\n{}", raw.unwrap());
+    let raw_encoded = config.write(filename, &res.unwrap()).unwrap();
+    println!("CONFIG:\n{}", raw_encoded);
 }
 
 // cargo test test_section_config2 -- --nocapture
@@ -897,9 +1003,11 @@ fn test_section_config2() {
     config.register_plugin(plugin);
 
     let raw = r"
-
+# this is a comment
 user: root at pam
+        # this is also a comment
         email root at example.com
+        #email comment
 
 group: mygroup
         comment a very important group
@@ -1248,3 +1356,148 @@ pub fn dump_section_config(config: &SectionConfig) -> String {
 
     res
 }
+
+#[test]
+fn test_comments_error() {
+    const PROPERTIES: ObjectSchema = ObjectSchema::new(
+        "lvmthin properties",
+        &[(
+            "content",
+            true,
+            &StringSchema::new("Storage content types.").schema(),
+        )],
+    );
+
+    let plugin = SectionConfigPlugin::new("lvmthin".to_string(), None, &PROPERTIES);
+
+    const ID_SCHEMA: Schema = StringSchema::new("Storage ID schema.")
+        .min_length(3)
+        .schema();
+    let mut config = SectionConfig::new(&ID_SCHEMA);
+    config.register_plugin(plugin);
+
+    let raw = r"
+
+lvmthin: local-lvm
+        content rootdir,im#ages
+
+lvmthin: local-lvm2
+#lvmthin: local-lvm2
+        #comment
+        content r#ootdir,images
+";
+
+    let res = config.parse("test.cfg", raw).unwrap();
+    println!("RES: {:?}", res);
+
+    // we only allow full-line comments, so check if these don't change the behavior
+    assert_eq!(
+        res.sections
+            .get("local-lvm")
+            .unwrap()
+            .1
+            .get("content")
+            .unwrap(),
+        &serde_json::Value::String("rootdir,im#ages".to_string())
+    );
+    assert_eq!(
+        res.sections
+            .get("local-lvm2")
+            .unwrap()
+            .1
+            .get("content")
+            .unwrap(),
+        &serde_json::Value::String("r#ootdir,images".to_string())
+    );
+
+    let raw_encoded = config.write("test.cfg", &res).unwrap();
+    println!("CONFIG:\n{}", raw_encoded);
+}
+
+#[test]
+fn test_comments_delete_section() {
+    const PROPERTIES: ObjectSchema = ObjectSchema::new(
+        "lvmthin properties",
+        &[
+            (
+                "content",
+                true,
+                &StringSchema::new("Storage content types.").schema(),
+            ),
+            (
+                "test",
+                true,
+                &StringSchema::new("Storage content types.").schema(),
+            ),
+        ],
+    );
+
+    let plugin = SectionConfigPlugin::new("lvmthin".to_string(), None, &PROPERTIES);
+
+    const ID_SCHEMA: Schema = StringSchema::new("Storage ID schema.")
+        .min_length(3)
+        .schema();
+    let mut config = SectionConfig::new(&ID_SCHEMA);
+    config.register_plugin(plugin);
+
+    let raw = r"
+
+# cool local-lvm 
+lvmthin: local-lvm
+        content rootdir,im#ages
+        test blah
+        # cool, but not in section
+# cool, but not in indented
+
+# lvmthin: local-lvm
+        # content rootdir,im#ages
+        # test blah
+
+lvmthin: local-lvm2
+#lvmthin: local-lvm2
+        #comment
+        content r#ootdir,images
+        # another one
+        test blah
+";
+
+    let mut res = config.parse("test.cfg", raw).unwrap();
+    println!("RES: {:?}", res);
+
+    // we only allow full-line comments, so check if these don't change the behavior
+    assert_eq!(
+        res.sections
+            .get("local-lvm")
+            .unwrap()
+            .1
+            .get("content")
+            .unwrap(),
+        &serde_json::Value::String("rootdir,im#ages".to_string())
+    );
+    assert_eq!(
+        res.sections
+            .get("local-lvm2")
+            .unwrap()
+            .1
+            .get("content")
+            .unwrap(),
+        &serde_json::Value::String("r#ootdir,images".to_string())
+    );
+
+    res.sections.remove("local-lvm");
+
+    // removing a single attribute
+    let mut new_section = res.sections.get("local-lvm2").unwrap().1.clone();
+    new_section.as_object_mut().unwrap().remove("test");
+    res.sections.insert(
+        "local-lvm2".to_string(),
+        ("lvmthin".to_string(), new_section),
+    );
+
+    let raw_encoded = config.write("test.cfg", &res).unwrap();
+    println!("CONFIG:\n{}", raw_encoded);
+    assert_eq!(raw_encoded.contains("# cool local-lvm"), false);
+    assert_eq!(raw_encoded.contains("# cool, but not in section"), false);
+    assert_eq!(raw_encoded.contains("# cool, but not in indented"), false);
+    assert_eq!(raw_encoded.contains("# another one"), true);
+}
-- 
2.39.2






More information about the pbs-devel mailing list