gatewayd: images, and links in untrusted replies, are posted as code
Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
@@ -0,0 +1,363 @@
|
||||
//! `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);
|
||||
}
|
||||
Reference in New Issue
Block a user