1#![forbid(unsafe_code)] #[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
9pub enum OutputCharset {
10 #[default]
12 Utf8,
13 Latin1,
15 Ascii,
19}
20
21impl OutputCharset {
22 #[inline]
23 fn represents(self, c: char) -> bool {
24 match self {
25 OutputCharset::Utf8 => true,
26 OutputCharset::Latin1 => (c as u32) <= 0xFF,
27 OutputCharset::Ascii => (c as u32) <= 0x7F,
28 }
29 }
30}
31
32pub struct XmlBuf {
35 inner: Vec<u8>,
36 charset: OutputCharset,
37}
38
39impl XmlBuf {
40 pub fn new() -> Self {
41 Self { inner: Vec::new(), charset: OutputCharset::Utf8 }
42 }
43
44 pub fn with_capacity(n: usize) -> Self {
45 Self { inner: Vec::with_capacity(n), charset: OutputCharset::Utf8 }
46 }
47
48 pub fn with_charset(n: usize, charset: OutputCharset) -> Self {
51 Self { inner: Vec::with_capacity(n), charset }
52 }
53
54 #[inline]
57 fn push_char_for_charset(&mut self, c: char) {
58 if self.charset.represents(c) {
59 let mut b = [0u8; 4];
60 self.inner.extend_from_slice(c.encode_utf8(&mut b).as_bytes());
61 } else {
62 self.push_str("&#");
64 let mut digits = [0u8; 10];
65 let mut i = digits.len();
66 let mut n = c as u32;
67 loop {
68 i -= 1;
69 digits[i] = b'0' + (n % 10) as u8;
70 n /= 10;
71 if n == 0 { break; }
72 }
73 self.inner.extend_from_slice(&digits[i..]);
74 self.push_byte(b';');
75 }
76 }
77
78 pub fn push_str(&mut self, s: &str) {
79 self.inner.extend_from_slice(s.as_bytes());
80 }
81
82 pub fn push_byte(&mut self, b: u8) {
83 self.inner.push(b);
84 }
85
86 pub fn len(&self) -> usize {
87 self.inner.len()
88 }
89
90 pub fn is_empty(&self) -> bool {
91 self.inner.is_empty()
92 }
93
94 pub fn as_bytes(&self) -> &[u8] {
95 &self.inner
96 }
97
98 pub fn into_bytes(self) -> Vec<u8> {
99 self.inner
100 }
101
102 pub fn into_string(self) -> String {
104 String::from_utf8(self.inner).expect("XmlBuf is always valid UTF-8")
105 }
106
107 pub fn push_escaped_text(&mut self, s: &str) {
119 for c in s.chars() {
120 match c {
121 '&' => self.push_str("&"),
122 '<' => self.push_str("<"),
123 '>' => self.push_str(">"),
124 '\u{D}' => self.push_str("
"),
125 c => self.push_char_for_charset(c),
126 }
127 }
128 }
129
130 pub fn push_escaped_attr(&mut self, s: &str) {
138 for c in s.chars() {
139 match c {
140 '&' => self.push_str("&"),
141 '<' => self.push_str("<"),
142 '"' => self.push_str("""),
143 '\u{9}' => self.push_str("	"),
144 '\u{A}' => self.push_str("
"),
145 '\u{D}' => self.push_str("
"),
146 c => self.push_char_for_charset(c),
147 }
148 }
149 }
150}
151
152impl Default for XmlBuf {
153 fn default() -> Self {
154 Self::new()
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 #[test]
163 fn escape_text() {
164 let mut b = XmlBuf::new();
165 b.push_escaped_text("a & b < c > d");
166 assert_eq!(b.into_string(), "a & b < c > d");
167 }
168
169 #[test]
170 fn escape_attr() {
171 let mut b = XmlBuf::new();
172 b.push_escaped_attr(r#"say "hello" & <bye>"#);
173 assert_eq!(b.into_string(), r#"say "hello" & <bye>"#);
175 }
176
177 #[test]
178 fn unicode_passthrough() {
179 let mut b = XmlBuf::new();
180 b.push_escaped_text("日本語 🦀");
181 assert_eq!(b.into_string(), "日本語 🦀");
182 }
183
184 #[test]
185 fn new_is_empty() {
186 let b = XmlBuf::new();
187 assert!(b.is_empty());
188 assert_eq!(b.len(), 0);
189 assert_eq!(b.as_bytes(), &[] as &[u8]);
190 }
191
192 #[test]
193 fn default_matches_new() {
194 let b = XmlBuf::default();
195 assert!(b.is_empty());
196 }
197
198 #[test]
199 fn with_capacity_reserves_but_is_empty() {
200 let b = XmlBuf::with_capacity(64);
201 assert!(b.is_empty());
202 assert_eq!(b.len(), 0);
203 }
204
205 #[test]
206 fn push_str_and_len() {
207 let mut b = XmlBuf::new();
208 b.push_str("hi");
209 assert_eq!(b.len(), 2);
210 assert!(!b.is_empty());
211 assert_eq!(b.as_bytes(), b"hi");
212 }
213
214 #[test]
215 fn push_byte_appends_one_byte() {
216 let mut b = XmlBuf::new();
217 b.push_byte(b'x');
218 b.push_byte(b'y');
219 assert_eq!(b.as_bytes(), b"xy");
220 }
221
222 #[test]
223 fn into_bytes_returns_inner() {
224 let mut b = XmlBuf::new();
225 b.push_str("abc");
226 assert_eq!(b.into_bytes(), b"abc".to_vec());
227 }
228}