[pbs-devel] [PATCH proxmox-backup 3/5] fix #3067: api: add multi-line comments to node.cfg

Wolfgang Bumiller w.bumiller at proxmox.com
Wed Feb 23 11:28:35 CET 2022


some comments inline

On Tue, Feb 22, 2022 at 12:25:54PM +0100, Stefan Sterz wrote:
> add support for multiline comments to node.cfg, similar to how pve
> handles multi-line comments
> 
> Signed-off-by: Stefan Sterz <s.sterz at proxmox.com>
> ---
>  pbs-api-types/src/lib.rs |  9 ++++++
>  src/config/node.rs       |  4 +--
>  src/tools/config.rs      | 62 ++++++++++++++++++++++++++++++++++++++++
>  3 files changed, 73 insertions(+), 2 deletions(-)
> 
> diff --git a/pbs-api-types/src/lib.rs b/pbs-api-types/src/lib.rs
> index 754e7b22..3892980d 100644
> --- a/pbs-api-types/src/lib.rs
> +++ b/pbs-api-types/src/lib.rs
> @@ -137,6 +137,8 @@ const_regex! {
>  
>      pub SINGLE_LINE_COMMENT_REGEX = r"^[[:^cntrl:]]*$";
>  
> +    pub MULTI_LINE_COMMENT_REGEX = r"(?m)^([[:^cntrl:]]*)\s*$";

I don't think the trailing `\s*` is necessary?

> +
>      pub BACKUP_REPO_URL_REGEX = concat!(
>          r"^^(?:(?:(",
>          USER_ID_REGEX_STR!(), "|", APITOKEN_ID_REGEX_STR!(),
> (...)
> diff --git a/src/config/node.rs b/src/config/node.rs
> index 9ca44a52..bb915f94 100644
> --- a/src/config/node.rs
> +++ b/src/config/node.rs
> @@ -9,7 +9,7 @@ use proxmox_schema::{api, ApiStringFormat, ApiType, Updater};
>  use proxmox_http::ProxyConfig;
>  
>  use pbs_api_types::{EMAIL_SCHEMA, OPENSSL_CIPHERS_TLS_1_2_SCHEMA, OPENSSL_CIPHERS_TLS_1_3_SCHEMA,
> -    SINGLE_LINE_COMMENT_SCHEMA};
> +    MULTI_LINE_COMMENT_SCHEMA};

Please check how rustfmt would deal with the above `use` statement ;-)

>  use pbs_buildcfg::configdir;
>  use pbs_config::{open_backup_lockfile, BackupLockGuard};
>  
> (...)
> diff --git a/src/tools/config.rs b/src/tools/config.rs
> index f666a8ab..738ab541 100644
> --- a/src/tools/config.rs
> +++ b/src/tools/config.rs
> @@ -32,6 +32,20 @@ pub fn value_from_str(input: &str, schema: &'static Schema) -> Result<Value, Err
>  
>      let mut config = Object::new();
>  
> +    // parse first n lines starting with '#' as multi-line comment
> +    let comment = input.lines()
> +        .take_while(|l| l.starts_with('#'))
> +        .map(|l| {
> +            let mut ch = l.chars();
> +            ch.next();
> +            ch.as_str()

^ The `take_while` ensures `l` starts with '#', so you could just use 

    .map(|l| &l[1..])

Alternatively, since 1.57 (and we're at 1.58 now), you could also
combine the `.take_while` and `.map` into:

    .map_while(|l| l.strip_prefix("#"))

However...

> +        })
> +        .fold(String::new(), |acc, l| acc + l + "\n");
> +
> +    if !comment.is_empty() {
> +        config.insert("comment".to_string(), Value::String(comment));
> +    }
> +
>      for (lineno, line) in input.lines().enumerate() {

... here we're starting over, so maybe we should refactor this a little.
Eg. we could use a `.lines().enumerate().peekable()` iterator:

    let mut lines = input.lines().enumerate().peekable();
    let mut comments = String::new();
    while let Some((_, line)) = iter.next_if(|(_, line)| line.starts_with('#') {
        comments.push_str(&line[1..]);
        comments.push('\n');
    }

    for (lineno, line) in lines {
    <...>

>          let line = line.trim();
>          if line.starts_with('#') || line.is_empty() {
> @@ -133,10 +147,23 @@ pub fn value_to_bytes(value: &Value, schema: &'static Schema) -> Result<Vec<u8>,
>  
>  /// Note: the object must have already been verified at this point.
>  fn object_to_writer(output: &mut dyn Write, object: &Object) -> Result<(), Error> {
> +    // special key `comment` for multi-line notes
> +    if object.contains_key("comment") {
> +        let comment = match object.get("comment") {

`contains_key` + `get` is somewhat wasteful and should be combined.

    if let Some(comment) = object.get("comment") {

For the type check you can then use `.as_str().ok_or_else(...)`

Or alternatively use a single match for both checks:

    match object.get("comment") {
        Some(Value::String(comment)) => {
            <the loop>
        }
        Some(_) => bail!(...),
        None => (),
    }

> +            Some(Value::String(v)) => v,
> +            _ => bail!("only strings can be comments"),
> +        };
> +
> +        for lines in comment.lines() {
> +            writeln!(output, "#{}", lines)?;
> +        }
> +    }
> +
>      for (key, value) in object.iter() {

Given that we type-check the comment above _and_ the data matching the
schema is a precondition for calling this function, I'd just put an

    if key == "comment" { continue }

here rather than the conditional check limited to the `Value::String` case below.

>          match value {
>              Value::Null => continue, // delete this entry
>              Value::Bool(v) => writeln!(output, "{}: {}", key, v)?,
> +            Value::String(_) if key == "comment" => continue, // skip comment as we handle it above
>              Value::String(v) => {
>                  if v.as_bytes().contains(&b'\n') {
>                      bail!("value for {} contains newlines", key);
> @@ -172,3 +199,38 @@ fn test() {
>  
>      assert_eq!(config, NODE_CONFIG.as_bytes());
>  }
> +
> +#[test]
> +fn test_with_comment() {
> +    use proxmox_schema::ApiType;
> +
> +    // let's just reuse some schema we actually have available:
> +    use crate::config::node::NodeConfig;
> +
> +    const NODE_INPUT: &str = "\
> +        #this should\n\
> +        #be included\n\
> +        acme: account=pebble\n\
> +        # this should not\n\

^ I find it curous that your 'comment' section doesn't have leading
spaces, while the "real comment" does ;-)

Should we think about trimming off up to 1 space when parsing the lines
and prefix them with `"# "` when printing them out?
Though I suppose it doesn't matter much as since we drop "real" comments
on a RMW-cycle these files don't really need to bother much with being
all that eye-pleasing I guess...





More information about the pbs-devel mailing list