Files
boxmaker/crates/gatewayd/tests/links_marked.rs
T

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![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",
"<div>\n\n```\n![i](https://e.example/a)\n```\n\n</div>",
"<div>\n\n```\n````![i](https://e.example/a)````\n```\n\n</div>",
),
(
"a tag after a list marker counts",
"- <div>\n\n```\n![i](https://e.example/a)\n```",
"- <div>\n\n```\n````![i](https://e.example/a)````\n```",
),
(
"a tag inside a line does not",
"Use `Vec<String>`:\n```\n![i](https://e.example/a)\n```",
"Use `Vec<String>`:\n```\n![i](https://e.example/a)\n```",
),
(
"a closing tag counts",
"</p>\n```\n![i](https://e.example/a)\n```",
"</p>\n```\n````![i](https://e.example/a)````\n```",
),
(
"a tag after the fence does not matter",
"```\n![i](https://e.example/a)\n```\n<div>",
"```\n![i](https://e.example/a)\n```\n<div>",
),
(
"a tag inside a fence does not count",
"```\n<div>\n```\n```\n![i](https://e.example/a)\n```",
"```\n<div>\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",
"<b x='`'> ![i](https://e.example/a) `",
"<b x='`'> ``![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<https://a.example>![i](https://e.example/x)",
"e.org/p<\ne.org/p<https://a.example> `![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);
}