Skip to main content

vcard/tree/codec/
encode.rs

1//! # Encode (model to syntax)
2//!
3//! The write side of the structural bridge: project the decoded model onto a
4//! raw syntax tree.
5//!
6//! A value's [`VcardCodec`] impl encodes it into a [`VcardValueNode`], a
7//! [`VcardParam`] into a [`VcardParamNode`], a [`VcardProp`] into a
8//! [`VcardLine`] (its name verbatim from the property, its value delegated to
9//! the value codec), and a [`Vcard`] into a whole [`VcardCst`].
10//!
11//! The card is encoded for its version's [`VcardEscaper`], which the value
12//! codecs use to escape every leaf (through the sibling
13//! [`escape`](crate::tree::codec::escape) codec) and to pick any
14//! version-specific value shape.
15//!
16//! Byte-preserving edits are the cursors' job, not this module's.
17//! [`Display`](core::fmt::Display) for [`Vcard`] renders a decoded card
18//! straight to its serialized bytes through here.
19
20use core::fmt;
21
22use alloc::{borrow::Cow, string::ToString, vec, vec::Vec};
23
24use crate::{
25    param::VcardParam,
26    prop::VcardProp,
27    tree::{
28        codec::{
29            VcardCodec,
30            escape::{escape_param, escape_with},
31            mode::VcardEscaper,
32        },
33        cst::VcardCst,
34        leaf::{VcardLeaf, VcardValueLeaf},
35        line::VcardLine,
36        param::node::VcardParamNode,
37        value::node::VcardValueNode,
38        wire::VcardWire,
39    },
40    validator::VcardValid,
41    vcard::Vcard,
42};
43
44impl Vcard<'_> {
45    /// Encode the whole card into a CST for its version's escaping mode.
46    pub fn encode(&self) -> VcardCst<'static> {
47        let escaper = VcardEscaper::for_version(self.version);
48
49        let mut cst = VcardCst::v4();
50        // NOTE: v4() seeds a VERSION line as the first property; set it to this
51        // card's version, then append the rest. VERSION stays an ordinary
52        // property.
53        cst.props[0] = VcardLine::text("VERSION", self.version.to_string());
54        cst.props
55            .extend(self.properties.iter().map(|prop| prop.encode(escaper)));
56
57        cst
58    }
59}
60
61impl<'a> From<Vcard<'a>> for VcardCst<'static> {
62    fn from(card: Vcard<'a>) -> Self {
63        card.encode()
64    }
65}
66
67impl From<VcardValid<Vcard<'_>>> for VcardCst<'static> {
68    fn from(card: VcardValid<Vcard<'_>>) -> Self {
69        card.into_inner().encode()
70    }
71}
72
73impl VcardProp<'_> {
74    /// Encode the property into a raw content line for the given escaping mode,
75    /// dispatching on its value.
76    pub fn encode(&self, escaper: VcardEscaper) -> VcardLine<'static> {
77        VcardLine {
78            name: VcardLeaf::from(self.name.to_string()),
79            params: self
80                .params
81                .iter()
82                .map(|param| param.encode(escaper))
83                .collect(),
84            value: self.value.encode(escaper),
85            eol: VcardLeaf::from("\r\n".to_string()),
86            wire: VcardWire::default(),
87        }
88    }
89}
90
91impl VcardParam<'_> {
92    /// Encode the parameter into a raw parameter node for the given escaping
93    /// mode, dispatching on its kind.
94    pub fn encode(&self, escaper: VcardEscaper) -> VcardParamNode<'static> {
95        use crate::param::VcardParamKind::*;
96
97        match self {
98            VcardParam::Language(v) => param_scalar(&Language, v, escaper),
99            VcardParam::Charset(v) => param_scalar(&Charset, v, escaper),
100            VcardParam::Encoding(v) => param_scalar(&Encoding, v, escaper),
101            VcardParam::Value(v) => param_scalar(&Value, v, escaper),
102            VcardParam::Pref(v) => param_scalar(&Pref, v, escaper),
103            VcardParam::AltId(v) => param_scalar(&AltId, v, escaper),
104            VcardParam::Pid(vs) => param_list(&Pid, vs, escaper),
105            VcardParam::Type(vs) => param_list(&Type, vs, escaper),
106            VcardParam::MediaType(v) => param_scalar(&MediaType, v, escaper),
107            VcardParam::CalScale(v) => param_scalar(&CalScale, v, escaper),
108            VcardParam::SortAs(vs) => param_list(&SortAs, vs, escaper),
109            VcardParam::Geo(v) => param_scalar(&Geo, v, escaper),
110            VcardParam::Tz(v) => param_scalar(&Tz, v, escaper),
111            VcardParam::Label(v) => param_scalar(&Label, v, escaper),
112            VcardParam::Author(v) => param_scalar(&Author, v, escaper),
113            VcardParam::AuthorName(v) => param_scalar(&AuthorName, v, escaper),
114            VcardParam::Created(v) => param_scalar(&Created, v, escaper),
115            VcardParam::Derived(v) => param_scalar(&Derived, v, escaper),
116            VcardParam::Jsptr(v) => param_scalar(&Jsptr, v, escaper),
117            VcardParam::Phonetic(v) => param_scalar(&Phonetic, v, escaper),
118            VcardParam::PropId(v) => param_scalar(&PropId, v, escaper),
119            VcardParam::Script(v) => param_scalar(&Script, v, escaper),
120            VcardParam::ServiceType(v) => param_scalar(&ServiceType, v, escaper),
121            VcardParam::Username(v) => param_scalar(&Username, v, escaper),
122
123            VcardParam::Unknown { name, values } => param_list(name, values, escaper),
124        }
125    }
126}
127
128/// Serialize the decoded card by encoding it into a CST (canonical).
129impl fmt::Display for Vcard<'_> {
130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131        write!(f, "{}", self.encode())
132    }
133}
134
135/// A one-component, one-value syntax node, escaping the value by the given
136/// mode.
137pub(crate) fn scalar_node(value: &str, escaper: VcardEscaper) -> VcardValueNode<'static> {
138    VcardValueNode::from_components(vec![encode_component(&[value], escaper)], escaper)
139}
140
141/// Own one value exactly as given, with no escaping at all.
142///
143/// A URI is not text: RFC 6350 section 4.2 gives it no escapes, so escaping
144/// its `;` or `,` on the way out would rewrite the reference the value is,
145/// and a value that decoded whole would not survive its own round trip.
146pub(crate) fn verbatim_node(value: &str, escaper: VcardEscaper) -> VcardValueNode<'static> {
147    VcardValueNode::from_raw(value.as_bytes().to_vec(), escaper)
148}
149
150/// Escape and own a clean value list into one component, by escaping mode.
151pub(crate) fn encode_component<S: AsRef<str>>(
152    values: &[S],
153    escaper: VcardEscaper,
154) -> Vec<VcardValueLeaf<'static>> {
155    values.iter().map(|v| encode_leaf(v, escaper)).collect()
156}
157
158/// Escape and own raw value bytes into one component, by escaping mode.
159///
160/// The foreign-charset escape hatch: only the structural separators are
161/// escaped, every other byte going out exactly as given.
162pub(crate) fn encode_bytes_component<B: AsRef<[u8]>>(
163    values: &[B],
164    escaper: VcardEscaper,
165) -> Vec<VcardValueLeaf<'static>> {
166    values
167        .iter()
168        .map(|v| VcardValueLeaf::from(escape_with(v.as_ref(), escaper).into_owned()))
169        .collect()
170}
171
172/// Escape one value into an owned leaf, by escaping mode. Backs the per-item
173/// value edits, which splice a single leaf and leave its siblings' bytes as
174/// they were parsed.
175pub(crate) fn encode_leaf<S: AsRef<str>>(
176    value: S,
177    escaper: VcardEscaper,
178) -> VcardValueLeaf<'static> {
179    VcardValueLeaf::from(escape_with(value.as_ref().as_bytes(), escaper).into_owned())
180}
181
182/// A parameter node from a single value, encoded by the given mode's parameter
183/// rules.
184fn param_scalar(name: &str, value: &str, escaper: VcardEscaper) -> VcardParamNode<'static> {
185    VcardParamNode {
186        name: VcardLeaf::from(name.to_string()),
187        values: vec![VcardLeaf::from(escape_param(value, escaper).into_owned())],
188        escaper,
189    }
190}
191
192/// A parameter node from a value list, encoded by the given mode's parameter
193/// rules.
194fn param_list(
195    name: &str,
196    values: &[Cow<'_, str>],
197    escaper: VcardEscaper,
198) -> VcardParamNode<'static> {
199    VcardParamNode {
200        name: VcardLeaf::from(name.to_string()),
201        values: values
202            .iter()
203            .map(|v| VcardLeaf::from(escape_param(v, escaper).into_owned()))
204            .collect(),
205        escaper,
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use alloc::{borrow::Cow, string::ToString, vec};
212
213    use crate::{
214        param::VcardParam,
215        tree::{
216            codec::{VcardCodec, mode::VcardEscaper},
217            cst::VcardCst,
218        },
219        value::{n::VcardN, text::VcardText},
220    };
221
222    #[test]
223    fn encodes_a_text_value_escaping_it() {
224        let node = VcardText(Cow::Borrowed("hi, there")).encode(VcardEscaper::V4_0);
225        assert_eq!(node.to_string(), r"hi\, there");
226    }
227
228    #[test]
229    fn encodes_the_structured_n_value_with_all_components() {
230        let n = VcardN {
231            family: vec![Cow::Borrowed("Doe")],
232            ..Default::default()
233        };
234        assert_eq!(n.encode(VcardEscaper::V4_0).to_string(), "Doe;;;;");
235    }
236
237    /// A 2.1 GEO decodes to a coordinate pair, and re-encoding for 2.1 must
238    /// write it back as a comma pair rather than the 3.0 semicolon form. The
239    /// same pair round-trips through 3.0 with a semicolon.
240    #[test]
241    fn encodes_the_geo_pair_in_the_cards_own_version() {
242        let cst = VcardCst::parse("BEGIN:VCARD\r\nVERSION:2.1\r\nGEO:37.0,-122.0\r\nEND:VCARD\r\n")
243            .unwrap();
244        let card = cst.decode();
245        assert!(
246            card.to_string().contains("GEO:37.0,-122.0\r\n"),
247            "{}",
248            card.to_string(),
249        );
250
251        let cst = VcardCst::parse("BEGIN:VCARD\r\nVERSION:3.0\r\nGEO:37.0;-122.0\r\nEND:VCARD\r\n")
252            .unwrap();
253        let card = cst.decode();
254        assert!(
255            card.to_string().contains("GEO:37.0;-122.0\r\n"),
256            "{}",
257            card.to_string(),
258        );
259    }
260
261    /// RFC 6868 section 3.1 read backwards, over the three characters a
262    /// parameter value cannot carry raw.
263    #[test]
264    fn encodes_the_rfc_6868_parameter_sequences() {
265        let param = VcardParam::Label(Cow::Borrowed("a\nb^c\"d"));
266
267        assert_eq!(
268            param.encode(VcardEscaper::V4_0).to_string(),
269            "LABEL=a^nb^^c^'d",
270        );
271    }
272
273    /// The decoded model holds a parameter's content, its RFC 6350 section 3.3
274    /// delimiters excluded, so the pair is put back around a value carrying a
275    /// character a bare SAFE-CHAR run may not hold.
276    #[test]
277    fn quotes_a_parameter_value_carrying_a_delimiter() {
278        let param = VcardParam::Geo(Cow::Borrowed("geo:37.386,-122.083"));
279
280        assert_eq!(
281            param.encode(VcardEscaper::V4_0).to_string(),
282            "GEO=\"geo:37.386,-122.083\"",
283        );
284    }
285
286    #[test]
287    fn writes_a_pre_4_0_parameter_unencoded() {
288        // NOTE: RFC 6868 updates RFC 6350 alone, so a 3.0 caret goes out as
289        // itself.
290        let param = VcardParam::Label(Cow::Borrowed("a^b"));
291
292        assert_eq!(param.encode(VcardEscaper::V3_0).to_string(), "LABEL=a^b");
293    }
294
295    #[test]
296    fn round_trips_a_parameter_byte_for_byte() {
297        let input = concat!(
298            "BEGIN:VCARD\r\n",
299            "VERSION:4.0\r\n",
300            "FN;LANGUAGE=en;GEO=\"geo:37.386,-122.083\"",
301            ";X-PATH=\"C:\\temp\";X-NOTE=a^nb^^c^'d:Ada\r\n",
302            "END:VCARD\r\n",
303        );
304        let cst = VcardCst::parse(input).unwrap();
305
306        assert_eq!(cst.decode().to_string(), input);
307    }
308}