ics/
util.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
use std::borrow::Cow;

/// Escapes comma, semicolon, backslash and newline character by prepending a
/// backslash. Newlines are normalized to a line feed character.
///
/// This method is only necessary for properties with the value type "TEXT".
///
/// # Example
/// ```
/// use ics::escape_text;
///
/// let line = "Hello, World! Today is a beautiful day to test: Escape Methods.\n Characters like ; or \\ must be escaped.";
/// let expected = "Hello\\, World! Today is a beautiful day to test: Escape Methods.\\n Characters like \\; or \\\\ must be escaped.";
/// assert_eq!(expected, escape_text(line));
pub fn escape_text<'a, S>(input: S) -> Cow<'a, str>
where
    S: Into<Cow<'a, str>>,
{
    let input = input.into();
    let mut escaped_chars_count = 0;
    let mut has_carriage_return_char = false;

    for b in input.bytes() {
        if b == b',' || b == b';' || b == b'\\' || b == b'\n' {
            escaped_chars_count += 1;
        } else if b == b'\r' {
            has_carriage_return_char = true;
        }
    }

    if has_carriage_return_char || escaped_chars_count > 0 {
        let escaped_chars = |c| c == ',' || c == ';' || c == '\\' || c == '\r' || c == '\n';
        let mut output = String::with_capacity(input.len() + escaped_chars_count);
        let mut last_end = 0;
        for (start, part) in input.match_indices(escaped_chars) {
            output.push_str(&input[last_end..start]);
            match part {
                // \r was in old MacOS versions the newline character
                "\r" => {
                    if input.get(start + 1..start + 2) != Some("\n") {
                        output.push_str("\\n");
                    }
                }
                // Newlines needs to be escaped to the literal `\n`
                "\n" => {
                    output.push_str("\\n");
                }
                c => {
                    output.push('\\');
                    output.push_str(c);
                }
            }
            last_end = start + part.len();
        }
        output.push_str(&input[last_end..input.len()]);
        Cow::Owned(output)
    } else {
        input
    }
}

#[cfg(test)]
mod escape_text_tests {
    use super::escape_text;

    #[test]
    fn escaped_chars() {
        let s = ",\r\n;:\\ \r\n\rö\r";
        let expected = "\\,\\n\\;:\\\\ \\n\\nö\\n";
        assert_eq!(expected, escape_text(s));
    }

    #[test]
    fn no_escaped_chars() {
        let s = "This is a simple sentence.";
        let expected = s.clone();
        assert_eq!(expected, escape_text(s));
    }

    // test run with default features enabled but should be correct regardless
    #[test]
    fn escape_property() {
        use crate::components::Property;

        let expected_value = "Hello\\, World! Today is a beautiful day to test: Escape Methods.\\n Characters like \\; or \\\\ must be escaped.\\n";
        let property = Property::new(
            "COMMENT",
            escape_text("Hello, World! Today is a beautiful day to test: Escape Methods.\n Characters like ; or \\ must be escaped.\r\n")
        );
        assert_eq!(expected_value, property.value);
    }
}