1use std::fmt;
49
50use crate::{css, html, javascript, json, rust, sql, uri, xml};
51
52macro_rules! display_fn {
53 (
54 $(#[$meta:meta])*
55 $name:ident => $module:ident :: $write_fn:ident
56 ) => {
57 $(#[$meta])*
58 pub fn $name(input: &str) -> impl fmt::Display + '_ {
59 struct W<'a>(&'a str);
60 impl fmt::Display for W<'_> {
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 if f.width().is_none() && f.precision().is_none() {
63 return $module::$write_fn(f, self.0);
64 }
65 let mut encoded = String::with_capacity(self.0.len());
66 $module::$write_fn(&mut encoded, self.0)?;
67 f.pad(&encoded)
68 }
69 }
70 W(input)
71 }
72 };
73}
74
75display_fn! {
76 display_html => html::write_html
78}
79
80display_fn! {
81 display_html_content => html::write_html_content
83}
84
85display_fn! {
86 display_html_attribute => html::write_html_attribute
88}
89
90display_fn! {
91 display_html_unquoted_attribute => html::write_html_unquoted_attribute
94}
95
96display_fn! {
97 display_xml => xml::write_xml
99}
100
101display_fn! {
102 display_xml_content => xml::write_xml_content
104}
105
106display_fn! {
107 display_xml_attribute => xml::write_xml_attribute
109}
110
111display_fn! {
112 display_xml_comment => xml::write_xml_comment
114}
115
116display_fn! {
117 display_cdata => xml::write_cdata
119}
120
121display_fn! {
122 display_xml11 => xml::write_xml11
124}
125
126display_fn! {
127 display_xml11_content => xml::write_xml11_content
129}
130
131display_fn! {
132 display_xml11_attribute => xml::write_xml11_attribute
134}
135
136display_fn! {
137 display_javascript => javascript::write_javascript
139}
140
141display_fn! {
142 display_javascript_attribute => javascript::write_javascript_attribute
145}
146
147display_fn! {
148 display_javascript_block => javascript::write_javascript_block
151}
152
153display_fn! {
154 display_javascript_source => javascript::write_javascript_source
157}
158
159display_fn! {
160 display_js_template => javascript::write_js_template
162}
163
164display_fn! {
165 display_css_string => css::write_css_string
167}
168
169display_fn! {
170 display_css_url => css::write_css_url
172}
173
174display_fn! {
175 display_uri_component => uri::write_uri_component
177}
178
179display_fn! {
180 display_uri_path => uri::write_uri_path
182}
183
184display_fn! {
185 display_form_urlencoded => uri::write_form_urlencoded
188}
189
190display_fn! {
191 display_json => json::write_json
193}
194
195display_fn! {
196 display_rust_string => rust::write_rust_string
198}
199
200display_fn! {
201 display_rust_char => rust::write_rust_char
203}
204
205display_fn! {
206 display_rust_byte_string => rust::write_rust_byte_string
209}
210
211display_fn! {
212 display_sql => sql::write_sql
214}
215
216display_fn! {
217 display_sql_backslash => sql::write_sql_backslash
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 macro_rules! assert_fmt_parity {
228 ($fmt:literal, $display_fn:ident, $input:expr, $encoded:expr) => {
229 assert_eq!(
230 format!($fmt, $display_fn($input)),
231 format!($fmt, $encoded),
232 "mismatch for {:?} formatted as {:?} on {}",
233 $input,
234 $fmt,
235 stringify!($display_fn),
236 );
237 };
238 }
239
240 macro_rules! display_matches_for {
241 ($name:ident, $display_fn:ident, $for_fn:path) => {
242 #[test]
243 fn $name() {
244 for input in [
245 "",
246 "hello",
247 "<script>alert('xss')</script>",
248 "café",
249 "世界",
250 "😀",
251 "a&b<c>d\"e'f",
252 "\x00\x01\x1F\x7F",
253 "\t\n\r",
254 "\u{0080}\u{009F}",
255 "\u{2028}\u{2029}",
256 "a\\b/c",
257 "key=val&foo=bar",
258 "`${inject}`",
259 ] {
260 let encoded = $for_fn(input);
261 assert_fmt_parity!("{}", $display_fn, input, encoded);
262 assert_fmt_parity!("{:>12}", $display_fn, input, encoded);
263 assert_fmt_parity!("{:<12}", $display_fn, input, encoded);
264 assert_fmt_parity!("{:*^12}", $display_fn, input, encoded);
265 assert_fmt_parity!("{:.0}", $display_fn, input, encoded);
266 assert_fmt_parity!("{:.4}", $display_fn, input, encoded);
267 assert_fmt_parity!("{:.99}", $display_fn, input, encoded);
268 assert_fmt_parity!("{:*>20.7}", $display_fn, input, encoded);
269 }
270 }
271 };
272 }
273
274 display_matches_for!(html, display_html, crate::for_html);
276 display_matches_for!(html_content, display_html_content, crate::for_html_content);
277 display_matches_for!(
278 html_attribute,
279 display_html_attribute,
280 crate::for_html_attribute
281 );
282 display_matches_for!(
283 html_unquoted_attribute,
284 display_html_unquoted_attribute,
285 crate::for_html_unquoted_attribute
286 );
287
288 display_matches_for!(xml, display_xml, crate::for_xml);
290 display_matches_for!(xml_content, display_xml_content, crate::for_xml_content);
291 display_matches_for!(
292 xml_attribute,
293 display_xml_attribute,
294 crate::for_xml_attribute
295 );
296 display_matches_for!(xml_comment, display_xml_comment, crate::for_xml_comment);
297 display_matches_for!(cdata, display_cdata, crate::for_cdata);
298 display_matches_for!(xml11, display_xml11, crate::for_xml11);
299 display_matches_for!(
300 xml11_content,
301 display_xml11_content,
302 crate::for_xml11_content
303 );
304 display_matches_for!(
305 xml11_attribute,
306 display_xml11_attribute,
307 crate::for_xml11_attribute
308 );
309
310 display_matches_for!(javascript, display_javascript, crate::for_javascript);
312 display_matches_for!(
313 javascript_attribute,
314 display_javascript_attribute,
315 crate::for_javascript_attribute
316 );
317 display_matches_for!(
318 javascript_block,
319 display_javascript_block,
320 crate::for_javascript_block
321 );
322 display_matches_for!(
323 javascript_source,
324 display_javascript_source,
325 crate::for_javascript_source
326 );
327 display_matches_for!(js_template, display_js_template, crate::for_js_template);
328
329 display_matches_for!(css_string, display_css_string, crate::for_css_string);
331 display_matches_for!(css_url, display_css_url, crate::for_css_url);
332
333 display_matches_for!(
335 uri_component,
336 display_uri_component,
337 crate::for_uri_component
338 );
339 display_matches_for!(uri_path, display_uri_path, crate::for_uri_path);
340 display_matches_for!(
341 form_urlencoded,
342 display_form_urlencoded,
343 crate::for_form_urlencoded
344 );
345
346 display_matches_for!(json, display_json, crate::for_json);
348
349 display_matches_for!(rust_string, display_rust_string, crate::for_rust_string);
351 display_matches_for!(rust_char, display_rust_char, crate::for_rust_char);
352 display_matches_for!(
353 rust_byte_string,
354 display_rust_byte_string,
355 crate::for_rust_byte_string
356 );
357
358 display_matches_for!(sql, display_sql, crate::for_sql);
360 display_matches_for!(
361 sql_backslash,
362 display_sql_backslash,
363 crate::for_sql_backslash
364 );
365
366 #[test]
369 fn inline_format_html() {
370 let input = "<b>bold</b>";
371 let result = format!("<p>{}</p>", display_html(input));
372 assert_eq!(result, "<p><b>bold</b></p>");
373 }
374
375 #[test]
376 fn inline_format_nested_contexts() {
377 let query = "hello world & goodbye";
378 let href = format!("/search?q={}", display_uri_component(query));
379 let attr = format!(r#"<a href="{}">"#, display_html_attribute(&href));
380 assert!(attr.contains("/search?q=hello%20world%20%26%20goodbye"));
381 }
382
383 #[test]
384 fn write_macro_integration() {
385 use std::fmt::Write;
386 let mut buf = String::new();
387 write!(buf, "<p>{}</p>", display_html("a & b")).unwrap();
388 assert_eq!(buf, "<p>a & b</p>");
389 }
390
391 #[test]
392 fn display_wrapper_is_reusable() {
393 let wrapper = display_html("<b>");
394 let first = format!("{wrapper}");
395 let second = format!("{wrapper}");
396 assert_eq!(first, second);
397 assert_eq!(first, "<b>");
398 }
399
400 #[derive(Default)]
401 struct ChunkCounter {
402 out: String,
403 chunks: usize,
404 }
405
406 impl fmt::Write for ChunkCounter {
407 fn write_str(&mut self, s: &str) -> fmt::Result {
408 if !s.is_empty() {
409 self.chunks += 1;
410 }
411 self.out.push_str(s);
412 Ok(())
413 }
414 }
415
416 #[test]
417 fn bare_spec_writes_through_without_buffering() {
418 use std::fmt::Write;
419 let mut sink = ChunkCounter::default();
420 write!(sink, "{}", display_html("a&b<c")).unwrap();
421 assert_eq!(sink.out, crate::for_html("a&b<c"));
422 assert!(
423 sink.chunks > 1,
424 "expected the encoder to write runs straight to the formatter, got {} chunk(s)",
425 sink.chunks
426 );
427 }
428
429 #[test]
430 fn format_spec_buffers_into_a_single_chunk() {
431 use std::fmt::Write;
432 let mut sink = ChunkCounter::default();
433 write!(sink, "{:.99}", display_html("a&b<c")).unwrap();
434 assert_eq!(sink.out, format!("{:.99}", crate::for_html("a&b<c")));
435 assert_eq!(sink.chunks, 1);
436 }
437}