ical/tree/value/
cursor.rs1#[cfg(feature = "encoding")]
26use alloc::string::String;
27use alloc::{borrow::Cow, vec::Vec};
28
29use crate::tree::{line::IcalLine, param::lens::IcalParamLens};
30
31pub struct IcalValueCursor<'c, 'a> {
34 pub line: &'c mut IcalLine<'a>,
36}
37
38impl IcalValueCursor<'_, '_> {
39 pub fn text(&self) -> Cow<'_, str> {
41 self.line.value.decode()
42 }
43
44 pub fn set_text(&mut self, value: impl AsRef<str>) {
48 self.line.value.set(&[value]);
49 }
50
51 pub fn bytes(&self) -> Cow<'_, [u8]> {
58 self.line.value.decode_bytes()
59 }
60
61 pub fn set_bytes(&mut self, value: impl AsRef<[u8]>) {
66 self.line.value.set_bytes(&[value]);
67 }
68
69 #[cfg(feature = "quoted-printable")]
76 pub fn quoted_printable(&self) -> Vec<u8> {
77 let raw = self.bytes();
78
79 if self.line.is_quoted_printable() {
80 quoted_printable::decode(raw.as_ref(), quoted_printable::ParseMode::Robust)
81 .unwrap_or_else(|_| raw.into_owned())
82 } else {
83 raw.into_owned()
84 }
85 }
86
87 #[cfg(feature = "encoding")]
92 pub fn charset(&self) -> String {
93 #[cfg(feature = "quoted-printable")]
94 let bytes = self.quoted_printable();
95 #[cfg(not(feature = "quoted-printable"))]
96 let bytes = self.bytes().into_owned();
97
98 let encoding = self
99 .line
100 .charset_label()
101 .and_then(|label| encoding_rs::Encoding::for_label(label.as_bytes()))
102 .unwrap_or(encoding_rs::UTF_8);
103
104 encoding.decode_without_bom_handling(&bytes).0.into_owned()
105 }
106
107 pub fn list(&self) -> Vec<Cow<'_, str>> {
110 self.line.value.decode_list()
111 }
112
113 pub fn set_list<S: AsRef<str>>(&mut self, values: &[S]) {
115 self.line.value.set(values);
116 }
117
118 pub fn component(&self, i: usize) -> Vec<Cow<'_, str>> {
120 self.line.value.decode_component_list(i)
121 }
122
123 pub fn set_component<S: AsRef<str>>(&mut self, i: usize, values: &[S]) {
125 self.line.value.set_component(i, values);
126 }
127
128 pub fn param<P: IcalParamLens>(&self) -> Option<P::Target<'_>> {
130 self.line.param::<P>()
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use alloc::{
137 format,
138 string::{String, ToString},
139 vec,
140 };
141
142 use crate::{prop::summary::SUMMARY, tree::cst::IcalCst};
143
144 const HEAD: &str = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//x//EN\r\n";
145 const TAIL: &str = "END:VCALENDAR\r\n";
146
147 fn cal(prop_line: &str) -> String {
148 format!("{HEAD}{prop_line}\r\n{TAIL}")
149 }
150
151 #[test]
152 fn edits_a_scalar_value_in_place_escaping_it() {
153 let raw = cal("SUMMARY:Lunch");
154 let mut c = IcalCst::parse(&raw).unwrap();
155 c.prop_mut::<SUMMARY>().unwrap().set_text("Tea, now");
156 assert!(c.to_string().contains("SUMMARY:Tea\\, now\r\n"));
157 }
158
159 #[test]
160 fn keeps_a_newline_written_into_a_vcalendar_1_0_value_on_its_line() {
161 let raw = "BEGIN:VCALENDAR\r\nVERSION:1.0\r\nSUMMARY:x\r\nEND:VCALENDAR\r\n";
164 let mut c = IcalCst::parse(raw.as_bytes()).unwrap();
165 c.prop_mut::<SUMMARY>().unwrap().set_text("a\nb");
166
167 assert!(c.to_string().contains("SUMMARY:a\\nb\r\n"));
168 assert!(IcalCst::parse(&c.to_bytes()).is_ok());
169 }
170
171 #[test]
172 fn writes_and_reads_a_foreign_charset_value_as_raw_bytes() {
173 use crate::prop::description::DESCRIPTION;
174
175 let raw = cal("DESCRIPTION;CHARSET=ISO-8859-1:x");
176 let mut c = IcalCst::parse(&raw).unwrap();
177
178 let latin1 = [b'c', b'a', b'f', 0xE9];
180 c.prop_mut::<DESCRIPTION>().unwrap().set_bytes(latin1);
181
182 assert_eq!(
183 c.prop_mut::<DESCRIPTION>().unwrap().bytes().as_ref(),
184 &latin1,
185 );
186 assert!(c.to_bytes().windows(4).any(|window| window == latin1));
187 }
188
189 #[cfg(feature = "quoted-printable")]
190 #[test]
191 fn quoted_printable_helper_resolves_octets() {
192 use crate::prop::description::DESCRIPTION;
193
194 let raw = cal("DESCRIPTION;CHARSET=ISO-8859-1;ENCODING=QUOTED-PRINTABLE:caf=E9");
195 let mut c = IcalCst::parse(&raw).unwrap();
196
197 assert_eq!(
198 c.prop_mut::<DESCRIPTION>().unwrap().quoted_printable(),
199 [b'c', b'a', b'f', 0xE9],
200 );
201 }
202
203 #[cfg(all(feature = "encoding", feature = "quoted-printable"))]
204 #[test]
205 fn charset_helper_transcodes_to_utf8() {
206 use crate::prop::description::DESCRIPTION;
207
208 let raw = cal("DESCRIPTION;CHARSET=ISO-8859-1;ENCODING=QUOTED-PRINTABLE:caf=E9");
209 let mut c = IcalCst::parse(&raw).unwrap();
210
211 assert_eq!(c.prop_mut::<DESCRIPTION>().unwrap().charset(), "café");
212 }
213
214 #[test]
215 fn edits_one_structured_component_preserving_the_rest() {
216 use crate::prop::geo::GEO;
217
218 let raw = cal("GEO:37.0;-122.0");
219 let mut c = IcalCst::parse(&raw).unwrap();
220 c.prop_mut::<GEO>().unwrap().set_component(1, &["-100.0"]);
221 assert!(c.to_string().contains("GEO:37.0;-100.0\r\n"));
222 }
223
224 #[test]
230 fn reads_and_writes_the_whole_value_not_its_first_component() {
231 use crate::prop::description::DESCRIPTION;
232
233 let raw = cal("DESCRIPTION:a;b");
234 let mut c = IcalCst::parse(&raw).unwrap();
235
236 {
237 let cursor = c.prop_mut::<DESCRIPTION>().unwrap();
238 assert_eq!(cursor.text(), "a;b");
239 assert_eq!(cursor.bytes().as_ref(), b"a;b");
240 assert_eq!(cursor.list(), vec!["a;b"]);
241 }
242
243 let whole = c.prop_mut::<DESCRIPTION>().unwrap().text().into_owned();
244 c.prop_mut::<DESCRIPTION>().unwrap().set_text(&whole);
245
246 assert!(c.to_string().contains("DESCRIPTION:a\\;b\r\n"), "got: {c}");
247 assert_eq!(c.prop_mut::<DESCRIPTION>().unwrap().text(), "a;b");
248 }
249
250 #[test]
252 fn reads_a_structured_component_past_its_first_comma() {
253 use crate::prop::request_status::REQUEST_STATUS;
254
255 let raw = cal("REQUEST-STATUS:2.0;ok;rcpt,two");
256 let c = IcalCst::parse(&raw).unwrap();
257 let status = c.prop::<REQUEST_STATUS>().unwrap();
258
259 assert_eq!(status.description, "ok");
260 assert_eq!(status.extra, "rcpt,two");
261 }
262
263 #[test]
264 fn exercises_every_generic_accessor() {
265 use crate::prop::categories::CATEGORIES;
266
267 let raw = cal("CATEGORIES:a,b");
268 let mut c = IcalCst::parse(&raw).unwrap();
269
270 {
271 let mut cursor = c.prop_mut::<CATEGORIES>().unwrap();
272
273 assert_eq!(cursor.text(), "a,b");
277 assert_eq!(cursor.list(), vec!["a", "b"]);
278 assert_eq!(cursor.component(0), vec!["a", "b"]);
279
280 cursor.set_text("x");
281 assert_eq!(cursor.text(), "x");
282
283 cursor.set_list(&["a", "b"]);
284 assert_eq!(cursor.list(), vec!["a", "b"]);
285
286 cursor.set_component(1, &["y"]);
289 assert_eq!(cursor.component(1), vec!["y"]);
290 }
291
292 assert!(c.to_string().contains("CATEGORIES:a,b;y\r\n"), "got: {c}");
293 }
294}