diff --git a/src/languages/azure_policy/aliases/normalizer/alias_resolution.rs b/src/languages/azure_policy/aliases/normalizer/alias_resolution.rs index 202145b0..d491f5f5 100644 --- a/src/languages/azure_policy/aliases/normalizer/alias_resolution.rs +++ b/src/languages/azure_policy/aliases/normalizer/alias_resolution.rs @@ -4,14 +4,13 @@ //! Per-alias path resolution: reads values from versioned ARM paths and places //! them at alias short name paths in the normalized output. -use alloc::string::String; - +use crate::Rc; use crate::Value; use super::super::obj_map::remove_element_field; use super::super::obj_map::{ - collision_safe_key, is_root_field_collision, obj_contains, obj_insert, obj_remove, - set_nested_lowercased, ObjMap, + collision_safe_key, is_root_field_collision, obj_contains, obj_insert, obj_insert_rc, + obj_remove, set_nested_lowercased, ObjMap, }; use super::super::types::ResolvedAliases; use super::element_remap::apply_element_remap_precomputed; @@ -48,12 +47,14 @@ pub fn apply_alias_entries( if let Some(value) = value { let value = normalize_value(&value, &entry.short_name, None); - let target = if is_root_field_collision(&entry.short_name, &entry.default_path) { - collision_safe_key(&entry.short_name) + if is_root_field_collision(&entry.short_name, &entry.default_path) { + let target = collision_safe_key(&entry.short_name); + set_nested_lowercased(result, &target, value); + } else if entry.short_name.contains('.') { + set_nested_lowercased(result, &entry.short_name, value); } else { - entry.short_name.clone() - }; - set_nested_lowercased(result, &target, value); + obj_insert_rc(result, Rc::clone(&entry.short_name_lc), value); + } } } @@ -84,13 +85,13 @@ pub fn apply_alias_entries( } /// Navigate an ARM path using precomputed segments (avoids per-call split). -fn navigate_arm_path_segments(value: &Value, segments: &[String]) -> Option { +fn navigate_arm_path_segments(value: &Value, segments: &[Rc]) -> Option { let mut current = value; for segment in segments { current = current .as_object() .ok()? - .get(&Value::from(segment.as_str()))?; + .get(&Value::String(Rc::clone(segment)))?; } Some(current.clone()) } diff --git a/src/languages/azure_policy/aliases/obj_map.rs b/src/languages/azure_policy/aliases/obj_map.rs index 8e8d2bcf..65c5bb06 100644 --- a/src/languages/azure_policy/aliases/obj_map.rs +++ b/src/languages/azure_policy/aliases/obj_map.rs @@ -7,7 +7,7 @@ //! then converts to `Value::Object` (a `BTreeMap`) only at //! the output boundary via [`make_value`]. -use alloc::string::{String, ToString as _}; +use alloc::string::String; use alloc::vec::Vec; use hashbrown::HashMap; @@ -41,6 +41,33 @@ pub fn obj_insert(map: &mut ObjMap, key: &str, val: Value) { map.insert(Rc::from(key), val); } +/// Insert a key-value pair using a pre-allocated `Rc` key. +/// +/// Avoids the `Rc::from(key)` heap allocation that [`obj_insert`] performs. +pub fn obj_insert_rc(map: &mut ObjMap, key: Rc, val: Value) { + map.insert(key, val); +} + +/// Lowercase a string, returning an `Rc`. +/// +/// Both paths allocate an `Rc` (header + string bytes). The fast-path +/// avoids creating an intermediate lowercased `String` when the input is +/// already all-lowercase ASCII. +pub fn rc_lowercase(s: &str) -> Rc { + if s.bytes().all(|b| !b.is_ascii_uppercase()) { + Rc::from(s) + } else { + Rc::from(s.to_ascii_lowercase()) + } +} + +/// Insert a key-value pair with the key lowercased, using [`rc_lowercase`] +/// for the allocation fast-path. +pub fn obj_insert_lc(map: &mut ObjMap, key: &str, val: Value) { + let lc = rc_lowercase(key); + map.insert(lc, val); +} + /// Check whether a key exists. pub fn obj_contains(map: &ObjMap, key: &str) -> bool { map.contains_key(key) @@ -112,7 +139,7 @@ pub fn set_nested_lowercased(result: &mut ObjMap, path: &str, value: Value) { } if segments.len() == 1 { if let Some(&seg) = segments.first() { - obj_insert(result, &seg.to_ascii_lowercase(), value); + obj_insert_lc(result, seg, value); } return; } @@ -144,28 +171,28 @@ fn set_nested_inner(obj: &mut ObjMap, segments: &[&str], value: Value, lowercase }; if segments.len() == 1 { - let key = if lowercase { - first.to_ascii_lowercase() + let key: Rc = if lowercase { + rc_lowercase(first) } else { - first.to_string() + Rc::from(first) }; - obj_insert(obj, &key, value); + obj_insert_rc(obj, key, value); return; } - let seg = if lowercase { - first.to_ascii_lowercase() + let seg: Rc = if lowercase { + rc_lowercase(first) } else { - first.to_string() + Rc::from(first) }; // Ensure an intermediate object exists at `seg`. - if !obj_contains(obj, &seg) { - obj_insert(obj, &seg, make_value(new_map())); + if !obj.contains_key(&*seg) { + obj_insert_rc(obj, Rc::clone(&seg), make_value(new_map())); } // Descend directly into the BTreeMap, avoiding ObjMap round-trip. - if let Some(Value::Object(inner_rc)) = obj_get_mut(obj, &seg) { + if let Some(Value::Object(inner_rc)) = obj.get_mut(&*seg) { let inner_btree = Rc::make_mut(inner_rc); set_nested_in_btree( inner_btree, @@ -191,12 +218,12 @@ pub fn set_nested_in_btree( return; }; - let key_str: String = if lowercase { - first.to_ascii_lowercase() + let key_rc: Rc = if lowercase { + rc_lowercase(first) } else { - first.to_string() + Rc::from(first) }; - let key_val = Value::String(Rc::from(key_str.as_str())); + let key_val = Value::String(Rc::clone(&key_rc)); if segments.len() == 1 { btree.insert(key_val, value); @@ -243,13 +270,24 @@ pub const ROOT_FIELDS: &[&str] = &[ "extendedLocation", ]; +const PROPERTIES_DOT: &[u8] = b"properties."; + /// Check whether an alias short name collides with a reserved ARM root field /// and needs a collision-safe key. pub fn is_root_field_collision(short_name: &str, default_path: &str) -> bool { ROOT_FIELDS .iter() .any(|f| f.eq_ignore_ascii_case(short_name)) - && default_path.to_ascii_lowercase().starts_with("properties.") + && default_path.len() > PROPERTIES_DOT.len() + && default_path + .as_bytes() + .get(..PROPERTIES_DOT.len()) + .is_some_and(|prefix| { + prefix + .iter() + .zip(PROPERTIES_DOT) + .all(|(a, b)| a.to_ascii_lowercase() == *b) + }) } /// Return a collision-safe key for an alias whose short name collides with a diff --git a/src/languages/azure_policy/aliases/types.rs b/src/languages/azure_policy/aliases/types.rs index cdab367d..994cff71 100644 --- a/src/languages/azure_policy/aliases/types.rs +++ b/src/languages/azure_policy/aliases/types.rs @@ -18,6 +18,8 @@ use alloc::vec::Vec; use serde::{Deserialize, Deserializer}; +use crate::Rc; + // ─── Top-level response wrappers ──────────────────────────────────────────── /// ARM API response envelope: `{ "value": [...] }` @@ -404,11 +406,13 @@ pub struct ResolvedEntry { // ── Precomputed fields (derived at registry-load time) ────────────── /// Whether `short_name` contains `[*]` (i.e., this is a wildcard/array alias). pub is_wildcard: bool, + /// Pre-lowercased short name as `Rc` for allocation-free common-case inserts. + pub(crate) short_name_lc: Rc, /// Precomputed `default_path.split('.').collect()` for fast ARM path navigation. - pub default_path_segments: Vec, + pub(crate) default_path_segments: Vec>, /// Precomputed path segments for each versioned path, in the same order /// as `versioned_paths`. - pub versioned_path_segments: Vec>, + pub(crate) versioned_path_segments: Vec>>, } impl ResolvedEntry { @@ -420,10 +424,15 @@ impl ResolvedEntry { metadata: Option, ) -> Self { let is_wildcard = short_name.contains("[*]"); - let default_path_segments = default_path.split('.').map(String::from).collect(); + let short_name_lc = if short_name.bytes().all(|b| !b.is_ascii_uppercase()) { + Rc::from(short_name.as_str()) + } else { + Rc::from(short_name.to_ascii_lowercase()) + }; + let default_path_segments = default_path.split('.').map(Rc::from).collect(); let versioned_path_segments = versioned_paths .iter() - .map(|(_, p)| p.split('.').map(String::from).collect()) + .map(|(_, p)| p.split('.').map(Rc::from).collect()) .collect(); Self { short_name, @@ -431,6 +440,7 @@ impl ResolvedEntry { versioned_paths, metadata, is_wildcard, + short_name_lc, default_path_segments, versioned_path_segments, } @@ -456,7 +466,7 @@ impl ResolvedEntry { /// Returns the versioned segments if `api_version` matches, otherwise /// the default segments. This avoids per-call `split('.')` for both /// default and versioned scalar alias navigation. - pub fn select_path_segments(&self, api_version: Option<&str>) -> &[String] { + pub(crate) fn select_path_segments(&self, api_version: Option<&str>) -> &[Rc] { if let Some(ver) = api_version { for (i, (v, _)) in self.versioned_paths.iter().enumerate() { if v.eq_ignore_ascii_case(ver) {