Skip to main content

nils_common/
markdown.rs

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}