content_line_writer/
lib.rs1#![deny(clippy::pedantic)]
5#![deny(clippy::unwrap_used)]
6#![warn(missing_docs)]
7
8use std::io::Write;
39
40use validate::{is_valid_name, is_valid_param_value, is_valid_value};
41
42pub mod error;
43mod validate;
44
45#[derive(Debug)]
54pub struct ContentLineWriter<W: Write> {
55 output: W,
56 line: usize,
58}
59
60impl<W: Write> ContentLineWriter<W> {
61 pub fn new(output: W) -> ContentLineWriter<W> {
63 ContentLineWriter { output, line: 0 }
64 }
65
66 pub fn start_line(mut self, name: &str) -> std::io::Result<LineWithName<W>> {
75 is_valid_name(name)?;
76
77 self.write_folding(name)?;
78
79 Ok(LineWithName(self))
80 }
81
82 fn write_value(mut self, value: &str) -> std::io::Result<Self> {
86 is_valid_value(value)?;
87
88 self.write_folding(":")?;
89 self.write_folding(value)?;
90 self.output.write_all(b"\r\n")?;
91
92 self.line = 0;
93 Ok(self)
94 }
95
96 fn write_param(&mut self, param_name: &str, param_value: &str) -> std::io::Result<()> {
97 is_valid_name(param_name)?;
98 is_valid_param_value(param_value)?;
99
100 self.write_folding(";")?;
101 self.write_folding(param_name)?;
102 self.write_folding("=")?;
103 self.write_folding(param_value)?;
105 Ok(())
106 }
107
108 #[inline]
110 fn write_folding(&mut self, data: &str) -> std::io::Result<()> {
111 for c in data.chars() {
112 self.write_folding_char(c)?;
113 self.line += 1;
114 }
115 Ok(())
116 }
117
118 fn write_folding_char(&mut self, c: char) -> std::io::Result<()> {
120 if self.line + c.len_utf8() >= 75 {
121 self.output.write_all(b"\r\n ")?;
123 self.line = 1;
124 }
125 let mut buf = [0u8; 4];
126 let encoded = c.encode_utf8(&mut buf);
127 self.output.write_all(encoded.as_bytes())?;
128 Ok(())
129 }
130
131 pub fn into_inner(self) -> W {
133 self.output
134 }
135}
136
137impl<W: std::io::Write> From<W> for ContentLineWriter<W> {
138 fn from(output: W) -> Self {
139 ContentLineWriter { output, line: 0 }
140 }
141}
142
143#[derive(Debug)]
147pub struct LineWithName<W: std::io::Write>(ContentLineWriter<W>);
148
149impl<W: std::io::Write> LineWithName<W> {
150 pub fn with_param(
156 mut self,
157 param_name: &str,
158 param_value: &str,
159 ) -> std::io::Result<LineWithParam<W>> {
160 self.0.write_param(param_name, param_value)?;
161 Ok(LineWithParam(self.0))
162 }
163
164 pub fn with_params<'p>(
170 mut self,
171 params: impl Iterator<Item = (&'p str, &'p str)>,
172 ) -> std::io::Result<LineWithParam<W>> {
173 for (name, value) in params {
174 self.0.write_param(name, value)?;
175 }
176 Ok(LineWithParam(self.0))
177 }
178
179 #[inline]
187 pub fn value(self, value: &str) -> std::io::Result<ContentLineWriter<W>> {
188 self.0.write_value(value)
189 }
190
191 #[inline]
217 pub fn without_params(self) -> LineWantValue<W> {
218 LineWantValue(self.0)
219 }
220}
221
222#[derive(Debug)]
224pub struct LineWithParam<W: std::io::Write>(ContentLineWriter<W>);
225
226impl<W: std::io::Write> LineWithParam<W> {
227 pub fn add_param_value(mut self, param_value: &str) -> std::io::Result<LineWithParam<W>> {
235 is_valid_param_value(param_value)?;
236 self.0.write_folding(",")?;
237 self.0.write_folding(param_value)?;
238 Ok(self)
239 }
240
241 pub fn with_param(
249 mut self,
250 param_name: &str,
251 param_value: &str,
252 ) -> std::io::Result<LineWithParam<W>> {
253 self.0.write_param(param_name, param_value)?;
254 Ok(self)
255 }
256
257 #[inline]
265 pub fn value(self, value: &str) -> std::io::Result<ContentLineWriter<W>> {
266 self.0.write_value(value)
267 }
268
269 #[inline]
275 pub fn end_params(self) -> LineWantValue<W> {
276 LineWantValue(self.0)
277 }
278}
279
280#[derive(Debug)]
282pub struct LineWantValue<W: std::io::Write>(ContentLineWriter<W>);
283
284impl<W: std::io::Write> LineWantValue<W> {
285 #[inline]
293 pub fn value(self, value: &str) -> std::io::Result<ContentLineWriter<W>> {
294 self.0.write_value(value)
295 }
296}
297
298#[cfg(test)]
302mod tests {
303 use crate::{
304 error::{NameError, ParamValueError},
305 ContentLineWriter,
306 };
307
308 #[test]
309 fn test_simple_case() {
310 let mut output = Vec::<u8>::new();
311 ContentLineWriter::new(&mut output)
312 .start_line("BEGIN")
313 .unwrap()
314 .value("VEVENT")
315 .unwrap();
316 let s = String::from_utf8(output).unwrap();
317 assert_eq!(s, "BEGIN:VEVENT\r\n");
318 }
319
320 #[test]
321 fn test_write_line_empty() {
322 let mut output = Vec::new();
323 let err = ContentLineWriter::new(&mut output)
324 .start_line("")
325 .unwrap_err();
326 assert_eq!(err.downcast::<NameError>().unwrap(), NameError::InvalidName);
327 }
328
329 #[test]
330 fn test_write_empty_value() {
331 let mut output = Vec::new();
332 ContentLineWriter::new(&mut output)
333 .start_line("NAME")
334 .unwrap()
335 .value("")
336 .unwrap();
337 let s = String::from_utf8(output).unwrap();
338 assert_eq!(s, "NAME:\r\n");
339 }
340
341 #[test]
342 fn test_write_invalid_vendor_id() {
343 let mut output = Vec::new();
344 let err = ContentLineWriter::new(&mut output)
345 .start_line("X-h-u")
346 .unwrap_err();
347 assert_eq!(
348 err.downcast::<NameError>().unwrap(),
349 NameError::InvalidVendorIdLength(1)
350 );
351 }
352
353 #[test]
354 fn test_write_valid_vendor_id() {
355 let mut output = Vec::new();
356 ContentLineWriter::new(&mut output)
357 .start_line("X-pimutils-test")
358 .unwrap()
359 .value("something")
360 .unwrap();
361 let s = String::from_utf8(output).unwrap();
362 assert_eq!(s, "X-pimutils-test:something\r\n");
363 }
364
365 #[test]
366 fn test_write_line_single_param_empty_value() {
367 let mut output = Vec::new();
368 ContentLineWriter::new(&mut output)
369 .start_line("name")
370 .unwrap()
371 .with_param("key", "")
372 .unwrap()
373 .value("value")
374 .unwrap();
375 dbg!(std::str::from_utf8(&output).unwrap());
376 assert_eq!(output, b"name;key=:value\r\n");
377 }
378
379 #[test]
380 fn test_write_line_single_param() {
381 let mut output = Vec::new();
382 ContentLineWriter::new(&mut output)
383 .start_line("name")
384 .unwrap()
385 .with_param("key", "value")
386 .unwrap()
387 .value("value")
388 .unwrap();
389 assert_eq!(output, b"name;key=value:value\r\n");
390 }
391
392 #[test]
393 fn test_write_line_multiple_params() {
394 let mut output = Vec::new();
395 ContentLineWriter::new(&mut output)
396 .start_line("name")
397 .unwrap()
398 .with_param("key1", "value1")
399 .unwrap()
400 .with_param("key2", "value2")
401 .unwrap()
402 .value("value")
403 .unwrap();
404 let s = String::from_utf8(output).unwrap();
405 assert_eq!(s, "name;key1=value1;key2=value2:value\r\n");
406 }
407
408 #[test]
409 fn test_write_line_with_parms() {
410 let params = vec![("key1", "value1"), ("key2", "value2")];
411 let mut output = Vec::new();
412 ContentLineWriter::new(&mut output)
413 .start_line("name")
414 .unwrap()
415 .with_params(params.into_iter())
416 .unwrap()
417 .value("value")
418 .unwrap();
419 let s = String::from_utf8(output).unwrap();
420 assert_eq!(s, "name;key1=value1;key2=value2:value\r\n");
421 }
422
423 #[test]
424 fn test_write_line_single_param_multiple_values() {
425 let mut output = Vec::new();
426 ContentLineWriter::new(&mut output)
427 .start_line("name")
428 .unwrap()
429 .with_param("key1", "value1")
430 .unwrap()
431 .add_param_value("value2")
432 .unwrap()
433 .value("value")
434 .unwrap();
435 let s = String::from_utf8(output).unwrap();
436 assert_eq!(s, "name;key1=value1,value2:value\r\n");
437 }
438
439 #[test]
440 fn test_write_line_quoted_value() {
441 let mut output = Vec::new();
442 let generator = ContentLineWriter::new(&mut output);
443 generator
444 .start_line("name")
445 .unwrap()
446 .value("\"quoted value\"")
447 .unwrap();
448 assert_eq!(output, b"name:\"quoted value\"\r\n");
449 }
450
451 #[test]
452 fn test_write_line_single_param_quoted() {
453 let mut output = Vec::new();
454 let generator = ContentLineWriter::new(&mut output);
455 generator
456 .start_line("name")
457 .unwrap()
458 .with_param("key", "\"quoted value\"")
459 .unwrap()
460 .value("value")
461 .unwrap();
462 assert_eq!(output, b"name;key=\"quoted value\":value\r\n");
463 }
464
465 #[test]
466 fn test_write_line_single_param_invalid_quoted() {
467 let mut output = Vec::new();
468 let generator = ContentLineWriter::new(&mut output);
469 let err = generator
470 .start_line("name")
471 .unwrap()
472 .with_param("key", "\"invalid quoted value")
473 .unwrap_err();
474 assert_eq!(
475 err.downcast::<ParamValueError>().unwrap(),
476 ParamValueError::MissingClosingQuote
477 );
478 }
479
480 #[test]
481 fn test_write_line_folding() {
482 let mut output = Vec::new();
483 let generator = ContentLineWriter::new(&mut output);
484 let value =
485 "This is a very long line, which in fact has over 75 characters and needs to be folded";
486 generator
487 .start_line("DESCRIPTION")
488 .unwrap()
489 .value(value)
490 .unwrap();
491 let s = String::from_utf8(output).unwrap();
492 let expected = concat!(
493 "DESCRIPTION:This is a very long line, which in fact has over 75 characters\r\n",
494 " and needs to be folded\r\n"
495 );
496 assert_eq!(s, expected);
497 }
498
499 #[test]
500 fn test_write_line_escaped_newline() {
501 let mut output = Vec::new();
502 let generator = ContentLineWriter::new(&mut output);
503 generator
504 .start_line("DESCRIPTION")
505 .unwrap()
506 .value("Test with\\nnewline")
507 .unwrap();
508 let s = String::from_utf8(output).unwrap();
509 assert_eq!(s, "DESCRIPTION:Test with\\nnewline\r\n");
510 }
511
512 #[test]
513 #[ignore = "We don't handle escaping, this needs an 'unchecked' API."]
514 fn test_write_line_escape_at_eol() {
515 let mut output = Vec::new();
516 let generator = ContentLineWriter::new(&mut output);
517 let value = format!("{}\\nyy", "x".repeat(67));
518 generator
519 .start_line("BEGIN")
520 .unwrap()
521 .value(&value)
522 .unwrap();
523 let s = String::from_utf8(output).unwrap();
524 let expected = concat!(
525 "BEGIN:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\r\n",
526 " yy\r\n"
527 );
528 assert_eq!(s, expected);
529 }
530}