1use std::error::Error;
2use std::fmt;
3
4const LITERAL_ESCAPED_CONTROLS: [&str; 3] = [r"\n", r"\r", r"\t"];
5
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct MarkdownPayloadViolation {
8 pub sequence: &'static str,
9 pub count: usize,
10}
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct MarkdownPayloadError {
14 violations: Vec<MarkdownPayloadViolation>,
15}
16
17impl MarkdownPayloadError {
18 pub fn violations(&self) -> &[MarkdownPayloadViolation] {
19 &self.violations
20 }
21}
22
23impl fmt::Display for MarkdownPayloadError {
24 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25 let details = self
26 .violations
27 .iter()
28 .map(|entry| format!("{} ({})", entry.sequence, entry.count))
29 .collect::<Vec<_>>()
30 .join(", ");
31 write!(
32 f,
33 "markdown payload contains literal escaped-control artifacts: {details}"
34 )
35 }
36}
37
38impl Error for MarkdownPayloadError {}
39
40pub fn markdown_payload_violations(markdown: &str) -> Vec<MarkdownPayloadViolation> {
41 let mut violations = Vec::new();
42
43 for sequence in LITERAL_ESCAPED_CONTROLS {
44 let count = markdown.match_indices(sequence).count();
45 if count > 0 {
46 violations.push(MarkdownPayloadViolation { sequence, count });
47 }
48 }
49
50 violations
51}
52
53pub fn validate_markdown_payload(markdown: &str) -> Result<(), MarkdownPayloadError> {
54 let violations = markdown_payload_violations(markdown);
55 if violations.is_empty() {
56 Ok(())
57 } else {
58 Err(MarkdownPayloadError { violations })
59 }
60}
61
62#[cfg(test)]
63mod tests {
64 use super::{markdown_payload_violations, validate_markdown_payload};
65
66 #[test]
67 fn markdown_payload_validator_accepts_real_control_chars() {
68 let payload = "line one\nline two\tvalue\r\n";
69 let result = validate_markdown_payload(payload);
70 assert!(
71 result.is_ok(),
72 "unexpected markdown payload error: {result:?}"
73 );
74 }
75
76 #[test]
77 fn markdown_payload_validator_rejects_literal_escaped_controls() {
78 let payload = r"line one\nline two\rline three\tvalue";
79 let err = validate_markdown_payload(payload).expect_err("expected markdown payload error");
80
81 assert_eq!(err.violations().len(), 3);
82 assert!(
83 err.to_string().contains(r"\n"),
84 "expected escaped-newline mention in {:?}",
85 err
86 );
87 assert!(
88 err.to_string().contains(r"\r"),
89 "expected escaped-return mention in {:?}",
90 err
91 );
92 assert!(
93 err.to_string().contains(r"\t"),
94 "expected escaped-tab mention in {:?}",
95 err
96 );
97 }
98
99 #[test]
100 fn markdown_payload_violations_reports_counts_per_sequence() {
101 let payload = r"one\n two\n three\t";
102 let violations = markdown_payload_violations(payload);
103
104 assert_eq!(violations.len(), 2);
105 assert_eq!(violations[0].sequence, r"\n");
106 assert_eq!(violations[0].count, 2);
107 assert_eq!(violations[1].sequence, r"\t");
108 assert_eq!(violations[1].count, 1);
109 }
110}