From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 1FBAFCE02 for ; Wed, 16 Aug 2023 16:49:40 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 192941820E for ; Wed, 16 Aug 2023 16:49:08 +0200 (CEST) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [94.136.29.106]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS for ; Wed, 16 Aug 2023 16:49:05 +0200 (CEST) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 2697B4100A for ; Wed, 16 Aug 2023 16:49:05 +0200 (CEST) From: Christoph Heiss To: pbs-devel@lists.proxmox.com Date: Wed, 16 Aug 2023 16:47:43 +0200 Message-ID: <20230816144746.1265108-14-c.heiss@proxmox.com> X-Mailer: git-send-email 2.41.0 In-Reply-To: <20230816144746.1265108-1-c.heiss@proxmox.com> References: <20230816144746.1265108-1-c.heiss@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.040 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pbs-devel] [RFC PATCH proxmox v2 13/15] section-config: add method to retrieve case-insensitive entries X-BeenThere: pbs-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Backup Server development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Wed, 16 Aug 2023 14:49:40 -0000 Add a new `SectionConfigData::lookup_icase()` method, to lookup sections which - after casefolding - might have the same names. Returned as a list, the caller has to take take responsibility how to handle such cases. To have the above a) with no impact on existing code-paths and b) still have reasonable runtime in the worse case, use a separate hashmap for the casefolded section ids, which only contains the names of the non-case-folded. Signed-off-by: Christoph Heiss --- Biggest concern might be the increased memory usage - approximate doubles the space needed for section names. Changes v1 -> v2: * New patch proxmox-section-config/Cargo.toml | 3 + proxmox-section-config/src/lib.rs | 115 +++++++++++++++++++++++++++++- 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/proxmox-section-config/Cargo.toml b/proxmox-section-config/Cargo.toml index 4da63f3..2c5b2b3 100644 --- a/proxmox-section-config/Cargo.toml +++ b/proxmox-section-config/Cargo.toml @@ -18,3 +18,6 @@ serde_json.workspace = true proxmox-schema.workspace = true # FIXME: remove! proxmox-lang.workspace = true + +[dev-dependencies] +serde = { workspace = true, features = [ "derive" ] } diff --git a/proxmox-section-config/src/lib.rs b/proxmox-section-config/src/lib.rs index 4441df1..96d824f 100644 --- a/proxmox-section-config/src/lib.rs +++ b/proxmox-section-config/src/lib.rs @@ -104,6 +104,7 @@ enum ParseState<'a> { #[derive(Debug, Clone)] pub struct SectionConfigData { pub sections: HashMap, + sections_lowercase: HashMap>, pub order: Vec, } @@ -118,6 +119,7 @@ impl SectionConfigData { pub fn new() -> Self { Self { sections: HashMap::new(), + sections_lowercase: HashMap::new(), order: Vec::new(), } } @@ -135,6 +137,15 @@ impl SectionConfigData { let json = serde_json::to_value(config)?; self.sections .insert(section_id.to_string(), (type_name.to_string(), json)); + + let section_id_lc = section_id.to_lowercase(); + if let Some(entry) = self.sections_lowercase.get_mut(§ion_id_lc) { + entry.push(section_id.to_owned()); + } else { + self.sections_lowercase + .insert(section_id_lc, vec![section_id.to_owned()]); + } + Ok(()) } @@ -158,13 +169,53 @@ impl SectionConfigData { } } - /// Lookup section data as native rust data type. + pub fn lookup_json_icase(&self, type_name: &str, id: &str) -> Result, Error> { + let sections = self + .sections_lowercase + .get(&id.to_lowercase()) + .ok_or_else(|| format_err!("no such {} '{}'", type_name, id))?; + + let mut result = Vec::with_capacity(sections.len()); + for section in sections { + let config = self.lookup_json(type_name, section)?; + result.push(config.clone()); + } + + Ok(result) + } + + /// Lookup section data as native rust data type, with the section id being compared + /// case-sensitive. pub fn lookup(&self, type_name: &str, id: &str) -> Result { let config = self.lookup_json(type_name, id)?; let data = T::deserialize(config)?; Ok(data) } + /// Lookup section data as native rust data type, with the section id being compared + /// case-insensitive. + pub fn lookup_icase( + &self, + type_name: &str, + id: &str, + ) -> Result, Error> { + let config = self.lookup_json_icase(type_name, id)?; + let data = config + .iter() + .fold(Ok(Vec::with_capacity(config.len())), |mut acc, c| { + if let Ok(acc) = &mut acc { + match T::deserialize(c) { + Ok(data) => acc.push(data), + Err(err) => return Err(err), + } + } + + acc + })?; + + Ok(data) + } + /// Record section ordering /// /// Sections are written in the recorder order. @@ -911,6 +962,68 @@ group: mygroup println!("CONFIG:\n{}", raw.unwrap()); } +#[test] +fn test_section_config_id_case_sensitivity() { + let filename = "user.cfg"; + + const ID_SCHEMA: Schema = StringSchema::new("default id schema.") + .min_length(3) + .schema(); + let mut config = SectionConfig::new(&ID_SCHEMA); + + #[derive(Debug, PartialEq, Eq, serde::Deserialize)] + struct User {} + + const USER_PROPERTIES: ObjectSchema = ObjectSchema::new( + "user properties", + &[( + "userid", + true, + &StringSchema::new("The id of the user (name@realm).") + .min_length(3) + .schema(), + )], + ); + + let plugin = SectionConfigPlugin::new( + "user".to_string(), + Some("userid".to_string()), + &USER_PROPERTIES, + ); + config.register_plugin(plugin); + + let raw = r" +user: root@pam + +user: ambiguous@pam + +user: AMBIguous@pam +"; + + let res = config.parse(filename, raw).unwrap(); + + assert_eq!( + res.lookup::("user", "root@pam") + .map_err(|err| format!("{err}")), + Ok(User {}) + ); + assert_eq!( + res.lookup::("user", "ambiguous@pam") + .map_err(|err| format!("{err}")), + Ok(User {}) + ); + assert_eq!( + res.lookup::("user", "AMBIguous@pam") + .map_err(|err| format!("{err}")), + Ok(User {}) + ); + assert_eq!( + res.lookup_icase::("user", "ambiguous@pam") + .map_err(|err| format!("{err}")), + Ok(vec![User {}, User {}]) + ); +} + #[test] fn test_section_config_with_all_of_schema() { let filename = "storage.cfg"; -- 2.41.0