364 lines
12 KiB
Rust
364 lines
12 KiB
Rust
//! `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",
|
|
"```\n````````",
|
|
),
|
|
(
|
|
"a closer may end with tildes, and what follows is prose",
|
|
"```\n\n```~\n",
|
|
"```\n\n```~\n````````",
|
|
),
|
|
(
|
|
"a longer closer closes, a shorter one does not",
|
|
"```\n\n````\n````\n```\n\n````",
|
|
"```\n\n````\n````\n```\n\n````",
|
|
),
|
|
(
|
|
"an opener indented four spaces is prose",
|
|
" ```\n \n ```",
|
|
" ```\n ````````\n ```",
|
|
),
|
|
(
|
|
"two backticks are no fence",
|
|
"``\n\n``",
|
|
"``\n``````\n``",
|
|
),
|
|
(
|
|
"a closer indented four spaces does not close",
|
|
"```\n\n ```\n\n```",
|
|
"```\n\n ```\n\n```",
|
|
),
|
|
(
|
|
"an opener indented three spaces is a fence",
|
|
" ```\n \n```",
|
|
" ```\n \n```",
|
|
),
|
|
(
|
|
"an opener with a pipe after it is prose",
|
|
"```|x\n\n```",
|
|
"```|x\n````````\n```",
|
|
),
|
|
(
|
|
"an opener with a backtick after it is prose",
|
|
"``` a`b\n\n```",
|
|
"``` a`b\n````````\n```",
|
|
),
|
|
(
|
|
"a fence continuing a list item is prose",
|
|
"- item\n```\n\n```",
|
|
"- item\n```\n````````\n```",
|
|
),
|
|
(
|
|
"a fence continuing a quote is prose",
|
|
"> quote\n```\n\n```",
|
|
"> quote\n```\n````````\n```",
|
|
),
|
|
(
|
|
"a fence continuing a numbered item is prose",
|
|
"1. item\n```\n\n```",
|
|
"1. item\n```\n````````\n```",
|
|
),
|
|
(
|
|
"a fence after an empty line below a list is a fence, even with an empty line inside",
|
|
"- item\n\n```\n\n\nx\n```",
|
|
"- item\n\n```\n\n\nx\n```",
|
|
),
|
|
(
|
|
"a line indented less than the opener ends it",
|
|
"- item\n\n ```\n\n ```",
|
|
"- item\n\n ```\n````````\n ```",
|
|
),
|
|
(
|
|
"an empty line inside an indented fence does not end it",
|
|
"- item\n\n ```\n one\n\n \n ```",
|
|
"- item\n\n ```\n one\n\n \n ```",
|
|
),
|
|
(
|
|
"a fence after an HTML tag is prose",
|
|
"<div>\n\n```\n\n```\n\n</div>",
|
|
"<div>\n\n```\n````````\n```\n\n</div>",
|
|
),
|
|
(
|
|
"a tag after a list marker counts",
|
|
"- <div>\n\n```\n\n```",
|
|
"- <div>\n\n```\n````````\n```",
|
|
),
|
|
(
|
|
"a tag inside a line does not",
|
|
"Use `Vec<String>`:\n```\n\n```",
|
|
"Use `Vec<String>`:\n```\n\n```",
|
|
),
|
|
(
|
|
"a closing tag counts",
|
|
"</p>\n```\n\n```",
|
|
"</p>\n```\n````````\n```",
|
|
),
|
|
(
|
|
"a tag after the fence does not matter",
|
|
"```\n\n```\n<div>",
|
|
"```\n\n```\n<div>",
|
|
),
|
|
(
|
|
"a tag inside a fence does not count",
|
|
"```\n<div>\n```\n```\n\n```",
|
|
"```\n<div>\n```\n```\n\n```",
|
|
),
|
|
];
|
|
|
|
const LINES: &[Case] = &[
|
|
(
|
|
"a carriage return ends a line",
|
|
"```\r\r```\rtext\r",
|
|
"```\r\r```\rtext\r````````",
|
|
),
|
|
(
|
|
"U+2424 ends a line",
|
|
"```\u{2424}\u{2424}```",
|
|
"```\u{2424}\u{2424}```",
|
|
),
|
|
(
|
|
"CRLF is one line end, not an empty line",
|
|
"`a\r\n``",
|
|
"`a\r\n` ```` `",
|
|
),
|
|
(
|
|
"a line of spaces inside an alt is written as it is",
|
|
"",
|
|
"``",
|
|
),
|
|
(
|
|
"an alt split by CRLF keeps it",
|
|
"",
|
|
"``",
|
|
),
|
|
];
|
|
|
|
const SPANS: &[Case] = &[
|
|
(
|
|
"a span on one line is code",
|
|
"a\n``",
|
|
"a\n``",
|
|
),
|
|
(
|
|
"after a lone backtick, a span of its length is prose",
|
|
"`a\n``",
|
|
"`a\n` ```` `",
|
|
),
|
|
(
|
|
"a span of another length is still code",
|
|
"`a\n````",
|
|
"`a\n````",
|
|
),
|
|
(
|
|
"a span holding a run of that length is prose",
|
|
"`lone\n``a `  ``",
|
|
"`lone\n``a ` `````` ``",
|
|
),
|
|
(
|
|
"an empty line starts afresh",
|
|
"`a\n\n``",
|
|
"`a\n\n``",
|
|
),
|
|
(
|
|
"a line of spaces does not",
|
|
"`a\n \n``",
|
|
"`a\n \n` ```` `",
|
|
),
|
|
(
|
|
"a span that does not close on its line is prose",
|
|
"`a\nb` ",
|
|
"`a\nb` ````",
|
|
),
|
|
(
|
|
"a span holding a pipe is prose (a table cell ends there)",
|
|
"| `x |  | y` |",
|
|
"| `x | ```` | y` |",
|
|
),
|
|
(
|
|
"an escaped backtick opens nothing",
|
|
"\\``",
|
|
"\\` ```` `",
|
|
),
|
|
(
|
|
"a backtick in a URL opens nothing (marked reads it as the URL's)",
|
|
"https://x.example/a`b  `",
|
|
"https://x.example/a`b ```` `",
|
|
),
|
|
(
|
|
"a backtick in a link destination opens nothing",
|
|
"[a](x `)  `",
|
|
"[a](x `) ```` `",
|
|
),
|
|
];
|
|
|
|
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",
|
|
" `",
|
|
"`````` `",
|
|
),
|
|
(
|
|
"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",
|
|
"\\)c) d",
|
|
"`\\)`c) d",
|
|
),
|
|
(
|
|
"no parenthesis closes the destination",
|
|
",
|
|
];
|
|
|
|
/// 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",
|
|
"** `**  `",
|
|
"** `** ```` `",
|
|
),
|
|
(
|
|
"a tag before a span that holds a `>` makes it prose",
|
|
"<b x='`'>  `",
|
|
"<b x='`'> ```` `",
|
|
),
|
|
(
|
|
"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) `*`",
|
|
"[a*b](https://l.example) `* ```` `",
|
|
),
|
|
(
|
|
"inline LaTeX before a span that holds a `$` makes it prose",
|
|
"$a `$  `",
|
|
"$a `$ ```` `",
|
|
),
|
|
(
|
|
"a new span splits at a `$` seen before it, which marked does not escape",
|
|
"$x ",
|
|
"$x ``",
|
|
),
|
|
(
|
|
"but not at a `$` seen only after it",
|
|
" $x",
|
|
"`` $x",
|
|
),
|
|
(
|
|
"a new span splits at a closer seen before it",
|
|
"*a* ",
|
|
"*a* ``",
|
|
),
|
|
(
|
|
"but not at one seen only after it",
|
|
" *a*",
|
|
"`` *a*",
|
|
),
|
|
(
|
|
"marked may read part of a run into a clean URL",
|
|
"www.x.example/p```)``",
|
|
"www.x.example/p```)` ```````` `",
|
|
),
|
|
(
|
|
"a clean URL ends at a `<` before a line end, which may start an autolink",
|
|
"e.org/p<\ne.org/p<https://a.example>",
|
|
"e.org/p<\ne.org/p<https://a.example> ``",
|
|
),
|
|
(
|
|
"a clean URL holding an image goes in code whole",
|
|
"https://x.example/ end",
|
|
"`https://x.example/` 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!("", "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 `"));
|
|
let got = disarm(&image(2047), true).text;
|
|
assert_eq!(got, format!("`"));
|
|
// 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);
|
|
}
|