diff --git a/Cargo.toml b/Cargo.toml index 45b3adf6..9ee8da7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ members = [ "proxmox-section-config", "proxmox-serde", "proxmox-shared-memory", + "proxmox-simple-config", "proxmox-sortable-macro", "proxmox-subscription", "proxmox-sys", diff --git a/proxmox-simple-config/Cargo.toml b/proxmox-simple-config/Cargo.toml new file mode 100644 index 00000000..d957edd0 --- /dev/null +++ b/proxmox-simple-config/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "proxmox-simple-config" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +exclude.workspace = true +description = "Simple key/value format for configuration files." + +[dependencies] +anyhow.workspace = true +serde_json.workspace = true +serde = { workspace = true, features = ["derive"] } +log = { workspace = true, optional = true } + +proxmox-schema = { workspace = true, features = ["api-macro", "api-types"] } diff --git a/proxmox-simple-config/debian/changelog b/proxmox-simple-config/debian/changelog new file mode 100644 index 00000000..cfea6b7a --- /dev/null +++ b/proxmox-simple-config/debian/changelog @@ -0,0 +1,6 @@ +rust-proxmox-simple-config (0.1.0-1) bookworm; urgency=medium + + * initial packaging (copied code from proxmox-backup) + + -- Proxmox Support Team Wed, 05 Jun 2024 12:52:06 +0200 + diff --git a/proxmox-simple-config/debian/control b/proxmox-simple-config/debian/control new file mode 100644 index 00000000..bb4a23da --- /dev/null +++ b/proxmox-simple-config/debian/control @@ -0,0 +1,61 @@ +Source: rust-proxmox-simple-config +Section: rust +Priority: optional +Build-Depends: debhelper (>= 12), + dh-cargo (>= 25), + cargo:native , + rustc:native , + libstd-rust-dev , + librust-anyhow-1+default-dev , + librust-proxmox-schema-3+api-macro-dev (>= 3.1.1-~~) , + librust-proxmox-schema-3+api-types-dev (>= 3.1.1-~~) , + librust-proxmox-schema-3+default-dev (>= 3.1.1-~~) , + librust-serde-1+default-dev , + librust-serde-1+derive-dev , + librust-serde-json-1+default-dev +Maintainer: Proxmox Support Team +Standards-Version: 4.6.2 +Vcs-Git: git://git.proxmox.com/git/proxmox.git +Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +X-Cargo-Crate: proxmox-simple-config +Rules-Requires-Root: no + +Package: librust-proxmox-simple-config-dev +Architecture: any +Multi-Arch: same +Depends: + ${misc:Depends}, + librust-anyhow-1+default-dev, + librust-proxmox-schema-3+api-macro-dev (>= 3.1.1-~~), + librust-proxmox-schema-3+api-types-dev (>= 3.1.1-~~), + librust-proxmox-schema-3+default-dev (>= 3.1.1-~~), + librust-serde-1+default-dev, + librust-serde-1+derive-dev, + librust-serde-json-1+default-dev +Suggests: + librust-proxmox-simple-config+log-dev (= ${binary:Version}) +Provides: + librust-proxmox-simple-config+default-dev (= ${binary:Version}), + librust-proxmox-simple-config-0-dev (= ${binary:Version}), + librust-proxmox-simple-config-0+default-dev (= ${binary:Version}), + librust-proxmox-simple-config-0.1-dev (= ${binary:Version}), + librust-proxmox-simple-config-0.1+default-dev (= ${binary:Version}), + librust-proxmox-simple-config-0.1.0-dev (= ${binary:Version}), + librust-proxmox-simple-config-0.1.0+default-dev (= ${binary:Version}) +Description: Simple key/value format for configuration files - Rust source code + Source code for Debianized Rust crate "proxmox-simple-config" + +Package: librust-proxmox-simple-config+log-dev +Architecture: any +Multi-Arch: same +Depends: + ${misc:Depends}, + librust-proxmox-simple-config-dev (= ${binary:Version}), + librust-log-0.4+default-dev (>= 0.4.17-~~) +Provides: + librust-proxmox-simple-config-0+log-dev (= ${binary:Version}), + librust-proxmox-simple-config-0.1+log-dev (= ${binary:Version}), + librust-proxmox-simple-config-0.1.0+log-dev (= ${binary:Version}) +Description: Simple key/value format for configuration files - feature "log" + This metapackage enables feature "log" for the Rust proxmox-simple-config + crate, by pulling in any additional dependencies needed by that feature. diff --git a/proxmox-simple-config/debian/copyright b/proxmox-simple-config/debian/copyright new file mode 100644 index 00000000..b227c290 --- /dev/null +++ b/proxmox-simple-config/debian/copyright @@ -0,0 +1,18 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ + +Files: + * +Copyright: 2019 - 2024 Proxmox Server Solutions GmbH +License: AGPL-3.0-or-later + This program is free software: you can redistribute it and/or modify it under + the terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or (at your option) any + later version. + . + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + details. + . + You should have received a copy of the GNU Affero General Public License along + with this program. If not, see . diff --git a/proxmox-simple-config/debian/debcargo.toml b/proxmox-simple-config/debian/debcargo.toml new file mode 100644 index 00000000..b7864cdb --- /dev/null +++ b/proxmox-simple-config/debian/debcargo.toml @@ -0,0 +1,7 @@ +overlay = "." +crate_src_path = ".." +maintainer = "Proxmox Support Team " + +[source] +vcs_git = "git://git.proxmox.com/git/proxmox.git" +vcs_browser = "https://git.proxmox.com/?p=proxmox.git" diff --git a/proxmox-simple-config/src/lib.rs b/proxmox-simple-config/src/lib.rs new file mode 100644 index 00000000..f3b1782f --- /dev/null +++ b/proxmox-simple-config/src/lib.rs @@ -0,0 +1,173 @@ +//! Our 'key: value' config format. + +use std::io::Write; + +use anyhow::{bail, format_err, Error}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use proxmox_schema::{ObjectSchemaType, Schema}; + +type Object = serde_json::Map; + +fn object_schema(schema: &'static Schema) -> Result<&'static dyn ObjectSchemaType, Error> { + Ok(match schema { + Schema::Object(schema) => schema, + Schema::AllOf(schema) => schema, + _ => bail!("invalid schema for config, must be an object schema"), + }) +} + +/// Parse a full string representing a config file. +pub fn from_str Deserialize<'de>>( + input: &str, + schema: &'static Schema, +) -> Result { + Ok(serde_json::from_value(value_from_str(input, schema)?)?) +} + +/// Parse a full string representing a config file. +pub fn value_from_str(input: &str, schema: &'static Schema) -> Result { + let schema = object_schema(schema)?; + + let mut config = Object::new(); + let mut lines = input.lines().enumerate().peekable(); + let mut description = String::new(); + + while let Some((_, line)) = lines.next_if(|(_, line)| line.starts_with('#')) { + description.push_str(&line[1..]); + description.push('\n'); + } + + if !description.is_empty() { + config.insert("description".to_string(), Value::String(description)); + } + + for (lineno, line) in lines { + let line = line.trim(); + if line.starts_with('#') || line.is_empty() { + continue; + } + + parse_line(&mut config, line, schema) + .map_err(|err| format_err!("line {}: {}", lineno, err))?; + } + + Ok(Value::Object(config)) +} + +/// Parse a single `key: value` line from a config file. +fn parse_line( + config: &mut Object, + line: &str, + schema: &'static dyn ObjectSchemaType, +) -> Result<(), Error> { + if line.starts_with('#') || line.is_empty() { + return Ok(()); + } + + let colon = line + .find(':') + .ok_or_else(|| format_err!("missing colon to separate key from value"))?; + if colon == 0 { + bail!("empty key not allowed"); + } + + let key = &line[..colon]; + let value = line[(colon + 1)..].trim_start(); + + parse_key_value(config, key, value, schema) +} + +/// Lookup the key in the schema, parse the value and insert it into the config object. +fn parse_key_value( + config: &mut Object, + key: &str, + value: &str, + schema: &'static dyn ObjectSchemaType, +) -> Result<(), Error> { + let schema = match schema.lookup(key) { + Some((_optional, schema)) => Some(schema), + None if schema.additional_properties() => None, + None => bail!( + "invalid key '{}' and schema does not allow additional properties", + key + ), + }; + + let value = parse_value(value, schema)?; + config.insert(key.to_owned(), value); + Ok(()) +} + +/// For this we can just reuse the schema's "parse_simple_value". +/// +/// "Additional" properties (`None` schema) will simply become strings. +/// +/// Note that this does not handle Object or Array types at all, so if we want to support them +/// natively without going over a `String` type, we can add this here. +fn parse_value(value: &str, schema: Option<&'static Schema>) -> Result { + match schema { + None => Ok(Value::String(value.to_owned())), + Some(schema) => schema.parse_simple_value(value), + } +} + +/// Parse a string as a property string into a deserializable type. This is just a short wrapper +/// around deserializing the s +pub fn from_property_string(input: &str, schema: &'static Schema) -> Result +where + T: for<'de> Deserialize<'de>, +{ + Ok(serde_json::from_value( + schema.parse_property_string(input)?, + )?) +} + +/// Serialize a data structure using a 'key: value' config file format. +pub fn to_bytes(value: &T, schema: &'static Schema) -> Result, Error> { + value_to_bytes(&serde_json::to_value(value)?, schema) +} + +/// Serialize a json value using a 'key: value' config file format. +pub fn value_to_bytes(value: &Value, schema: &'static Schema) -> Result, Error> { + let schema = object_schema(schema)?; + + schema.verify_json(value)?; + + let object = value + .as_object() + .ok_or_else(|| format_err!("value must be an object"))?; + + let mut out = Vec::new(); + object_to_writer(&mut out, object)?; + Ok(out) +} + +/// 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 `description` for multi-line notes, must be written before everything else + if let Some(Value::String(description)) = object.get("description") { + for lines in description.lines() { + writeln!(output, "#{}", lines)?; + } + } + + for (key, value) in object.iter() { + match value { + _ if key == "description" => continue, // skip description as we handle it above + Value::Null => continue, // delete this entry + Value::Bool(v) => writeln!(output, "{}: {}", key, v)?, + Value::String(v) => { + if v.as_bytes().contains(&b'\n') { + bail!("value for {} contains newlines", key); + } + writeln!(output, "{}: {}", key, v)? + } + Value::Number(v) => writeln!(output, "{}: {}", key, v)?, + Value::Array(_) => bail!("arrays are not supported in config files"), + Value::Object(_) => bail!("complex objects are not supported in config files"), + } + } + Ok(()) +}