Skip to main content

ical/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 [`Codec`] impl encodes it into a [`IcalValueNode`], an
7//! [`IcalParam`] into an [`IcalParamNode`], a [`IcalProp`] into an
8//! [`IcalLine`], an [`IcalComponent`] into a nested [`IcalCst`], and an
9//! [`Ical`] into the whole `VCALENDAR` [`IcalCst`], recursively.
10//!
11//! The whole calendar is encoded for its version's [`Escaper`], which the
12//! value codecs use to escape every leaf.
13//!
14//! [`Display`](core::fmt::Display) for [`Ical`] renders a decoded calendar
15//! straight to its serialized bytes through here.
16
17use core::fmt;
18
19use alloc::{borrow::Cow, boxed::Box, string::ToString, vec, vec::Vec};
20
21use crate::{
22    component::IcalComponent,
23    ical::Ical,
24    param::IcalParam,
25    prop::IcalProp,
26    tree::{
27        codec::{
28            Codec,
29            escape::{escape_param, escape_with},
30            mode::Escaper,
31        },
32        cst::{IcalCst, IcalItem},
33        leaf::{IcalLeaf, IcalValueLeaf},
34        line::IcalLine,
35        param::node::IcalParamNode,
36        value::node::IcalValueNode,
37        wire::IcalWire,
38    },
39    validator::IcalValid,
40};
41
42impl Ical<'_> {
43    /// Encode the whole calendar into a `VCALENDAR` CST for its version's
44    /// escaping mode. `VERSION` is emitted as the first property.
45    pub fn encode(&self) -> IcalCst<'static> {
46        let escaper = Escaper::for_version(self.version);
47
48        let mut items = Vec::with_capacity(1 + self.props.len() + self.components.len());
49        items.push(IcalItem::Prop(IcalLine::text(
50            "VERSION",
51            self.version.to_string(),
52        )));
53        items.extend(
54            self.props
55                .iter()
56                .map(|prop| IcalItem::Prop(prop.encode(escaper))),
57        );
58        items.extend(
59            self.components
60                .iter()
61                .map(|component| IcalItem::Component(Box::new(component.encode(escaper)))),
62        );
63
64        IcalCst {
65            begin: Some(IcalLine::text("BEGIN", "VCALENDAR")),
66            items,
67            end: Some(IcalLine::text("END", "VCALENDAR")),
68            trailing: Cow::Borrowed(""),
69        }
70    }
71}
72
73impl IcalComponent<'_> {
74    /// Encode this component (and its nested components) into a CST for the
75    /// given escaping mode.
76    pub fn encode(&self, escaper: Escaper) -> IcalCst<'static> {
77        let name = self.name.to_string();
78
79        let mut items = Vec::with_capacity(self.props.len() + self.components.len());
80        items.extend(
81            self.props
82                .iter()
83                .map(|prop| IcalItem::Prop(prop.encode(escaper))),
84        );
85        items.extend(
86            self.components
87                .iter()
88                .map(|component| IcalItem::Component(Box::new(component.encode(escaper)))),
89        );
90
91        IcalCst {
92            begin: Some(IcalLine::text("BEGIN", name.clone())),
93            items,
94            end: Some(IcalLine::text("END", name)),
95            trailing: Cow::Borrowed(""),
96        }
97    }
98}
99
100impl<'a> From<Ical<'a>> for IcalCst<'static> {
101    fn from(cal: Ical<'a>) -> Self {
102        cal.encode()
103    }
104}
105
106impl<'a> From<IcalValid<Ical<'a>>> for IcalCst<'static> {
107    fn from(valid: IcalValid<Ical<'a>>) -> Self {
108        valid.into_inner().encode()
109    }
110}
111
112impl IcalProp<'_> {
113    /// Encode the property into a raw content line for the given escaping mode,
114    /// dispatching on its value.
115    pub fn encode(&self, escaper: Escaper) -> IcalLine<'static> {
116        IcalLine {
117            name: IcalLeaf::from(self.name.to_string()),
118            params: self
119                .params
120                .iter()
121                .map(|param| param.encode(escaper))
122                .collect(),
123            value: self.value.encode(escaper),
124            eol: IcalLeaf::from("\r\n".to_string()),
125            // NOTE: An encoded property has no wire history: it is written out
126            // unfolded, in canonical form.
127            wire: IcalWire::default(),
128        }
129    }
130}
131
132impl IcalParam<'_> {
133    /// Encode the parameter into a raw parameter node for the given escaping
134    /// mode, dispatching on its kind.
135    pub fn encode(&self, escaper: Escaper) -> IcalParamNode<'static> {
136        use crate::param::IcalParamKind::*;
137
138        match self {
139            IcalParam::AltRep(v) => param_scalar(&AltRep, v, escaper),
140            IcalParam::Cn(v) => param_scalar(&Cn, v, escaper),
141            IcalParam::CuType(v) => param_scalar(&CuType, v, escaper),
142            IcalParam::DelegatedFrom(vs) => param_list(&DelegatedFrom, vs, escaper),
143            IcalParam::DelegatedTo(vs) => param_list(&DelegatedTo, vs, escaper),
144            IcalParam::Dir(v) => param_scalar(&Dir, v, escaper),
145            IcalParam::Encoding(v) => param_scalar(&Encoding, v, escaper),
146            IcalParam::FmtType(v) => param_scalar(&FmtType, v, escaper),
147            IcalParam::FbType(v) => param_scalar(&FbType, v, escaper),
148            IcalParam::Language(v) => param_scalar(&Language, v, escaper),
149            IcalParam::Member(vs) => param_list(&Member, vs, escaper),
150            IcalParam::PartStat(v) => param_scalar(&PartStat, v, escaper),
151            IcalParam::Range(v) => param_scalar(&Range, v, escaper),
152            IcalParam::Related(v) => param_scalar(&Related, v, escaper),
153            IcalParam::RelType(v) => param_scalar(&RelType, v, escaper),
154            IcalParam::Role(v) => param_scalar(&Role, v, escaper),
155            IcalParam::Rsvp(v) => param_scalar(&Rsvp, v, escaper),
156            IcalParam::SentBy(v) => param_scalar(&SentBy, v, escaper),
157            IcalParam::TzId(v) => param_scalar(&TzId, v, escaper),
158            IcalParam::Value(v) => param_scalar(&Value, v, escaper),
159            IcalParam::Display(v) => param_scalar(&Display, v, escaper),
160            IcalParam::Email(v) => param_scalar(&Email, v, escaper),
161            IcalParam::Feature(vs) => param_list(&Feature, vs, escaper),
162            IcalParam::Label(v) => param_scalar(&Label, v, escaper),
163            IcalParam::Order(v) => param_scalar(&Order, v, escaper),
164            IcalParam::Schema(v) => param_scalar(&Schema, v, escaper),
165            IcalParam::Derived(v) => param_scalar(&Derived, v, escaper),
166            IcalParam::ScheduleAgent(v) => param_scalar(&ScheduleAgent, v, escaper),
167            IcalParam::ScheduleForceSend(v) => param_scalar(&ScheduleForceSend, v, escaper),
168            IcalParam::ScheduleStatus(v) => param_scalar(&ScheduleStatus, v, escaper),
169            IcalParam::LinkRel(v) => param_scalar(&LinkRel, v, escaper),
170            IcalParam::Gap(v) => param_scalar(&Gap, v, escaper),
171            IcalParam::Charset(v) => param_scalar(&Charset, v, escaper),
172
173            IcalParam::Unknown { name, values } => param_list(name, values, escaper),
174        }
175    }
176}
177
178/// Serialize the decoded calendar by encoding it into a CST (canonical).
179impl fmt::Display for Ical<'_> {
180    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181        write!(f, "{}", self.encode())
182    }
183}
184
185/// A one-component, one-value syntax node, escaping the value by the given
186/// mode.
187pub(crate) fn scalar_node(value: &str, escaper: Escaper) -> IcalValueNode<'static> {
188    IcalValueNode::from_components(vec![encode_component(&[value], escaper)], escaper)
189}
190
191/// Own one value exactly as given, with no escaping at all.
192///
193/// A URI is not text: RFC 5545 section 3.3.13 gives it no escapes, so escaping
194/// its `;` or `,` on the way out would rewrite the reference the value is, and
195/// a value that decoded whole would not survive its own round trip.
196pub(crate) fn verbatim_node(value: &str, escaper: Escaper) -> IcalValueNode<'static> {
197    IcalValueNode::from_raw(value.as_bytes().to_vec(), escaper)
198}
199
200/// Escape and own a clean value list into one component, by escaping mode.
201pub(crate) fn encode_component<S: AsRef<str>>(
202    values: &[S],
203    escaper: Escaper,
204) -> Vec<IcalValueLeaf<'static>> {
205    values
206        .iter()
207        .map(|v| IcalValueLeaf::from(escape_with(v.as_ref().as_bytes(), escaper).into_owned()))
208        .collect()
209}
210
211/// Escape and own raw value bytes into one component, by escaping mode.
212///
213/// The foreign-charset escape hatch: only the structural separators are
214/// escaped, every other byte going out exactly as given.
215pub(crate) fn encode_bytes_component<B: AsRef<[u8]>>(
216    values: &[B],
217    escaper: Escaper,
218) -> Vec<IcalValueLeaf<'static>> {
219    values
220        .iter()
221        .map(|v| IcalValueLeaf::from(escape_with(v.as_ref(), escaper).into_owned()))
222        .collect()
223}
224
225/// A parameter node from a single value, encoded by the given mode's parameter
226/// rules.
227fn param_scalar(name: &str, value: &str, escaper: Escaper) -> IcalParamNode<'static> {
228    IcalParamNode {
229        name: IcalLeaf::from(name.to_string()),
230        values: vec![IcalLeaf::from(escape_param(value, escaper).into_owned())],
231        escaper,
232    }
233}
234
235/// A parameter node from a value list, encoded by the given mode's parameter
236/// rules.
237fn param_list(name: &str, values: &[Cow<'_, str>], escaper: Escaper) -> IcalParamNode<'static> {
238    IcalParamNode {
239        name: IcalLeaf::from(name.to_string()),
240        values: values
241            .iter()
242            .map(|v| IcalLeaf::from(escape_param(v, escaper).into_owned()))
243            .collect(),
244        escaper,
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use alloc::{borrow::Cow, string::ToString};
251
252    use crate::{
253        param::IcalParam,
254        tree::{
255            codec::{Codec, mode::Escaper},
256            cst::IcalCst,
257        },
258        value::text::IcalText,
259    };
260
261    #[test]
262    fn encodes_a_text_value_escaping_it() {
263        let node = IcalText(Cow::Borrowed("hi, there")).encode(Escaper::Modern);
264        assert_eq!(node.to_string(), r"hi\, there");
265    }
266
267    #[test]
268    fn round_trips_a_decoded_calendar_back_to_bytes() {
269        let input = concat!(
270            "BEGIN:VCALENDAR\r\n",
271            "VERSION:2.0\r\n",
272            "PRODID:-//x//EN\r\n",
273            "BEGIN:VEVENT\r\n",
274            "UID:1\r\n",
275            "DTSTAMP:20260101T000000Z\r\n",
276            "SUMMARY:Lunch\r\n",
277            "END:VEVENT\r\n",
278            "END:VCALENDAR\r\n",
279        );
280        let cst = IcalCst::parse(input).unwrap();
281        let cal = cst.decode();
282        assert_eq!(cal.to_string(), input);
283    }
284
285    #[test]
286    fn encodes_the_rfc_6868_parameter_sequences() {
287        // NOTE: RFC 6868 section 3.1 read backwards, over the three characters
288        // a parameter value cannot carry raw.
289        let param = IcalParam::Cn(Cow::Borrowed("a\nb^c\"d"));
290
291        assert_eq!(param.encode(Escaper::Modern).to_string(), "CN=a^nb^^c^'d",);
292    }
293
294    /// The decoded model holds a parameter's content, its RFC 5545 section 3.1
295    /// delimiters excluded, so the pair is put back around a value carrying a
296    /// character a bare `paramtext` may not hold.
297    #[test]
298    fn quotes_a_parameter_value_carrying_a_delimiter() {
299        let param = IcalParam::AltRep(Cow::Borrowed("cid:part1.0001@example.org"));
300
301        assert_eq!(
302            param.encode(Escaper::Modern).to_string(),
303            "ALTREP=\"cid:part1.0001@example.org\"",
304        );
305    }
306
307    #[test]
308    fn round_trips_a_parameter_byte_for_byte() {
309        let input = concat!(
310            "BEGIN:VCALENDAR\r\n",
311            "VERSION:2.0\r\n",
312            "BEGIN:VEVENT\r\n",
313            "UID:1\r\n",
314            "DTSTAMP:20260101T000000Z\r\n",
315            "SUMMARY;LANGUAGE=en;ALTREP=\"cid:part1.0001@example.org\"",
316            ";X-PATH=\"C:\\temp\";X-NOTE=a^nb^^c^'d:Lunch\r\n",
317            "END:VEVENT\r\n",
318            "END:VCALENDAR\r\n",
319        );
320        let cst = IcalCst::parse(input).unwrap();
321
322        assert_eq!(cst.decode().to_string(), input);
323    }
324}