diff --git a/wacore/binary/src/attrs.rs b/wacore/binary/src/attrs.rs index 945977591..b89cdb93b 100644 --- a/wacore/binary/src/attrs.rs +++ b/wacore/binary/src/attrs.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use crate::error::{BinaryError, Result}; use crate::jid::Jid; -use crate::node::{Attrs, Node, NodeRef, NodeValue, ValueRef}; +use crate::node::{Attrs, Node, NodeRef, NodeStr, NodeValue, ValueRef}; pub struct AttrParser<'a> { pub attrs: &'a Attrs, @@ -11,7 +11,7 @@ pub struct AttrParser<'a> { } pub struct AttrParserRef<'a> { - pub attrs: &'a [(Cow<'a, str>, ValueRef<'a>)], + pub(crate) attrs: &'a [(NodeStr<'a>, ValueRef<'a>)], pub errors: Vec, } @@ -36,11 +36,7 @@ impl<'a> AttrParserRef<'a> { } fn get_raw(&mut self, key: &str, require: bool) -> Option<&'a ValueRef<'a>> { - let val = self - .attrs - .iter() - .find(|(k, _)| k.as_ref() == key) - .map(|(_, v)| v); + let val = self.attrs.iter().find(|(k, _)| **k == *key).map(|(_, v)| v); if require && val.is_none() { self.errors.push(BinaryError::AttrParse(format!( diff --git a/wacore/binary/src/decoder.rs b/wacore/binary/src/decoder.rs index a273ab0cc..b0f25b25f 100644 --- a/wacore/binary/src/decoder.rs +++ b/wacore/binary/src/decoder.rs @@ -1,11 +1,20 @@ use crate::error::{BinaryError, Result}; use crate::jid::JidRef; -use crate::node::{AttrsRef, NodeContentRef, NodeRef, NodeVec, ValueRef}; +use crate::node::{AttrsRef, NodeContentRef, NodeRef, NodeStr, NodeVec, ValueRef}; use crate::token; +use compact_str::CompactString; use std::borrow::Cow; +use std::fmt::Write; #[cfg(feature = "simd")] use std::simd::{Simd, prelude::*, u8x16}; +/// Format a JidRef directly into CompactString, avoiding the intermediate String. +fn jid_ref_to_compact(j: &JidRef<'_>) -> CompactString { + let mut s = CompactString::default(); + write!(s, "{}", j).expect("JidRef Display cannot fail"); + s +} + pub(crate) struct Decoder<'a> { data: &'a [u8], position: usize, @@ -88,10 +97,10 @@ impl<'a> Decoder<'a> { } #[inline(always)] - fn read_string(&mut self, len: usize) -> Result> { + fn read_string(&mut self, len: usize) -> Result> { let bytes = self.read_bytes(len)?; match std::str::from_utf8(bytes) { - Ok(s) => Ok(Cow::Borrowed(s)), + Ok(s) => Ok(NodeStr::Borrowed(s)), Err(e) => Err(BinaryError::InvalidUtf8(e)), } } @@ -107,9 +116,8 @@ impl<'a> Decoder<'a> { } fn read_jid_pair(&mut self) -> Result> { - let user_val = self.read_value_as_string()?; - let server_str = self.read_value_as_string()?.unwrap_or(Cow::Borrowed("")); - let user = user_val.unwrap_or(Cow::Borrowed("")); + let user = self.read_value_as_string()?.unwrap_or_default(); + let server_str = self.read_value_as_string()?.unwrap_or_default(); let server = crate::jid::Server::try_from(server_str.as_ref()).map_err(|_| { BinaryError::AttrParse(format!("JID_PAIR unknown server: {}", server_str)) })?; @@ -125,7 +133,7 @@ impl<'a> Decoder<'a> { fn read_ad_jid(&mut self) -> Result> { let agent = self.read_u8()?; let device = self.read_u8()? as u16; - let user = self + let user: NodeStr<'a> = self .read_value_as_string()? .ok_or(BinaryError::InvalidNode)?; @@ -157,7 +165,7 @@ impl<'a> Decoder<'a> { .ok_or(BinaryError::InvalidNode)?; let device = self.read_u16_be()?; let integrator = self.read_u16_be()?; - let server_str = self.read_value_as_string()?.unwrap_or(Cow::Borrowed("")); + let server_str = self.read_value_as_string()?.unwrap_or_default(); if server_str.as_ref() != crate::jid::INTEROP_SERVER { return Err(BinaryError::InvalidNode); } @@ -175,7 +183,7 @@ impl<'a> Decoder<'a> { .read_value_as_string()? .ok_or(BinaryError::InvalidNode)?; let device = self.read_u16_be()?; - let server_str = self.read_value_as_string()?.unwrap_or(Cow::Borrowed("")); + let server_str = self.read_value_as_string()?.unwrap_or_default(); if server_str.as_ref() != crate::jid::MESSENGER_SERVER { return Err(BinaryError::InvalidNode); } @@ -188,13 +196,13 @@ impl<'a> Decoder<'a> { }) } - fn read_value_as_string(&mut self) -> Result>> { + fn read_value_as_string(&mut self) -> Result>> { let tag = self.read_u8()?; self.read_value_as_string_from_tag(tag) } #[inline(always)] - fn read_value_as_string_from_tag(&mut self, tag: u8) -> Result>> { + fn read_value_as_string_from_tag(&mut self, tag: u8) -> Result>> { match tag { token::LIST_EMPTY => Ok(None), token::BINARY_8 => { @@ -211,27 +219,31 @@ impl<'a> Decoder<'a> { } token::JID_PAIR => self .read_jid_pair() - .map(|j| Some(Cow::Owned(j.to_string()))), - token::AD_JID => self.read_ad_jid().map(|j| Some(Cow::Owned(j.to_string()))), + .map(|j| Some(NodeStr::Owned(jid_ref_to_compact(&j)))), + token::AD_JID => self + .read_ad_jid() + .map(|j| Some(NodeStr::Owned(jid_ref_to_compact(&j)))), token::INTEROP_JID => self .read_interop_jid() - .map(|j| Some(Cow::Owned(j.to_string()))), - token::FB_JID => self.read_fb_jid().map(|j| Some(Cow::Owned(j.to_string()))), - token::NIBBLE_8 | token::HEX_8 => self.read_packed(tag).map(|s| Some(Cow::Owned(s))), + .map(|j| Some(NodeStr::Owned(jid_ref_to_compact(&j)))), + token::FB_JID => self + .read_fb_jid() + .map(|j| Some(NodeStr::Owned(jid_ref_to_compact(&j)))), + token::NIBBLE_8 | token::HEX_8 => { + self.read_packed(tag).map(|s| Some(NodeStr::Owned(s))) + } tag @ token::DICTIONARY_0..=token::DICTIONARY_3 => { let index = self.read_u8()?; token::get_double_token(tag - token::DICTIONARY_0, index) - .map(|s| Some(Cow::Borrowed(s))) + .map(|s| Some(NodeStr::Borrowed(s))) .ok_or(BinaryError::InvalidToken(tag)) } _ => token::get_single_token(tag) - .map(|s| Some(Cow::Borrowed(s))) + .map(|s| Some(NodeStr::Borrowed(s))) .ok_or(BinaryError::InvalidToken(tag)), } } - /// Read a value that can be either a string or a JID. - /// This avoids string allocation for JID tokens by returning the JidRef directly. fn read_value(&mut self) -> Result>> { let tag = self.read_u8()?; match tag { @@ -248,56 +260,58 @@ impl<'a> Decoder<'a> { let size = self.read_u32_be()? as usize; self.read_string(size).map(|s| Some(ValueRef::String(s))) } - // JID tokens - return JidRef directly without string allocation token::JID_PAIR => self.read_jid_pair().map(|j| Some(ValueRef::Jid(j))), token::AD_JID => self.read_ad_jid().map(|j| Some(ValueRef::Jid(j))), token::INTEROP_JID => self.read_interop_jid().map(|j| Some(ValueRef::Jid(j))), token::FB_JID => self.read_fb_jid().map(|j| Some(ValueRef::Jid(j))), token::NIBBLE_8 | token::HEX_8 => self .read_packed(tag) - .map(|s| Some(ValueRef::String(Cow::Owned(s)))), + .map(|s| Some(ValueRef::String(NodeStr::Owned(s)))), tag @ token::DICTIONARY_0..=token::DICTIONARY_3 => { let index = self.read_u8()?; token::get_double_token(tag - token::DICTIONARY_0, index) - .map(|s| Some(ValueRef::String(Cow::Borrowed(s)))) + .map(|s| Some(ValueRef::String(NodeStr::Borrowed(s)))) .ok_or(BinaryError::InvalidToken(tag)) } _ => token::get_single_token(tag) - .map(|s| Some(ValueRef::String(Cow::Borrowed(s)))) + .map(|s| Some(ValueRef::String(NodeStr::Borrowed(s)))) .ok_or(BinaryError::InvalidToken(tag)), } } - fn read_packed(&mut self, tag: u8) -> Result { + /// Decode packed nibble/hex into a stack buffer, then create CompactString. + /// Max unpacked length is 254 bytes (127 packed × 2), so the stack buffer + /// is always sufficient. Short values (≤24 bytes) are stored inline. + fn read_packed(&mut self, tag: u8) -> Result { let packed_len_byte = self.read_u8()?; let is_half_byte = (packed_len_byte & 0x80) != 0; let len = (packed_len_byte & 0x7F) as usize; if len == 0 { - return Ok(String::new()); + return Ok(CompactString::default()); } - let raw_len = if is_half_byte { (len * 2) - 1 } else { len * 2 }; let packed_data = self.read_bytes(len)?; - let mut unpacked_bytes = Vec::with_capacity(raw_len); + let mut buf = [0u8; 254]; + let mut pos = 0; match tag { - token::HEX_8 => Self::decode_packed_hex(packed_data, &mut unpacked_bytes), - token::NIBBLE_8 => Self::decode_packed_nibble(packed_data, &mut unpacked_bytes)?, + token::HEX_8 => Self::decode_packed_hex(packed_data, &mut buf, &mut pos), + token::NIBBLE_8 => Self::decode_packed_nibble(packed_data, &mut buf, &mut pos)?, _ => return Err(BinaryError::InvalidToken(tag)), } - if is_half_byte { - unpacked_bytes.pop(); + if is_half_byte && pos > 0 { + pos -= 1; } - // Lookup tables produce only ASCII bytes ('0'..'9', 'A'..'F', '-', '.', '\0'), - // so from_utf8 will never fail. Use the safe version to avoid unsafe. - Ok(String::from_utf8(unpacked_bytes).expect("packed decode produced non-ASCII")) + // All output bytes are ASCII, so from_utf8 cannot fail. + let s = std::str::from_utf8(&buf[..pos]).expect("packed decode produced non-ASCII"); + Ok(CompactString::from(s)) } #[inline] - fn decode_packed_hex(packed_data: &[u8], unpacked_bytes: &mut Vec) { + fn decode_packed_hex(packed_data: &[u8], out: &mut [u8], pos: &mut usize) { #[cfg(feature = "simd")] let packed_data = { const HEX_LOOKUP: [u8; 16] = *b"0123456789ABCDEF"; @@ -305,7 +319,6 @@ impl<'a> Decoder<'a> { let low_mask = Simd::splat(0x0F); let (chunks, remainder) = packed_data.as_chunks::<16>(); - unpacked_bytes.reserve(chunks.len() * 32); for chunk in chunks { let data = u8x16::from_array(*chunk); let high_nibbles = (data >> 4) & low_mask; @@ -313,8 +326,10 @@ impl<'a> Decoder<'a> { let high_chars = lookup_table.swizzle_dyn(high_nibbles); let low_chars = lookup_table.swizzle_dyn(low_nibbles); let (lo, hi) = Simd::interleave(high_chars, low_chars); - unpacked_bytes.extend_from_slice(lo.as_array()); - unpacked_bytes.extend_from_slice(hi.as_array()); + out[*pos..*pos + 16].copy_from_slice(lo.as_array()); + *pos += 16; + out[*pos..*pos + 16].copy_from_slice(hi.as_array()); + *pos += 16; } remainder }; @@ -322,13 +337,15 @@ impl<'a> Decoder<'a> { for &byte in packed_data { let high = (byte & 0xF0) >> 4; let low = byte & 0x0F; - unpacked_bytes.push(Self::unpack_hex(high)); - unpacked_bytes.push(Self::unpack_hex(low)); + out[*pos] = Self::unpack_hex(high); + *pos += 1; + out[*pos] = Self::unpack_hex(low); + *pos += 1; } } #[inline] - fn decode_packed_nibble(packed_data: &[u8], unpacked_bytes: &mut Vec) -> Result<()> { + fn decode_packed_nibble(packed_data: &[u8], out: &mut [u8], pos: &mut usize) -> Result<()> { #[cfg(feature = "simd")] let packed_data = { const NIBBLE_LOOKUP: [u8; 16] = *b"0123456789-.\x00\x00\x00\x00"; @@ -338,7 +355,6 @@ impl<'a> Decoder<'a> { let f15 = Simd::splat(15); let (chunks, remainder) = packed_data.as_chunks::<16>(); - unpacked_bytes.reserve(chunks.len() * 32); for chunk in chunks { let data = u8x16::from_array(*chunk); @@ -348,7 +364,6 @@ impl<'a> Decoder<'a> { let hi_valid = high_nibbles.simd_le(le11) | high_nibbles.simd_eq(f15); let lo_valid = low_nibbles.simd_le(le11) | low_nibbles.simd_eq(f15); if !(hi_valid & lo_valid).all() { - // Validate first, then decode scalar as a conservative fallback. for byte in *chunk { let high = (byte & 0xF0) >> 4; let low = byte & 0x0F; @@ -358,8 +373,10 @@ impl<'a> Decoder<'a> { for byte in *chunk { let high = (byte & 0xF0) >> 4; let low = byte & 0x0F; - unpacked_bytes.push(Self::unpack_nibble(high)?); - unpacked_bytes.push(Self::unpack_nibble(low)?); + out[*pos] = Self::unpack_nibble(high)?; + *pos += 1; + out[*pos] = Self::unpack_nibble(low)?; + *pos += 1; } continue; } @@ -367,8 +384,10 @@ impl<'a> Decoder<'a> { let high_chars = lookup_table.swizzle_dyn(high_nibbles); let low_chars = lookup_table.swizzle_dyn(low_nibbles); let (lo, hi) = Simd::interleave(high_chars, low_chars); - unpacked_bytes.extend_from_slice(lo.as_array()); - unpacked_bytes.extend_from_slice(hi.as_array()); + out[*pos..*pos + 16].copy_from_slice(lo.as_array()); + *pos += 16; + out[*pos..*pos + 16].copy_from_slice(hi.as_array()); + *pos += 16; } remainder }; @@ -376,8 +395,10 @@ impl<'a> Decoder<'a> { for &byte in packed_data { let high = (byte & 0xF0) >> 4; let low = byte & 0x0F; - unpacked_bytes.push(Self::unpack_nibble(high)?); - unpacked_bytes.push(Self::unpack_nibble(low)?); + out[*pos] = Self::unpack_nibble(high)?; + *pos += 1; + out[*pos] = Self::unpack_nibble(low)?; + *pos += 1; } Ok(()) @@ -409,10 +430,9 @@ impl<'a> Decoder<'a> { let key = self .read_value_as_string()? .ok_or(BinaryError::NonStringKey)?; - // Use read_value to get ValueRef - avoids string allocation for JIDs let value = self .read_value()? - .unwrap_or(ValueRef::String(Cow::Borrowed(""))); + .unwrap_or(ValueRef::String(NodeStr::Borrowed(""))); attrs.push((key, value)); } Ok(attrs) diff --git a/wacore/binary/src/jid.rs b/wacore/binary/src/jid.rs index 54fc71f76..6be2be1ce 100644 --- a/wacore/binary/src/jid.rs +++ b/wacore/binary/src/jid.rs @@ -1,5 +1,5 @@ +use crate::node::NodeStr; use compact_str::CompactString; -use std::borrow::Cow; use std::fmt; use std::str::FromStr; @@ -374,7 +374,7 @@ pub struct Jid { #[derive(Debug, Clone, PartialEq, Eq, Hash, yoke::Yokeable)] pub struct JidRef<'a> { - pub user: Cow<'a, str>, + pub user: NodeStr<'a>, pub server: Server, pub agent: u8, pub device: u16, @@ -505,13 +505,6 @@ impl Jid { } } - pub fn actual_agent(&self) -> u8 { - match self.server { - Server::Pn | Server::Lid | Server::Hosted | Server::HostedLid => 0, - _ => self.agent, - } - } - pub fn to_non_ad(&self) -> Self { Self { user: self.user.clone(), @@ -596,19 +589,9 @@ impl<'a> JidExt for JidRef<'a> { } impl<'a> JidRef<'a> { - pub fn new(user: Cow<'a, str>, server: Server) -> Self { - Self { - user, - server, - agent: 0, - device: 0, - integrator: 0, - } - } - pub fn to_owned(&self) -> Jid { Jid { - user: CompactString::from(self.user.as_ref()), + user: self.user.to_compact_string(), server: self.server, agent: self.agent, device: self.device, diff --git a/wacore/binary/src/lib.rs b/wacore/binary/src/lib.rs index 7e7e53710..12bde6f76 100644 --- a/wacore/binary/src/lib.rs +++ b/wacore/binary/src/lib.rs @@ -25,4 +25,6 @@ pub use marshal::{ marshal, marshal_auto, marshal_exact, marshal_ref, marshal_ref_auto, marshal_ref_exact, marshal_ref_to, marshal_ref_to_vec, marshal_to, marshal_to_vec, }; -pub use node::{Attrs, Node, NodeContent, NodeContentRef, NodeRef, NodeValue, OwnedNodeRef}; +pub use node::{ + Attrs, Node, NodeContent, NodeContentRef, NodeRef, NodeStr, NodeValue, OwnedNodeRef, +}; diff --git a/wacore/binary/src/node.rs b/wacore/binary/src/node.rs index 7131de0f3..ad89db1b0 100644 --- a/wacore/binary/src/node.rs +++ b/wacore/binary/src/node.rs @@ -4,6 +4,107 @@ use crate::token; use compact_str::CompactString; use std::borrow::Cow; +/// Borrowed-or-inline string for decoded nodes. Short owned values (≤24 bytes) +/// are stored inline via `CompactString`, avoiding heap allocation. +#[derive(Clone, yoke::Yokeable)] +pub enum NodeStr<'a> { + Borrowed(&'a str), + Owned(CompactString), +} + +impl NodeStr<'_> { + /// Clone-preserving conversion. Avoids re-parsing the inner CompactString + /// when converting owned NodeStr values in `to_owned()` paths. + #[inline] + pub fn to_compact_string(&self) -> CompactString { + match self { + NodeStr::Borrowed(s) => CompactString::from(*s), + NodeStr::Owned(cs) => cs.clone(), + } + } +} + +impl Default for NodeStr<'_> { + #[inline] + fn default() -> Self { + NodeStr::Borrowed("") + } +} + +impl std::ops::Deref for NodeStr<'_> { + type Target = str; + #[inline(always)] + fn deref(&self) -> &str { + match self { + NodeStr::Borrowed(s) => s, + NodeStr::Owned(cs) => cs.as_str(), + } + } +} + +impl AsRef for NodeStr<'_> { + #[inline(always)] + fn as_ref(&self) -> &str { + self + } +} + +impl std::fmt::Debug for NodeStr<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Debug::fmt(&**self, f) + } +} + +impl std::fmt::Display for NodeStr<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self) + } +} + +impl PartialEq for NodeStr<'_> { + #[inline] + fn eq(&self, other: &Self) -> bool { + **self == **other + } +} + +impl Eq for NodeStr<'_> {} + +impl std::hash::Hash for NodeStr<'_> { + #[inline] + fn hash(&self, state: &mut H) { + (**self).hash(state) + } +} + +impl PartialEq for NodeStr<'_> { + #[inline] + fn eq(&self, other: &str) -> bool { + &**self == other + } +} + +impl PartialEq<&str> for NodeStr<'_> { + #[inline] + fn eq(&self, other: &&str) -> bool { + &**self == *other + } +} + +impl<'a> From<&'a str> for NodeStr<'a> { + #[inline] + fn from(s: &'a str) -> Self { + NodeStr::Borrowed(s) + } +} + +impl From for NodeStr<'_> { + #[inline] + fn from(s: CompactString) -> Self { + NodeStr::Owned(s) + } +} + /// Intern a string as a `Cow::Borrowed(&'static str)` if it matches a known token, /// otherwise allocate a `Cow::Owned(String)`. This avoids heap allocations for the /// vast majority of tag names and attribute keys which are protocol tokens. @@ -273,24 +374,24 @@ impl FromIterator<(Cow<'static, str>, NodeValue)> for Attrs { Self(iter.into_iter().collect()) } } -pub type AttrsRef<'a> = Vec<(Cow<'a, str>, ValueRef<'a>)>; +pub type AttrsRef<'a> = Vec<(NodeStr<'a>, ValueRef<'a>)>; /// A decoded attribute value that can be either a string or a structured JID. /// This avoids string allocation when decoding JID tokens - the JidRef is returned /// directly and only converted to a string when actually needed. #[derive(Debug, Clone, PartialEq, yoke::Yokeable)] pub enum ValueRef<'a> { - String(Cow<'a, str>), + String(NodeStr<'a>), Jid(JidRef<'a>), } impl<'a> ValueRef<'a> { - /// String view of the value. Works for both variants. - /// - String variant: Cow::Borrowed — zero copy + /// String view of the value. Borrows from `self`. + /// - String variant: borrows the inner str — zero copy /// - Jid variant: Cow::Owned — allocates only when needed - pub fn as_str(&self) -> Cow<'a, str> { + pub fn as_str(&self) -> Cow<'_, str> { match self { - ValueRef::String(s) => s.clone(), + ValueRef::String(s) => Cow::Borrowed(s), ValueRef::Jid(j) => Cow::Owned(j.to_string()), } } @@ -336,7 +437,7 @@ pub enum NodeContent { #[derive(Debug, Clone, PartialEq, yoke::Yokeable)] pub enum NodeContentRef<'a> { Bytes(Cow<'a, [u8]>), - String(Cow<'a, str>), + String(NodeStr<'a>), Nodes(Box>), } @@ -345,7 +446,7 @@ impl NodeContent { pub fn as_content_ref(&self) -> NodeContentRef<'_> { match self { NodeContent::Bytes(b) => NodeContentRef::Bytes(Cow::Borrowed(b)), - NodeContent::String(s) => NodeContentRef::String(Cow::Borrowed(s.as_str())), + NodeContent::String(s) => NodeContentRef::String(NodeStr::Borrowed(s.as_str())), NodeContent::Nodes(nodes) => { NodeContentRef::Nodes(Box::new(nodes.iter().map(|n| n.as_node_ref()).collect())) } @@ -363,7 +464,7 @@ pub struct Node { #[derive(Debug, Clone, PartialEq, yoke::Yokeable)] pub struct NodeRef<'a> { - pub tag: Cow<'a, str>, + pub tag: NodeStr<'a>, pub attrs: AttrsRef<'a>, pub content: Option>>, } @@ -385,22 +486,22 @@ impl Node { /// The returned NodeRef borrows from self. pub fn as_node_ref(&self) -> NodeRef<'_> { NodeRef { - tag: Cow::Borrowed(self.tag.as_ref()), + tag: NodeStr::Borrowed(self.tag.as_ref()), attrs: self .attrs .iter() .map(|(k, v)| { let value_ref = match v { - NodeValue::String(s) => ValueRef::String(Cow::Borrowed(s.as_str())), + NodeValue::String(s) => ValueRef::String(NodeStr::Borrowed(s.as_str())), NodeValue::Jid(j) => ValueRef::Jid(JidRef { - user: Cow::Borrowed(&j.user), + user: NodeStr::Borrowed(&j.user), server: j.server, agent: j.agent, device: j.device, integrator: j.integrator, }), }; - (Cow::Borrowed(k.as_ref()), value_ref) + (NodeStr::Borrowed(k.as_ref()), value_ref) }) .collect(), content: self.content.as_ref().map(|c| Box::new(c.as_content_ref())), @@ -459,11 +560,7 @@ impl Node { } impl<'a> NodeRef<'a> { - pub fn new( - tag: Cow<'a, str>, - attrs: AttrsRef<'a>, - content: Option>, - ) -> Self { + pub fn new(tag: NodeStr<'a>, attrs: AttrsRef<'a>, content: Option>) -> Self { Self { tag, attrs, @@ -486,7 +583,7 @@ impl<'a> NodeRef<'a> { self.attrs.iter().find(|(k, _)| k == key).map(|(_, v)| v) } - pub fn attrs_iter(&self) -> impl Iterator, &ValueRef<'a>)> { + pub fn attrs_iter(&self) -> impl Iterator, &ValueRef<'a>)> { self.attrs.iter().map(|(k, v)| (k, v)) } @@ -524,7 +621,7 @@ impl<'a> NodeRef<'a> { /// Extract text content, handling both String and Bytes (lossy UTF-8). pub fn content_as_string(&self) -> Option { match self.content.as_deref() { - Some(NodeContentRef::String(s)) => Some(CompactString::from(s.as_ref())), + Some(NodeContentRef::String(s)) => Some(s.to_compact_string()), Some(NodeContentRef::Bytes(b)) => Some(CompactString::from( String::from_utf8_lossy(b.as_ref()).as_ref(), )), @@ -563,7 +660,7 @@ impl<'a> NodeRef<'a> { .iter() .map(|(k, v)| { let value = match v { - ValueRef::String(s) => NodeValue::String(CompactString::from(s.as_ref())), + ValueRef::String(s) => NodeValue::String(s.to_compact_string()), ValueRef::Jid(j) => NodeValue::Jid(j.to_owned()), }; (intern_cow(k), value) @@ -571,7 +668,7 @@ impl<'a> NodeRef<'a> { .collect::(), content: self.content.as_deref().map(|c| match c { NodeContentRef::Bytes(b) => NodeContent::Bytes(b.to_vec()), - NodeContentRef::String(s) => NodeContent::String(CompactString::from(s.as_ref())), + NodeContentRef::String(s) => NodeContent::String(s.to_compact_string()), NodeContentRef::Nodes(nodes) => { NodeContent::Nodes(nodes.iter().map(|n| n.to_owned()).collect()) }