[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(§ion_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