//! `links::disarm` where the server's CommonMark parser and the clients' marked may read a reply //! differently: such text is read as prose, so an image in it still goes in a code span (M5b //! spec, section 9). Each case names the rule it pins. Do not edit. use gatewayd::links::disarm; /// (case, input, clean reply). The untrusted reply is checked by the tests in `links.rs`. type Case = (&'static str, &'static str, &'static str); const FENCES: &[Case] = &[ ( "an unclosed fence is prose", "```\n![i](https://e.example/a)", "```\n````![i](https://e.example/a)````", ), ( "a closer may end with tildes, and what follows is prose", "```\n![i](https://e.example/a)\n```~\n![j](https://e.example/b)", "```\n![i](https://e.example/a)\n```~\n````![j](https://e.example/b)````", ), ( "a longer closer closes, a shorter one does not", "```\n![i](https://e.example/a)\n````\n````\n```\n![j](https://e.example/b)\n````", "```\n![i](https://e.example/a)\n````\n````\n```\n![j](https://e.example/b)\n````", ), ( "an opener indented four spaces is prose", " ```\n ![i](https://e.example/a)\n ```", " ```\n ````![i](https://e.example/a)````\n ```", ), ( "two backticks are no fence", "``\n![i](https://e.example/a)\n``", "``\n```![i](https://e.example/a)```\n``", ), ( "a closer indented four spaces does not close", "```\n![i](https://e.example/a)\n ```\n![j](https://e.example/b)\n```", "```\n![i](https://e.example/a)\n ```\n![j](https://e.example/b)\n```", ), ( "an opener indented three spaces is a fence", " ```\n ![i](https://e.example/a)\n```", " ```\n ![i](https://e.example/a)\n```", ), ( "an opener with a pipe after it is prose", "```|x\n![i](https://e.example/a)\n```", "```|x\n````![i](https://e.example/a)````\n```", ), ( "an opener with a backtick after it is prose", "``` a`b\n![i](https://e.example/a)\n```", "``` a`b\n````![i](https://e.example/a)````\n```", ), ( "a fence continuing a list item is prose", "- item\n```\n![i](https://e.example/a)\n```", "- item\n```\n````![i](https://e.example/a)````\n```", ), ( "a fence continuing a quote is prose", "> quote\n```\n![i](https://e.example/a)\n```", "> quote\n```\n````![i](https://e.example/a)````\n```", ), ( "a fence continuing a numbered item is prose", "1. item\n```\n![i](https://e.example/a)\n```", "1. item\n```\n````![i](https://e.example/a)````\n```", ), ( "a fence after an empty line below a list is a fence, even with an empty line inside", "- item\n\n```\n![i](https://e.example/a)\n\nx\n```", "- item\n\n```\n![i](https://e.example/a)\n\nx\n```", ), ( "a line indented less than the opener ends it", "- item\n\n ```\n![i](https://e.example/a)\n ```", "- item\n\n ```\n````![i](https://e.example/a)````\n ```", ), ( "an empty line inside an indented fence does not end it", "- item\n\n ```\n one\n\n ![i](https://e.example/a)\n ```", "- item\n\n ```\n one\n\n ![i](https://e.example/a)\n ```", ), ( "a fence after an HTML tag is prose", "
\n\n```\n![i](https://e.example/a)\n```\n\n
", "
\n\n```\n````![i](https://e.example/a)````\n```\n\n
", ), ( "a tag after a list marker counts", "-
\n\n```\n![i](https://e.example/a)\n```", "-
\n\n```\n````![i](https://e.example/a)````\n```", ), ( "a tag inside a line does not", "Use `Vec`:\n```\n![i](https://e.example/a)\n```", "Use `Vec`:\n```\n![i](https://e.example/a)\n```", ), ( "a closing tag counts", "

\n```\n![i](https://e.example/a)\n```", "

\n```\n````![i](https://e.example/a)````\n```", ), ( "a tag after the fence does not matter", "```\n![i](https://e.example/a)\n```\n
", "```\n![i](https://e.example/a)\n```\n
", ), ( "a tag inside a fence does not count", "```\n
\n```\n```\n![i](https://e.example/a)\n```", "```\n
\n```\n```\n![i](https://e.example/a)\n```", ), ]; const LINES: &[Case] = &[ ( "a carriage return ends a line", "```\r![i](https://e.example/a)\r```\rtext\r![j](https://e.example/b)", "```\r![i](https://e.example/a)\r```\rtext\r````![j](https://e.example/b)````", ), ( "U+2424 ends a line", "```\u{2424}![i](https://e.example/a)\u{2424}```", "```\u{2424}![i](https://e.example/a)\u{2424}```", ), ( "CRLF is one line end, not an empty line", "`a\r\n`![i](https://e.example/a)`", "`a\r\n` ``![i](https://e.example/a)`` `", ), ( "a line of spaces inside an alt is written as it is", "![a\n \nb](https://e.example/x)", "`![a`\n \n`b](https://e.example/x)`", ), ( "an alt split by CRLF keeps it", "![a\r\nb](https://e.example/x)", "`![a`\r\n`b](https://e.example/x)`", ), ]; const SPANS: &[Case] = &[ ( "a span on one line is code", "a\n`![i](https://e.example/a)`", "a\n`![i](https://e.example/a)`", ), ( "after a lone backtick, a span of its length is prose", "`a\n`![i](https://e.example/a)`", "`a\n` ``![i](https://e.example/a)`` `", ), ( "a span of another length is still code", "`a\n``![i](https://e.example/a)``", "`a\n``![i](https://e.example/a)``", ), ( "a span holding a run of that length is prose", "`lone\n``a ` ![i](https://e.example/a) ``", "`lone\n``a ` ```![i](https://e.example/a)``` ``", ), ( "an empty line starts afresh", "`a\n\n`![i](https://e.example/a)`", "`a\n\n`![i](https://e.example/a)`", ), ( "a line of spaces does not", "`a\n \n`![i](https://e.example/a)`", "`a\n \n` ``![i](https://e.example/a)`` `", ), ( "a span that does not close on its line is prose", "`a\nb` ![i](https://e.example/a)", "`a\nb` ``![i](https://e.example/a)``", ), ( "a span holding a pipe is prose (a table cell ends there)", "| `x | ![i](https://e.example/a) | y` |", "| `x | ``![i](https://e.example/a)`` | y` |", ), ( "an escaped backtick opens nothing", "\\`![i](https://e.example/a)`", "\\` ``![i](https://e.example/a)`` `", ), ( "a backtick in a URL opens nothing (marked reads it as the URL's)", "https://x.example/a`b ![i](https://e.example/a) `", "https://x.example/a`b ``![i](https://e.example/a)`` `", ), ( "a backtick in a link destination opens nothing", "[a](x `) ![i](https://e.example/a) `", "[a](x `) ``![i](https://e.example/a)`` `", ), ]; const BRACKETS: &[Case] = &[ ( "a label holding a bracket in code", "![a `]` b](https://e.example/a)", "``![a ``\\` ``]``\\` `` b](https://e.example/a)``", ), ( "a label holding a lone backtick", "![a ``](https://e.example/a) `", "```![a ```\\`\\` ```](https://e.example/a)``` `", ), ( "nested brackets and escapes in the label", "![a [b] \\] c](https://e.example/a)", "`![a [b] \\] c](https://e.example/a)`", ), ( "nested parentheses in the destination; a backslash does not escape one", "![a](https://e.example/a_(b)\\)c) d", "`![a](https://e.example/a_(b)\\)`c) d", ), ( "no parenthesis closes the destination", "![a](https://e.example/a b", "`![a]`(https://e.example/a b", ), ]; /// What marked lets take a backtick before a code span starts: emphasis, a tag, a URL, a link. const STEALS: &[Case] = &[ ( "emphasis before a span that holds its closer makes it prose", "** `** ![i](https://e.example/a) `", "** `** ``![i](https://e.example/a)`` `", ), ( "a tag before a span that holds a `>` makes it prose", " ![i](https://e.example/a) `", " ``![i](https://e.example/a)`` `", ), ( "a `[` that starts no link is escaped when a `](` follows", "[x [y](https://l.example) and [z]", "\\[x [y](https://l.example) and [z]", ), ( "the characters of a clean link count as seen", "[a*b](https://l.example) `*![i](https://e.example/a)`", "[a*b](https://l.example) `* ``![i](https://e.example/a)`` `", ), ( "inline LaTeX before a span that holds a `$` makes it prose", "$a `$ ![i](https://e.example/a) `", "$a `$ ``![i](https://e.example/a)`` `", ), ( "a new span splits at a `$` seen before it, which marked does not escape", "$x ![i](https://e.example/a$b)", "$x `![i](https://e.example/a`$ `b)`", ), ( "but not at a `$` seen only after it", "![i](https://e.example/a$b) $x", "`![i](https://e.example/a$b)` $x", ), ( "a new span splits at a closer seen before it", "*a* ![i](https://e.example/b*c)", "*a* `![i](https://e.example/b`\\* `c)`", ), ( "but not at one seen only after it", "![i](https://e.example/b*c) *a*", "`![i](https://e.example/b*c)` *a*", ), ( "marked may read part of a run into a clean URL", "www.x.example/p```)`![i](https://e.example/a)`", "www.x.example/p```)` ````![i](https://e.example/a)```` `", ), ( "a clean URL ends at a `<` before a line end, which may start an autolink", "e.org/p<\ne.org/p![i](https://e.example/x)", "e.org/p<\ne.org/p `![i](https://e.example/x)`", ), ( "a clean URL holding an image goes in code whole", "https://x.example/![i](https://e.example/a) end", "`https://x.example/![i](https://e.example/a)` end", ), ]; fn check(table: &[Case]) { for &(case, input, clean) in table { let once = disarm(input, false); assert_eq!(once.text, clean, "{case}"); assert_eq!(disarm(clean, false).text, clean, "{case}: twice"); } } #[test] fn fences_both_parsers_read_as_code() { check(FENCES); } #[test] fn lines_end_where_marked_ends_them() { check(LINES); } #[test] fn spans_both_parsers_read_as_code() { check(SPANS); } #[test] fn labels_and_destinations_end_where_either_parser_ends_them() { check(BRACKETS); } #[test] fn what_marked_lets_take_a_backtick_leaves_no_image() { check(STEALS); } #[test] fn a_construct_is_looked_for_in_its_first_2048_characters() { // From its `[` to its `)`: 2048 characters, found whole. let image = |alt: usize| format!("![{}](https://e.example/x)", "a".repeat(alt)); assert_eq!( disarm(&image(2025), false).text, format!("`{}`", image(2025)) ); // One more, and the `)` is out of reach: the image is read as `![alt]`. let alt = "a".repeat(2026); let got = disarm(&image(2026), false).text; assert_eq!(got, format!("`![{alt}]`(https://e.example/x)")); // With the `]` out of reach too, only the `![` goes in code; in an untrusted reply, the URL // as well. let alt = "a".repeat(2047); let got = disarm(&image(2047), false).text; assert_eq!(got, format!("`![`{alt}](https://e.example/x)")); let got = disarm(&image(2047), true).text; assert_eq!(got, format!("`![`{alt}]( `https://e.example/x`)")); // A link out of reach keeps only its URL in code. let got = disarm(&format!("[{alt}](https://l.example/x)"), true); assert_eq!(got.text, format!("[{alt}]( `https://l.example/x`)")); assert!(got.links); } #[test] fn an_untrusted_reply_follows_the_same_rules() { let text = "- item\n```\nhttps://e.example/a\n```\n\n```\nhttps://e.example/b\n```"; let got = disarm(text, true); assert_eq!( got.text, "- item\n```\n````https://e.example/a````\n```\n\n```\nhttps://e.example/b\n```" ); assert!(got.links); }