Skip to main content

edifact_primitives/
escape.rs

1//! Release-character escaping of component values.
2//!
3//! Escaping works on `char`s, not bytes. The delimiters are ASCII, so a
4//! multi-byte UTF-8 character can never be one of them; reading a value byte
5//! by byte and pushing each byte back as a `char` re-encodes every byte of
6//! such a character on its own (`ä`, `C3 A4`, becomes `ä`).
7
8use crate::EdifactDelimiters;
9
10impl EdifactDelimiters {
11    /// Whether `c` is one of the characters a value must escape: the element
12    /// and component separators, the segment terminator and the release
13    /// character itself.
14    fn must_escape(&self, c: char) -> bool {
15        c.is_ascii()
16            && [self.element, self.component, self.segment, self.release].contains(&(c as u8))
17    }
18
19    /// Append `value` to `out`, putting the release character before every
20    /// delimiter in it (`a+b` → `a?+b`).
21    pub fn escape_into(&self, value: &str, out: &mut String) {
22        if !value.chars().any(|c| self.must_escape(c)) {
23            out.push_str(value);
24            return;
25        }
26        for c in value.chars() {
27            if self.must_escape(c) {
28                out.push(self.release as char);
29            }
30            out.push(c);
31        }
32    }
33
34    /// `value` with its release characters removed (`a?+b` → `a+b`): each one
35    /// stands for the character after it. A trailing release character with
36    /// nothing after it is kept.
37    pub fn unescape(&self, value: &str) -> String {
38        let release = self.release as char;
39        if !value.contains(release) {
40            return value.to_string();
41        }
42        let mut out = String::with_capacity(value.len());
43        let mut chars = value.chars().peekable();
44        while let Some(c) = chars.next() {
45            if c == release {
46                if let Some(next) = chars.next() {
47                    out.push(next);
48                    continue;
49                }
50            }
51            out.push(c);
52        }
53        out
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    fn escaped(value: &str) -> String {
62        let mut out = String::new();
63        EdifactDelimiters::default().escape_into(value, &mut out);
64        out
65    }
66
67    #[test]
68    fn escape_keeps_multibyte_characters_whole() {
69        assert_eq!(
70            escaped("Zählpunkt? unbekannt + Müller"),
71            "Zählpunkt?? unbekannt ?+ Müller"
72        );
73    }
74
75    #[test]
76    fn escape_leaves_a_plain_value_alone() {
77        assert_eq!(escaped("Müller"), "Müller");
78    }
79
80    #[test]
81    fn escape_marks_every_delimiter() {
82        assert_eq!(escaped("a+b:c'd?e"), "a?+b?:c?'d??e");
83    }
84
85    #[test]
86    fn unescape_keeps_multibyte_characters_whole() {
87        let d = EdifactDelimiters::default();
88        assert_eq!(
89            d.unescape("Zählpunkt?? unbekannt ?+ Müller"),
90            "Zählpunkt? unbekannt + Müller"
91        );
92        assert_eq!(d.unescape("?ä"), "ä");
93    }
94
95    #[test]
96    fn unescape_inverts_escape() {
97        let d = EdifactDelimiters::default();
98        for value in ["", "x", "a+b:c'd?e", "Grüße ?+ € 'ß'", "?"] {
99            assert_eq!(d.unescape(&escaped(value)), value, "{value:?}");
100        }
101    }
102
103    #[test]
104    fn unescape_keeps_a_trailing_release_character() {
105        assert_eq!(EdifactDelimiters::default().unescape("abc?"), "abc?");
106    }
107}