nextcloud_dav/xml/
escape.rs1pub fn escape_text(value: &str) -> String {
8 let mut escaped = String::with_capacity(value.len());
9 for character in value.chars() {
10 match character {
11 '&' => escaped.push_str("&"),
12 '<' => escaped.push_str("<"),
13 '>' => escaped.push_str(">"),
14 '"' => escaped.push_str("""),
15 '\'' => escaped.push_str("'"),
16 other => escaped.push(other),
17 }
18 }
19 escaped
20}
21
22pub fn unescape_text(value: &str) -> String {
27 let mut decoded = String::with_capacity(value.len());
28 let mut rest = value;
29 while let Some(index) = rest.find('&') {
30 decoded.push_str(&rest[..index]);
31 rest = &rest[index..];
32 match decode_entity(rest) {
33 Some((character, consumed)) => {
34 decoded.push(character);
35 rest = &rest[consumed..];
36 }
37 None => {
38 decoded.push('&');
39 rest = &rest[1..];
40 }
41 }
42 }
43 decoded.push_str(rest);
44 decoded
45}
46
47fn decode_entity(input: &str) -> Option<(char, usize)> {
50 let end = input.find(';')?;
51 if end > 12 {
54 return None;
55 }
56 let body = &input[1..end];
57 let character = match body {
58 "amp" => '&',
59 "lt" => '<',
60 "gt" => '>',
61 "quot" => '"',
62 "apos" => '\'',
63 numeric => char::from_u32(decode_number(numeric)?)?,
64 };
65 Some((character, end + 1))
66}
67
68fn decode_number(body: &str) -> Option<u32> {
69 let (digits, radix) = match body.strip_prefix("#x").or_else(|| body.strip_prefix("#X")) {
70 Some(hex) => (hex, 16),
71 None => (body.strip_prefix('#')?, 10),
72 };
73 u32::from_str_radix(digits, radix).ok()
74}
75
76#[cfg(test)]
77mod tests {
78 use super::{escape_text, unescape_text};
79
80 #[test]
81 fn escapes_every_special_character() {
82 assert_eq!(
83 escape_text("a & b < c > d \" e ' f"),
84 "a & b < c > d " e ' f"
85 );
86 }
87
88 #[test]
89 fn escapes_a_filename_with_ampersand_and_angle_bracket() {
90 assert_eq!(
91 escape_text("Q1 & Q2 <final>.txt"),
92 "Q1 & Q2 <final>.txt"
93 );
94 }
95
96 #[test]
97 fn leaves_ordinary_text_untouched() {
98 assert_eq!(escape_text("Photos/2024"), "Photos/2024");
99 }
100
101 #[test]
102 fn round_trips_escaped_text() {
103 let original = "Tom & Jerry's \"best\" <take>";
104 assert_eq!(unescape_text(&escape_text(original)), original);
105 }
106
107 #[test]
108 fn decodes_numeric_references() {
109 assert_eq!(unescape_text("AB"), "AB");
110 }
111
112 #[test]
113 fn keeps_unknown_entities_verbatim() {
114 assert_eq!(unescape_text("a &unknown; b"), "a &unknown; b");
115 assert_eq!(unescape_text("a & b"), "a & b");
116 }
117}