432 lines
13 KiB
Rust
432 lines
13 KiB
Rust
//! `links::disarm` on each form of image and link, clean and untrusted, and on code that must be
|
|
//! left as it is (M5b spec, section 9; the plan's review focus 4). Do not edit.
|
|
|
|
use gatewayd::links::{NOTICE, disarm};
|
|
|
|
/// (case, input, clean reply, untrusted reply, whether the untrusted reply had a link disarmed).
|
|
type Case = (&'static str, &'static str, &'static str, &'static str, bool);
|
|
|
|
const IMAGES: &[Case] = &[
|
|
(
|
|
"inline image",
|
|
"An image  here.",
|
|
"An image `` here.",
|
|
"An image `` here.",
|
|
false,
|
|
),
|
|
(
|
|
"image with a size",
|
|
"",
|
|
"``",
|
|
"``",
|
|
false,
|
|
),
|
|
(
|
|
"image with a width and a title",
|
|
"",
|
|
"``",
|
|
"``",
|
|
false,
|
|
),
|
|
(
|
|
"reference image and its definition",
|
|
"![alt][logo]\n\n[logo]: https://e.example/l.png",
|
|
"`![alt][logo]`\n\n[logo]: https://e.example/l.png",
|
|
"`![alt][logo]`\n\n`[logo]: https://e.example/l.png`",
|
|
true,
|
|
),
|
|
(
|
|
"collapsed and shortcut reference images",
|
|
"![logo][] and ![logo]",
|
|
"`![logo][]` and `![logo]`",
|
|
"`![logo][]` and `![logo]`",
|
|
false,
|
|
),
|
|
(
|
|
"reference image with a space before its label",
|
|
"![alt] [logo] end",
|
|
"`![alt] [logo]` end",
|
|
"`![alt] [logo]` end",
|
|
false,
|
|
),
|
|
(
|
|
"data in an image URL (T37)",
|
|
"Done. ",
|
|
"Done. ``",
|
|
"Done. ``",
|
|
false,
|
|
),
|
|
(
|
|
"image glued to text, non-ASCII too",
|
|
"AB 字字",
|
|
"A ``B 字 ``字",
|
|
"A ``B 字 ``字",
|
|
false,
|
|
),
|
|
(
|
|
"image alt over two lines",
|
|
"",
|
|
"``",
|
|
"``",
|
|
false,
|
|
),
|
|
(
|
|
"image alt with a pipe",
|
|
"",
|
|
"``",
|
|
"``",
|
|
false,
|
|
),
|
|
(
|
|
"image inside a link",
|
|
"[](https://b.example)",
|
|
"`[](https://b.example)`",
|
|
"`[](https://b.example)`",
|
|
true,
|
|
),
|
|
(
|
|
"image inside brackets that are no link",
|
|
"[]",
|
|
"\\[ ``]",
|
|
"\\[ ``]",
|
|
false,
|
|
),
|
|
(
|
|
"no bracket closes the alt",
|
|
"",
|
|
"\\",
|
|
"\\! `[x](https://e.example/a)`",
|
|
true,
|
|
),
|
|
(
|
|
"escaped backslash, then an image",
|
|
"\\\\",
|
|
"\\\\ ``",
|
|
"\\\\ ``",
|
|
false,
|
|
),
|
|
];
|
|
|
|
const LINKS: &[Case] = &[
|
|
(
|
|
"plain URL, its full stop left out",
|
|
"See https://example.com/a.",
|
|
"See https://example.com/a.",
|
|
"See `https://example.com/a`.",
|
|
true,
|
|
),
|
|
(
|
|
"autolink",
|
|
"<https://example.com/a>",
|
|
"<https://example.com/a>",
|
|
"`<https://example.com/a>`",
|
|
true,
|
|
),
|
|
(
|
|
"email autolink",
|
|
"Write to <owner@example.com>.",
|
|
"Write to <owner@example.com>.",
|
|
"Write to `<owner@example.com>`.",
|
|
true,
|
|
),
|
|
(
|
|
"Markdown link with a title",
|
|
"[the docs](https://docs.example/p \"Title\")",
|
|
"[the docs](https://docs.example/p \"Title\")",
|
|
"`[the docs](https://docs.example/p \"Title\")`",
|
|
true,
|
|
),
|
|
(
|
|
"reference link and its definition",
|
|
"[docs][d]\n\n[d]: https://docs.example/p",
|
|
"[docs][d]\n\n[d]: https://docs.example/p",
|
|
"`[docs][d]`\n\n`[d]: https://docs.example/p`",
|
|
true,
|
|
),
|
|
(
|
|
"relative link",
|
|
"[home](/channels/town)",
|
|
"[home](/channels/town)",
|
|
"`[home](/channels/town)`",
|
|
true,
|
|
),
|
|
(
|
|
"www without a scheme",
|
|
"at www.example.com today",
|
|
"at www.example.com today",
|
|
"at `www.example.com` today",
|
|
true,
|
|
),
|
|
(
|
|
"domain and path without a scheme",
|
|
"example.org/path and example.org",
|
|
"example.org/path and example.org",
|
|
"`example.org/path` and example.org",
|
|
true,
|
|
),
|
|
(
|
|
"schemes without slashes",
|
|
"mailto:owner@example.com or tel:5551234",
|
|
"mailto:owner@example.com or tel:5551234",
|
|
"`mailto:owner@example.com` or `tel:5551234`",
|
|
true,
|
|
),
|
|
(
|
|
"prose with colons",
|
|
"Note: a ratio of 3:2 and key:value",
|
|
"Note: a ratio of 3:2 and key:value",
|
|
"Note: a ratio of 3:2 and `key:value`",
|
|
true,
|
|
),
|
|
(
|
|
"URL with parentheses, inside parentheses",
|
|
"(see https://en.example/wiki/Rust_(language))",
|
|
"(see https://en.example/wiki/Rust_(language))",
|
|
"(see `https://en.example/wiki/Rust_(language)`)",
|
|
true,
|
|
),
|
|
(
|
|
"URL up to a backtick",
|
|
"https://x.example/a`b",
|
|
"https://x.example/a`b",
|
|
"``https://x.example/a`` `b",
|
|
true,
|
|
),
|
|
(
|
|
"non-ASCII URL and text",
|
|
"Привет, мир: https://пример.рф/путь и всё.",
|
|
"Привет, мир: https://пример.рф/путь и всё.",
|
|
"Привет, мир: `https://пример.рф/путь` и всё.",
|
|
true,
|
|
),
|
|
(
|
|
"two links on a line",
|
|
"[a](https://a.example) or https://b.example/x?y=1",
|
|
"[a](https://a.example) or https://b.example/x?y=1",
|
|
"`[a](https://a.example)` or `https://b.example/x?y=1`",
|
|
true,
|
|
),
|
|
(
|
|
"angle brackets around a space are no autolink",
|
|
"<a b:c>",
|
|
"<a b:c>",
|
|
"<a `b:c`>",
|
|
true,
|
|
),
|
|
(
|
|
"a scheme inside a word is no URL",
|
|
"1http://x.example/p",
|
|
"1http://x.example/p",
|
|
"1http://x.example/p",
|
|
false,
|
|
),
|
|
(
|
|
"a last part of five letters is no domain",
|
|
"x.abcde/p and x.abcd/p",
|
|
"x.abcde/p and x.abcd/p",
|
|
"x.abcde/p and `x.abcd/p`",
|
|
true,
|
|
),
|
|
(
|
|
"www with digits",
|
|
"www2.example.com",
|
|
"www2.example.com",
|
|
"`www2.example.com`",
|
|
true,
|
|
),
|
|
(
|
|
"brackets and angle brackets that are no link",
|
|
"[not a link] and <foo> and a < b",
|
|
"[not a link] and <foo> and a < b",
|
|
"[not a link] and <foo> and a < b",
|
|
false,
|
|
),
|
|
(
|
|
"definition padded with spaces, ending in a backtick",
|
|
" [x]: https://e.example/`",
|
|
" [x]: https://e.example/`",
|
|
"`` [x]: https://e.example/``\\`",
|
|
true,
|
|
),
|
|
(
|
|
"a definition needs a label",
|
|
"[]: https://e.example/x",
|
|
"[]: https://e.example/x",
|
|
"[]: `https://e.example/x`",
|
|
true,
|
|
),
|
|
(
|
|
"a definition only at the start of a line",
|
|
"text [x]: https://e.example",
|
|
"text [x]: https://e.example",
|
|
"text [x]: `https://e.example`",
|
|
true,
|
|
),
|
|
];
|
|
|
|
const CODE: &[Case] = &[
|
|
(
|
|
"code spans",
|
|
"Run `curl https://example.com/x` and `` now.",
|
|
"Run `curl https://example.com/x` and `` now.",
|
|
"Run `curl https://example.com/x` and `` now.",
|
|
false,
|
|
),
|
|
(
|
|
"fenced block",
|
|
"Before:\n```sh\ncurl https://example.com/x\n\n```\nAfter.",
|
|
"Before:\n```sh\ncurl https://example.com/x\n\n```\nAfter.",
|
|
"Before:\n```sh\ncurl https://example.com/x\n\n```\nAfter.",
|
|
false,
|
|
),
|
|
(
|
|
"tilde fence, and a longer fence holding a shorter one",
|
|
"~~~\n\n~~~\n````\n```\nhttps://b.example\n````",
|
|
"~~~\n\n~~~\n````\n```\nhttps://b.example\n````",
|
|
"~~~\n\n~~~\n````\n```\nhttps://b.example\n````",
|
|
false,
|
|
),
|
|
(
|
|
"span with a backtick inside",
|
|
"``a ` https://x.example`` and ````",
|
|
"``a ` https://x.example`` and ````",
|
|
"``a ` https://x.example`` and ````",
|
|
false,
|
|
),
|
|
(
|
|
"link text in code",
|
|
"[`code`](https://d.example)",
|
|
"[`code`](https://d.example)",
|
|
"``[``\\` ``code``\\` ``](https://d.example)``",
|
|
true,
|
|
),
|
|
(
|
|
"code in code, and a link beside it",
|
|
"`https://a.example` https://b.example",
|
|
"`https://a.example` https://b.example",
|
|
"`https://a.example` ``https://b.example``",
|
|
true,
|
|
),
|
|
];
|
|
|
|
const TICKS: &[Case] = &[
|
|
(
|
|
"runs longer than any in the reply",
|
|
"``x`` then https://y.example and ",
|
|
"``x`` then https://y.example and ``````",
|
|
"``x`` then ```https://y.example``` and ``````",
|
|
true,
|
|
),
|
|
(
|
|
"a fence counts",
|
|
"```\ncode\n```\nhttps://y.example",
|
|
"```\ncode\n```\nhttps://y.example",
|
|
"```\ncode\n```\n````https://y.example````",
|
|
true,
|
|
),
|
|
(
|
|
"backticks inside the image go between spans, escaped",
|
|
"",
|
|
"````",
|
|
"````",
|
|
false,
|
|
),
|
|
(
|
|
"a backtick right after the image",
|
|
"x `y`",
|
|
"x ```` `y`",
|
|
"x ```` `y`",
|
|
false,
|
|
),
|
|
(
|
|
"a lone backtick right before the image",
|
|
"`",
|
|
"` ````",
|
|
"` ````",
|
|
false,
|
|
),
|
|
];
|
|
|
|
fn check(table: &[Case]) {
|
|
for &(case, input, clean, untrusted, links) in table {
|
|
let got = disarm(input, false);
|
|
assert_eq!(got.text, clean, "{case}: clean");
|
|
assert!(!got.links, "{case}: a clean reply never has links disarmed");
|
|
let got = disarm(input, true);
|
|
assert_eq!(got.text, untrusted, "{case}: untrusted");
|
|
assert_eq!(got.links, links, "{case}: links");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn images_are_code_in_every_reply() {
|
|
check(IMAGES);
|
|
}
|
|
|
|
#[test]
|
|
fn links_are_code_only_when_untrusted() {
|
|
check(LINKS);
|
|
}
|
|
|
|
#[test]
|
|
fn code_is_left_as_it_is() {
|
|
check(CODE);
|
|
}
|
|
|
|
#[test]
|
|
fn new_spans_use_a_longer_run_and_never_touch_a_backtick() {
|
|
check(TICKS);
|
|
}
|
|
|
|
#[test]
|
|
fn disarming_twice_changes_nothing() {
|
|
for table in [IMAGES, LINKS, CODE, TICKS] {
|
|
for &(case, input, ..) in table {
|
|
for untrusted in [false, true] {
|
|
let once = disarm(input, untrusted).text;
|
|
let twice = disarm(&once, untrusted);
|
|
assert_eq!(twice.text, once, "{case}: untrusted {untrusted}");
|
|
assert!(
|
|
!twice.links,
|
|
"{case}: untrusted {untrusted}: nothing left to disarm"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn text_without_images_or_links_is_unchanged() {
|
|
for text in [
|
|
"",
|
|
"Plain words.",
|
|
"Ünïcödé, 日本語, emoji 🦀, and a tab\there.",
|
|
"Lines\n\nwith\r\nbreaks\rof\u{2424}every kind\n",
|
|
"A list:\n- one\n- two\n\n> a quote\n\n# A heading",
|
|
"Emphasis: *a*, **b**, _c_, ~~d~~ and a table:\n| a | b |\n|---|---|\n| 1 | 2 |",
|
|
"Paths: crates/gatewayd/src/links.rs, ../x.txt, C:\\temp and std::io",
|
|
] {
|
|
for untrusted in [false, true] {
|
|
let got = disarm(text, untrusted);
|
|
assert_eq!(got.text, text, "untrusted {untrusted}");
|
|
assert!(!got.links);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn the_notice_is_one_line() {
|
|
assert_eq!(
|
|
NOTICE,
|
|
"Links in this reply are shown as code because this conversation has read untrusted content."
|
|
);
|
|
assert!(!NOTICE.contains('\n'));
|
|
}
|