1use 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 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 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 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 wire: IcalWire::default(),
128 }
129 }
130}
131
132impl IcalParam<'_> {
133 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
178impl fmt::Display for Ical<'_> {
180 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181 write!(f, "{}", self.encode())
182 }
183}
184
185pub(crate) fn scalar_node(value: &str, escaper: Escaper) -> IcalValueNode<'static> {
188 IcalValueNode::from_components(vec![encode_component(&[value], escaper)], escaper)
189}
190
191pub(crate) fn verbatim_node(value: &str, escaper: Escaper) -> IcalValueNode<'static> {
197 IcalValueNode::from_raw(value.as_bytes().to_vec(), escaper)
198}
199
200pub(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
211pub(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
225fn 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
235fn 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 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 #[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}