Skip to main content

ical/tree/value/
node.rs

1//! # Value node
2//!
3//! The raw value of a content line, on the syntax side.
4//!
5//! [`IcalValueNode`] is the syntactic peer of the decoded
6//! [`IcalValue`](crate::value::IcalValue): the bytes after a line's colon,
7//! understood as `;`-separated components of `,`-separated
8//! [`IcalValueLeaf`](crate::tree::leaf::IcalValueLeaf) values (raw bytes, so
9//! a foreign charset survives). Straight from parse the value is kept as one
10//! unsplit `raw` slice and only walked on demand; an edit or a model encode
11//! splits it into owned `components`, which then become the source of truth.
12//! Either way the splitting is generic (it counts and preserves separators so
13//! the value round-trips); what those components *mean* is the lens's business.
14//! The codec that unescapes components into decoded values ([`decode_at`],
15//! [`decode_scalar_at`]) and re-escapes edits back ([`set_at`]) lives on this
16//! type; the escaping rules it applies come from the sibling
17//! [`mode`](crate::tree::codec::mode) codec.
18//!
19//! [`decode_at`]: IcalValueNode::decode_at
20//! [`decode_scalar_at`]: IcalValueNode::decode_scalar_at
21//! [`set_at`]: IcalValueNode::set_at
22
23use core::fmt;
24
25use alloc::{borrow::Cow, string::String, vec::Vec};
26
27use crate::tree::{
28    codec::{
29        encode::encode_component,
30        escape::escape_with,
31        mode::Escaper,
32        unescape::{unescape_bytes, unescape_with},
33    },
34    leaf::IcalValueLeaf,
35};
36
37/// A raw value: `;`-separated components, each a list of `,`-separated raw
38/// value leaves.
39///
40/// From parse the value is held as one unsplit `raw` slice and
41/// walked lazily, so a parse that never decodes and a byte-faithful reserialize
42/// do no splitting at all. The first edit (or a model encode) splits it into
43/// owned `components`, which then take over as the source of
44/// truth. The `escaper` records which version's escaping rules the codec must
45/// apply; it is stamped from the card version after parsing (see
46/// `IcalCst::parse`).
47#[derive(Clone, Debug, Default)]
48pub struct IcalValueNode<'a> {
49    /// The unsplit value bytes, straight from parse and authoritative until an
50    /// edit or a model encode replaces them; `None` once `components` is the
51    /// source of truth.
52    raw: Option<Cow<'a, [u8]>>,
53    /// The split components, authoritative when `raw` is `None`; while `raw` is
54    /// `Some` this stays empty and reads split `raw` on demand.
55    components: Vec<Vec<IcalValueLeaf<'a>>>,
56    /// The escaping rules to read and write this value with.
57    pub escaper: Escaper,
58}
59
60impl<'a> IcalValueNode<'a> {
61    /// Wrap a raw value, unsplit and borrowed, to be walked lazily. The colon,
62    /// name and eol are the line's business; this is only the bytes after the
63    /// colon.
64    pub fn parse(value: &'a [u8]) -> Self {
65        Self {
66            raw: Some(Cow::Borrowed(value)),
67            components: Vec::new(),
68            escaper: Escaper::default(),
69        }
70    }
71
72    /// Build a value node from already-split components (the model encode
73    /// path); there is no `raw`, so the components are the source of truth.
74    pub(crate) fn from_components(
75        components: Vec<Vec<IcalValueLeaf<'a>>>,
76        escaper: Escaper,
77    ) -> Self {
78        Self {
79            raw: None,
80            components,
81            escaper,
82        }
83    }
84
85    /// The number of `;`-separated components (at least one, since an empty
86    /// value is one empty component).
87    pub fn component_count(&self) -> usize {
88        match &self.raw {
89            Some(raw) => {
90                let mut count = 0;
91                split_on(raw, b';', |_| count += 1);
92                count
93            }
94            None => self.components.len(),
95        }
96    }
97
98    /// Decode the `i`th component into a clean (unescaped) value list.
99    pub fn decode_at(&self, i: usize) -> Vec<Cow<'_, str>> {
100        match self.component_at(i) {
101            Some(Component::Raw(bytes)) => {
102                let mut values = Vec::new();
103                split_on(bytes, b',', |value| {
104                    values.push(unescape_with(value, self.escaper))
105                });
106                values
107            }
108            Some(Component::Split(leaves)) => leaves
109                .iter()
110                .map(|leaf| unescape_with(leaf.as_bytes(), self.escaper))
111                .collect(),
112            None => Vec::new(),
113        }
114    }
115
116    /// The `i`th component's first value as raw unescaped bytes, not
117    /// transcoded, for a value carrying a foreign charset.
118    pub fn decode_bytes_at(&self, i: usize) -> Cow<'_, [u8]> {
119        match self.component_at(i) {
120            Some(Component::Raw(bytes)) => unescape_bytes(first_value(bytes), self.escaper),
121            Some(Component::Split(leaves)) => leaves
122                .first()
123                .map(|leaf| unescape_bytes(leaf.as_bytes(), self.escaper))
124                .unwrap_or(Cow::Borrowed(b"")),
125            None => Cow::Borrowed(b""),
126        }
127    }
128
129    /// Decode the `i`th component's first value (empty when there is none).
130    pub fn decode_scalar_at(&self, i: usize) -> Cow<'_, str> {
131        match self.component_at(i) {
132            Some(Component::Raw(bytes)) => unescape_with(first_value(bytes), self.escaper),
133            Some(Component::Split(leaves)) => leaves
134                .first()
135                .map(|leaf| unescape_with(leaf.as_bytes(), self.escaper))
136                .unwrap_or(Cow::Borrowed("")),
137            None => Cow::Borrowed(""),
138        }
139    }
140
141    /// Decode the `i`th component as a single value, keeping its `,`-separated
142    /// pieces joined. For values like URIs whose comma is a literal part of the
143    /// value, not a list separator (so they must not be truncated).
144    pub fn decode_joined_at(&self, i: usize) -> Cow<'_, str> {
145        match self.component_at(i) {
146            // NOTE: The whole component slice already has the commas in place,
147            // so unescaping it verbatim keeps them literal.
148            Some(Component::Raw(bytes)) => unescape_with(bytes, self.escaper),
149            Some(Component::Split(leaves)) => {
150                if leaves.len() <= 1 {
151                    return leaves
152                        .first()
153                        .map(|leaf| unescape_with(leaf.as_bytes(), self.escaper))
154                        .unwrap_or(Cow::Borrowed(""));
155                }
156
157                let mut raw = Vec::new();
158                for (j, leaf) in leaves.iter().enumerate() {
159                    if j > 0 {
160                        raw.push(b',');
161                    }
162                    raw.extend_from_slice(leaf.as_bytes());
163                }
164
165                Cow::Owned(unescape_with(&raw, self.escaper).into_owned())
166            }
167            None => Cow::Borrowed(""),
168        }
169    }
170
171    /// The raw (still-escaped) bytes of the first component's first value, for
172    /// the simple single-value lines (the envelope values and diagnostics).
173    pub(crate) fn first_value_bytes(&self) -> &[u8] {
174        match self.component_at(0) {
175            Some(Component::Raw(bytes)) => first_value(bytes),
176            Some(Component::Split(leaves)) => {
177                leaves.first().map(|leaf| leaf.as_bytes()).unwrap_or(b"")
178            }
179            None => b"",
180        }
181    }
182
183    /// Set the `i`th component, escaping each value. Pads with empty components
184    /// when needed; every other component is left untouched, so a parsed card
185    /// keeps its bytes.
186    pub fn set_at<S: AsRef<str>>(&mut self, i: usize, values: &[S]) {
187        self.materialize();
188
189        while self.components.len() <= i {
190            self.components.push(Vec::new());
191        }
192
193        self.components[i] = encode_component(values, self.escaper);
194    }
195
196    /// Set the `i`th component from raw value bytes (the foreign-charset escape
197    /// hatch), escaping structural separators but writing the bytes verbatim.
198    pub fn set_bytes_at<B: AsRef<[u8]>>(&mut self, i: usize, values: &[B]) {
199        self.materialize();
200
201        while self.components.len() <= i {
202            self.components.push(Vec::new());
203        }
204
205        self.components[i] = values
206            .iter()
207            .map(|v| IcalValueLeaf::from(escape_with(v.as_ref(), self.escaper).into_owned()))
208            .collect();
209    }
210
211    /// Serialize the raw value bytes (name, colon and eol are the line's job)
212    /// into `out`, exactly as parsed. An untouched value emits its `raw` slice
213    /// with no reassembly.
214    pub(crate) fn write_bytes(&self, out: &mut Vec<u8>) {
215        if let Some(raw) = &self.raw {
216            out.extend_from_slice(raw);
217            return;
218        }
219
220        for (i, component) in self.components.iter().enumerate() {
221            if i > 0 {
222                out.push(b';');
223            }
224
225            for (j, leaf) in component.iter().enumerate() {
226                if j > 0 {
227                    out.push(b',');
228                }
229
230                out.extend_from_slice(leaf.as_bytes());
231            }
232        }
233    }
234
235    /// Convert into an owned value node (`'static`), keeping it lazy: an
236    /// unsplit value stays unsplit, only its bytes become owned.
237    pub(crate) fn into_static(self) -> IcalValueNode<'static> {
238        match self.raw {
239            Some(raw) => IcalValueNode {
240                raw: Some(Cow::Owned(raw.into_owned())),
241                components: Vec::new(),
242                escaper: self.escaper,
243            },
244            None => IcalValueNode {
245                raw: None,
246                components: self
247                    .components
248                    .into_iter()
249                    .map(|component| {
250                        component
251                            .into_iter()
252                            .map(IcalValueLeaf::into_static)
253                            .collect()
254                    })
255                    .collect(),
256                escaper: self.escaper,
257            },
258        }
259    }
260
261    /// Locate the `i`th component, either as a raw slice of the unsplit value
262    /// or as the already-split leaves.
263    fn component_at(&self, i: usize) -> Option<Component<'_, 'a>> {
264        match &self.raw {
265            Some(raw) => {
266                let mut found = None;
267                let mut index = 0;
268                split_on(raw, b';', |component| {
269                    if index == i {
270                        found = Some(component);
271                    }
272                    index += 1;
273                });
274                found.map(Component::Raw)
275            }
276            None => self
277                .components
278                .get(i)
279                .map(|leaves| Component::Split(leaves)),
280        }
281    }
282
283    /// Split the unsplit `raw` value into owned components so it can be edited
284    /// in place; a no-op once already split. Only edits pay this cost.
285    fn materialize(&mut self) {
286        let Some(raw) = self.raw.take() else {
287            return;
288        };
289
290        self.components = match raw {
291            Cow::Borrowed(bytes) => split_all(bytes),
292            Cow::Owned(bytes) => split_all_owned(&bytes),
293        };
294    }
295}
296
297impl fmt::Display for IcalValueNode<'_> {
298    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
299        if let Some(raw) = &self.raw {
300            return f.write_str(&String::from_utf8_lossy(raw));
301        }
302
303        for (i, component) in self.components.iter().enumerate() {
304            if i > 0 {
305                f.write_str(";")?;
306            }
307
308            for (j, leaf) in component.iter().enumerate() {
309                if j > 0 {
310                    f.write_str(",")?;
311                }
312
313                f.write_str(&String::from_utf8_lossy(leaf.as_bytes()))?;
314            }
315        }
316
317        Ok(())
318    }
319}
320
321/// One located component: a slice of the still-unsplit value, or its leaves
322/// once the value has been split for editing.
323enum Component<'s, 'a> {
324    /// The component's bytes, still `,`-joined, borrowed from the unsplit
325    /// value.
326    Raw(&'s [u8]),
327    /// The component's already-split leaves.
328    Split(&'s [IcalValueLeaf<'a>]),
329}
330
331/// The first `,`-separated value of a component (escape-aware): the bytes up to
332/// the first unescaped comma, or the whole component when it has none.
333fn first_value(component: &[u8]) -> &[u8] {
334    let mut first = None;
335    split_on(component, b',', |value| {
336        first.get_or_insert(value);
337    });
338    first.unwrap_or(component)
339}
340
341/// Split a value into its `;`-separated components, each a list of its
342/// `,`-separated leaves, borrowing from `bytes`.
343fn split_all(bytes: &[u8]) -> Vec<Vec<IcalValueLeaf<'_>>> {
344    let mut components = Vec::new();
345    split_on(bytes, b';', |component| {
346        let mut values = Vec::new();
347        split_on(component, b',', |value| {
348            values.push(IcalValueLeaf::from(value));
349        });
350        components.push(values);
351    });
352    components
353}
354
355/// Split like [`split_all`] but copying each leaf, for the owned bytes an
356/// edit-after-`into_static` value carries (which no slice can borrow from).
357fn split_all_owned(bytes: &[u8]) -> Vec<Vec<IcalValueLeaf<'static>>> {
358    let mut components = Vec::new();
359    split_on(bytes, b';', |component| {
360        let mut values = Vec::new();
361        split_on(component, b',', |value| {
362            values.push(IcalValueLeaf::from(value.to_vec()));
363        });
364        components.push(values);
365    });
366    components
367}
368
369/// Call `piece` for each span between unescaped `sep` bytes, always at least
370/// once. A backslash escapes the next byte (so `\;` / `\,` do not split), and
371/// `memchr` skips straight to the next `sep` or backslash instead of scanning
372/// byte by byte, so a large separator-free value (e.g. base64) is skipped in
373/// one pass.
374fn split_on<'b>(bytes: &'b [u8], sep: u8, mut piece: impl FnMut(&'b [u8])) {
375    let mut start = 0;
376    let mut i = 0;
377
378    while let Some(offset) = memchr::memchr2(b'\\', sep, &bytes[i..]) {
379        let pos = i + offset;
380        if bytes[pos] == b'\\' {
381            i = (pos + 2).min(bytes.len());
382        } else {
383            piece(&bytes[start..pos]);
384            start = pos + 1;
385            i = pos + 1;
386        }
387    }
388
389    piece(&bytes[start..]);
390}
391
392#[cfg(test)]
393mod tests {
394    use alloc::{borrow::Cow, string::ToString, vec};
395
396    use crate::tree::value::IcalValueNode;
397
398    #[test]
399    fn splits_components_and_values_then_round_trips() {
400        let node = IcalValueNode::parse(b"a;b,c;");
401        assert_eq!(node.component_count(), 3);
402        assert_eq!(
403            node.decode_at(1),
404            vec![Cow::Borrowed("b"), Cow::Borrowed("c")]
405        );
406        assert_eq!(node.to_string(), "a;b,c;");
407    }
408
409    #[test]
410    fn keeps_escaped_separators_inside_one_value() {
411        let node = IcalValueNode::parse(br"a\,b\;c;d");
412        assert_eq!(node.component_count(), 2);
413        assert_eq!(node.decode_at(0).len(), 1);
414        assert_eq!(node.to_string(), r"a\,b\;c;d");
415    }
416
417    #[test]
418    fn an_edit_splits_and_preserves_untouched_components() {
419        let mut node = IcalValueNode::parse(b"a;b;c");
420        node.set_at(1, &["X"]);
421        assert_eq!(node.to_string(), "a;X;c");
422    }
423}