Repository: kivikakk/comrak (Rust)

Issue:
Escaping of IPv6 addresses in autolinks
Hello!

This code:

```rust
let text = "test http://[319:3cf0:dd1d:47b9:20c:29ff:fe2c:39be]/test";
let options = Options {
    extension: ExtensionOptions::builder()
        .autolink(true)
        .build(),
    ..Default::default()
};
let html = markdown_to_html(text, &options);
```

produces this HTML:

```
<p>test <a href="http://%5B319:3cf0:dd1d:47b9:20c:29ff:fe2c:39be%5D/test">http://[319:3cf0:dd1d:47b9:20c:29ff:fe2c:39be]/test</a></p>\n
```

Square brackets are escaped, but they shouldn't. Same result with `relaxed_autolinks = true`.


Discussion:
Nice spot! I'll have a fix going shortly.
@silverpill Can you let me know if #578 looks like it'll work for all your use cases? I don't find myself linking to v6 addresses directly in documents very much.
@kivikakk It works with 'http' and 'https' URIs, but not with other schemes (when `relaxed_autolinks` option is enabled).

For example: `nex://[319:3cf0:dd1d:47b9:20c:29ff:fe2c:39be]/test`
Thanks! I would not have checked that. Fixed in #578; let me know if all good.
It's all good! Thank you!
Merged! Lemme know if you'd like a release or anything.



Patch:
diff --git a/flake.nix b/flake.nix
index 5ab32413..04d168d9 100644
--- a/flake.nix
+++ b/flake.nix
@@ -91,11 +91,13 @@
               "rustc"
               "rust-analyzer"
               "clippy"
+              "rustfmt"
             ])
             pkgs.rust-analyzer
             pkgs.clippy
             pkgs.cargo-fuzz
             pkgs.python3
+            pkgs.re2c
           ];
         };
       }
diff --git a/src/html.rs b/src/html.rs
index f0a9db9b..517957c2 100644
--- a/src/html.rs
+++ b/src/html.rs
@@ -1762,7 +1762,13 @@ pub fn escape(output: &mut dyn Write, buffer: &[u8]) -> io::Result<()> {
 /// We assume they actually want the query string "?q=a%20b", a search for
 /// the string "a b", rather than "?q=a%2520b", a search for the literal
 /// string "a%20b".
-pub fn escape_href(output: &mut dyn Write, buffer: &[u8]) -> io::Result<()> {
+///
+/// We take some care with the square bracket characters `[` and `]`. We permit
+/// them to be unescaped in the output iff they surround what reasonably appears
+/// to be an IPv6 address in the host part of a URI.  If `relaxed_ipv6` is
+/// `true`, any scheme is permitted for such an address.  Otherwise, only `http`
+/// or `https` are permitted.
+pub fn escape_href(output: &mut dyn Write, buffer: &[u8], relaxed_ipv6: bool) -> io::Result<()> {
     const HREF_SAFE: [bool; 256] = character_set!(
         b"-_.+!*(),%#@?=;:/,+$~",
         b"abcdefghijklmnopqrstuvwxyz",
@@ -1772,6 +1778,16 @@ pub fn escape_href(output: &mut dyn Write, buffer: &[u8]) -> io::Result<()> {
     let size = buffer.len();
     let mut i = 0;
 
+    let possible_ipv6_url_end = if relaxed_ipv6 {
+        scanners::ipv6_relaxed_url_start(buffer)
+    } else {
+        scanners::ipv6_url_start(buffer)
+    };
+    if let Some(ipv6_url_end) = possible_ipv6_url_end {
+        output.write_all(&buffer[0..ipv6_url_end])?;
+        i = ipv6_url_end;
+    }
+
     while i < size {
         let org = i;
         while i < size && HREF_SAFE[buffer[i] as usize] {
diff --git a/src/html/context.rs b/src/html/context.rs
index 990f61ad..2531b470 100644
--- a/src/html/context.rs
+++ b/src/html/context.rs
@@ -68,7 +68,8 @@ impl<'o, 'c, T> Context<'o, 'c, T> {
 
     /// Convenience wrapper for [`html::escape_href`].
     pub fn escape_href(&mut self, buffer: &[u8]) -> io::Result<()> {
-        html::escape_href(self, buffer)
+        let relaxed_autolinks = self.options.parse.relaxed_autolinks;
+        html::escape_href(self, buffer, relaxed_autolinks)
     }
 }
 
diff --git a/src/scanners.re b/src/scanners.re
index 6dd52e1b..25f563d3 100644
--- a/src/scanners.re
+++ b/src/scanners.re
@@ -328,6 +328,26 @@ pub fn dangerous_url(s: &[u8]) -> Option<usize> {
 */
 }
 
+pub fn ipv6_url_start(s: &[u8]) -> Option<usize> {
+    let mut cursor = 0;
+    let mut marker = 0;
+    let len = s.len();
+/*!re2c
+    'http' [s]? '://[' [0-9a-fA-F:]+ ('%25' [a-zA-Z0-9]+)? ']' { return Some(cursor); }
+    * { return None; }
+*/
+}
+
+pub fn ipv6_relaxed_url_start(s: &[u8]) -> Option<usize> {
+    let mut cursor = 0;
+    let mut marker = 0;
+    let len = s.len();
+/*!re2c
+    [a-z]+ '://[' [0-9a-fA-F:]+ ('%25' [a-zA-Z0-9]+)? ']' { return Some(cursor); }
+    * { return None; }
+*/
+}
+
 /*!re2c
 
     table_spoiler = ['|']['|'];
@@ -456,5 +476,3 @@ pub fn description_item_start(s: &[u8]) -> Option<usize> {
     * { return None; }
 */
 }
-
-// vim: set ft=rust:
diff --git a/src/scanners.rs b/src/scanners.rs
index d545bd1e..eca08e3c 100644
--- a/src/scanners.rs
+++ b/src/scanners.rs
@@ -1,4 +1,4 @@
-/* Generated by re2rust 4.0 */
+/* Generated by re2rust 4.1 */
 
 use crate::parser::alert::AlertType;
 
@@ -22131,6 +22131,661 @@ pub fn dangerous_url(s: &[u8]) -> Option<usize> {
     }
 }
 
+pub fn ipv6_url_start(s: &[u8]) -> Option<usize> {
+    let mut cursor = 0;
+    let mut marker = 0;
+    let len = s.len();
+
+    {
+        #[allow(unused_assignments)]
+        let mut yych: u8 = 0;
+        let mut yystate: usize = 0;
+        'yyl: loop {
+            match yystate {
+                0 => {
+                    yych = unsafe {
+                        if cursor < len {
+                            *s.get_unchecked(cursor)
+                        } else {
+                            0
+                        }
+                    };
+                    cursor += 1;
+                    match yych {
+                        0x48 | 0x68 => {
+                            yystate = 3;
+                            continue 'yyl;
+                        }
+                        _ => {
+                            yystate = 1;
+                            continue 'yyl;
+                        }
+                    }
+                }
+                1 => {
+                    yystate = 2;
+                    continue 'yyl;
+                }
+                2 => {
+                    return None;
+                }
+                3 => {
+                    marker = cursor;
+                    yych = unsafe {
+                        if cursor < len {
+                            *s.get_unchecked(cursor)
+                        } else {
+                            0
+                        }
+                    };
+                    match yych {
+                        0x54 | 0x74 => {
+                            cursor += 1;
+                            yystate = 4;
+                            continue 'yyl;
+                        }
+                        _ => {
+                            yystate = 2;
+                            continue 'yyl;
+                        }
+                    }
+                }
+                4 => {
+                    yych = unsafe {
+                        if cursor < len {
+                            *s.get_unchecked(cursor)
+                        } else {
+                            0
+                        }
+                    };
+                    match yych {
+                        0x54 | 0x74 => {
+                            cursor += 1;
+                            yystate = 6;
+                            continue 'yyl;
+                        }
+                        _ => {
+                            yystate = 5;
+                            continue 'yyl;
+                        }
+                    }
+                }
+                5 => {
+                    cursor = marker;
+                    yystate = 2;
+                    continue 'yyl;
+                }
+                6 => {
+                    yych = unsafe {
+                        if cursor < len {
+                            *s.get_unchecked(cursor)
+                        } else {
+                            0
+                        }
+                    };
+                    match yych {
+                        0x50 | 0x70 => {
+                            cursor += 1;
+                            yystate = 7;
+                            continue 'yyl;
+                        }
+                        _ => {
+                            yystate = 5;
+                            continue 'yyl;
+                        }
+                    }
+                }
+                7 => {
+                    yych = unsafe {
+                        if cursor < len {
+                            *s.get_unchecked(cursor)
+                        } else {
+                            0
+                        }
+                    };
+                    match yych {
+                        0x3A => {
+                            cursor += 1;
+                            yystate = 8;
+                            continue 'yyl;
+                        }
+                        0x73 => {
+                            cursor += 1;
+                            yystate = 9;
+                            continue 'yyl;
+                        }
+                        _ => {
+                            yystate = 5;
+                            continue 'yyl;
+                        }
+                    }
+                }
+                8 => {
+                    yych = unsafe {
+                        if cursor < len {
+                            *s.get_unchecked(cursor)
+                        } else {
+                            0
+                        }
+                    };
+                    match yych {
+                        0x2F => {
+                            cursor += 1;
+                            yystate = 10;
+                            continue 'yyl;
+                        }
+                        _ => {
+                            yystate = 5;
+                            continue 'yyl;
+                        }
+                    }
+                }
+                9 => {
+                    yych = unsafe {
+                        if cursor < len {
+                            *s.get_unchecked(cursor)
+                        } else {
+                            0
+                        }
+                    };
+                    match yych {
+                        0x3A => {
+                            cursor += 1;
+                            yystate = 8;
+                            continue 'yyl;
+                        }
+                        _ => {
+                            yystate = 5;
+                            continue 'yyl;
+                        }
+                    }
+                }
+                10 => {
+                    yych = unsafe {
+                        if cursor < len {
+                            *s.get_unchecked(cursor)
+                        } else {
+                            0
+                        }
+                    };
+                    match yych {
+                        0x2F => {
+                            cursor += 1;
+                            yystate = 11;
+                            continue 'yyl;
+                        }
+                        _ => {
+                            yystate = 5;
+                            continue 'yyl;
+                        }
+                    }
+                }
+                11 => {
+                    yych = unsafe {
+                        if cursor < len {
+                            *s.get_unchecked(cursor)
+                        } else {
+                            0
+                        }
+                    };
+                    match yych {
+                        0x5B => {
+                            cursor += 1;
+                            yystate = 12;
+                            continue 'yyl;
+                        }
+                        _ => {
+                            yystate = 5;
+                            continue 'yyl;
+                        }
+                    }
+                }
+                12 => {
+                    yych = unsafe {
+                        if cursor < len {
+                            *s.get_unchecked(cursor)
+                        } else {
+                            0
+                        }
+                    };
+                    match yych {
+                        0x25 | 0x5D => {
+                            yystate = 5;
+                            continue 'yyl;
+                        }
+                        _ => {
+                            yystate = 14;
+                            continue 'yyl;
+                        }
+                    }
+                }
+                13 => {
+                    yych = unsafe {
+                        if cursor < len {
+                            *s.get_unchecked(cursor)
+                        } else {
+                            0
+                        }
+                    };
+                    yystate = 14;
+                    continue 'yyl;
+                }
+                14 => match yych {
+                    0x25 => {
+                        cursor += 1;
+                        yystate = 15;
+                        continue 'yyl;
+                    }
+                    0x30..=0x3A | 0x41..=0x46 | 0x61..=0x66 => {
+                        cursor += 1;
+                        yystate = 13;
+                        continue 'yyl;
+                    }
+                    0x5D => {
+                        cursor += 1;
+                        yystate = 16;
+                        continue 'yyl;
+                    }
+                    _ => {
+                        yystate = 5;
+                        continue 'yyl;
+                    }
+                },
+                15 => {
+                    yych = unsafe {
+                        if cursor < len {
+                            *s.get_unchecked(cursor)
+                        } else {
+                            0
+                        }
+                    };
+                    match yych {
+                        0x32 => {
+                            cursor += 1;
+                            yystate = 17;
+                            continue 'yyl;
+                        }
+                        _ => {
+                            yystate = 5;
+                            continue 'yyl;
+                        }
+                    }
+                }
+                16 => {
+                    return Some(cursor);
+                }
+                17 => {
+                    yych = unsafe {
+                        if cursor < len {
+                            *s.get_unchecked(cursor)
+                        } else {
+                            0
+                        }
+                    };
+                    match yych {
+                        0x35 => {
+                            cursor += 1;
+                            yystate = 18;
+                            continue 'yyl;
+                        }
+                        _ => {
+                            yystate = 5;
+                            continue 'yyl;
+                        }
+                    }
+                }
+                18 => {
+                    yych = unsafe {
+                        if cursor < len {
+                            *s.get_unchecked(cursor)
+                        } else {
+                            0
+                        }
+                    };
+                    match yych {
+                        0x5D => {
+                            yystate = 5;
+                            continue 'yyl;
+                        }
+                        _ => {
+                            yystate = 20;
+                            continue 'yyl;
+                        }
+                    }
+                }
+                19 => {
+                    yych = unsafe {
+                        if cursor < len {
+                            *s.get_unchecked(cursor)
+                        } else {
+                            0
+                        }
+                    };
+                    yystate = 20;
+                    continue 'yyl;
+                }
+                20 => match yych {
+                    0x30..=0x39 | 0x41..=0x5A | 0x61..=0x7A => {
+                        cursor += 1;
+                        yystate = 19;
+                        continue 'yyl;
+                    }
+                    0x5D => {
+                        cursor += 1;
+                        yystate = 16;
+                        continue 'yyl;
+                    }
+                    _ => {
+                        yystate = 5;
+                        continue 'yyl;
+                    }
+                },
+                _ => panic!("internal lexer error"),
+            }
+        }
+    }
+}
+
+pub fn ipv6_relaxed_url_start(s: &[u8]) -> Option<usize> {
+    let mut cursor = 0;
+    let mut marker = 0;
+    let len = s.len();
+
+    {
+        #[allow(unused_assignments)]
+        let mut yych: u8 = 0;
+        let mut yystate: usize = 0;
+        'yyl: loop {
+            match yystate {
+                0 => {
+                    yych = unsafe {
+                        if cursor < len {
+                            *s.get_unchecked(cursor)
+                        } else {
+                            0
+                        }
+                    };
+                    cursor += 1;
+                    match yych {
+                        0x61..=0x7A => {
+                            yystate = 3;
+                            continue 'yyl;
+                        }
+                        _ => {
+                            yystate = 1;
+                            continue 'yyl;
+                        }
+                    }
+                }
+                1 => {
+                    yystate = 2;
+                    continue 'yyl;
+                }
+                2 => {
+                    return None;
+                }
+                3 => {
+                    marker = cursor;
+                    yych = unsafe {
+                        if cursor < len {
+                            *s.get_unchecked(cursor)
+                        } else {
+                            0
+                        }
+                    };
+                    match yych {
+                        0x3A => {
+                            cursor += 1;
+                            yystate = 4;
+                            continue 'yyl;
+                        }
+                        0x61..=0x7A => {
+                            cursor += 1;
+                            yystate = 6;
+                            continue 'yyl;
+                        }
+                        _ => {
+                            yystate = 2;
+                            continue 'yyl;
+                        }
+                    }
+                }
+                4 => {
+                    yych = unsafe {
+                        if cursor < len {
+                            *s.get_unchecked(cursor)
+                        } else {
+                            0
+                        }
+                    };
+                    match yych {
+                        0x2F => {
+                            cursor += 1;
+                            yystate = 7;
+                            continue 'yyl;
+                        }
+                        _ => {
+                            yystate = 5;
+                            continue 'yyl;
+                        }
+                    }
+                }
+                5 => {
+                    cursor = marker;
+                    yystate = 2;
+                    continue 'yyl;
+                }
+                6 => {
+                    yych = unsafe {
+                        if cursor < len {
+                            *s.get_unchecked(cursor)
+                        } else {
+                            0
+                        }
+                    };
+                    match yych {
+                        0x3A => {
+                            cursor += 1;
+                            yystate = 4;
+                            continue 'yyl;
+                        }
+                        0x61..=0x7A => {
+                            cursor += 1;
+                            yystate = 6;
+                            continue 'yyl;
+                        }
+                        _ => {
+                            yystate = 5;
+                            continue 'yyl;
+                        }
+                    }
+                }
+                7 => {
+                    yych = unsafe {
+                        if cursor < len {
+                            *s.get_unchecked(cursor)
+                        } else {
+                            0
+                        }
+                    };
+                    match yych {
+                        0x2F => {
+                            cursor += 1;
+                            yystate = 8;
+                            continue 'yyl;
+                        }
+                        _ => {
+                            yystate = 5;
+                            continue 'yyl;
+                        }
+                    }
+                }
+                8 => {
+                    yych = unsafe {
+                        if cursor < len {
+                            *s.get_unchecked(cursor)
+                        } else {
+                            0
+                        }
+                    };
+                    match yych {
+                        0x5B => {
+                            cursor += 1;
+                            yystate = 9;
+                            continue 'yyl;
+                        }
+                        _ => {
+                            yystate = 5;
+                            continue 'yyl;
+                        }
+                    }
+                }
+                9 => {
+                    yych = unsafe {
+                        if cursor < len {
+                            *s.get_unchecked(cursor)
+                        } else {
+                            0
+                        }
+                    };
+                    match yych {
+                        0x25 | 0x5D => {
+                            yystate = 5;
+                            continue 'yyl;
+                        }
+                        _ => {
+                            yystate = 11;
+                            continue 'yyl;
+                        }
+                    }
+                }
+                10 => {
+                    yych = unsafe {
+                        if cursor < len {
+                            *s.get_unchecked(cursor)
+                        } else {
+                            0
+                        }
+                    };
+                    yystate = 11;
+                    continue 'yyl;
+                }
+                11 => match yych {
+                    0x25 => {
+                        cursor += 1;
+                        yystate = 12;
+                        continue 'yyl;
+                    }
+                    0x30..=0x3A | 0x41..=0x46 | 0x61..=0x66 => {
+                        cursor += 1;
+                        yystate = 10;
+                        continue 'yyl;
+                    }
+                    0x5D => {
+                        cursor += 1;
+                        yystate = 13;
+                        continue 'yyl;
+                    }
+                    _ => {
+                        yystate = 5;
+                        continue 'yyl;
+                    }
+                },
+                12 => {
+                    yych = unsafe {
+                        if cursor < len {
+                            *s.get_unchecked(cursor)
+                        } else {
+                            0
+                        }
+                    };
+                    match yych {
+                        0x32 => {
+                            cursor += 1;
+                            yystate = 14;
+                            continue 'yyl;
+                        }
+                        _ => {
+                            yystate = 5;
+                            continue 'yyl;
+                        }
+                    }
+                }
+                13 => {
+                    return Some(cursor);
+                }
+                14 => {
+                    yych = unsafe {
+                        if cursor < len {
+                            *s.get_unchecked(cursor)
+                        } else {
+                            0
+                        }
+                    };
+                    match yych {
+                        0x35 => {
+                            cursor += 1;
+                            yystate = 15;
+                            continue 'yyl;
+                        }
+                        _ => {
+                            yystate = 5;
+                            continue 'yyl;
+                        }
+                    }
+                }
+                15 => {
+                    yych = unsafe {
+                        if cursor < len {
+                            *s.get_unchecked(cursor)
+                        } else {
+                            0
+                        }
+                    };
+                    match yych {
+                        0x5D => {
+                            yystate = 5;
+                            continue 'yyl;
+                        }
+                        _ => {
+                            yystate = 17;
+                            continue 'yyl;
+                        }
+                    }
+                }
+                16 => {
+                    yych = unsafe {
+                        if cursor < len {
+                            *s.get_unchecked(cursor)
+                        } else {
+                            0
+                        }
+                    };
+                    yystate = 17;
+                    continue 'yyl;
+                }
+                17 => match yych {
+                    0x30..=0x39 | 0x41..=0x5A | 0x61..=0x7A => {
+                        cursor += 1;
+                        yystate = 16;
+                        continue 'yyl;
+                    }
+                    0x5D => {
+                        cursor += 1;
+                        yystate = 13;
+                        continue 'yyl;
+                    }
+                    _ => {
+                        yystate = 5;
+                        continue 'yyl;
+                    }
+                },
+                _ => panic!("internal lexer error"),
+            }
+        }
+    }
+}
+
 pub fn table_start(s: &[u8]) -> Option<usize> {
     let mut cursor = 0;
     let mut marker = 0;
@@ -24666,5 +25321,3 @@ pub fn description_item_start(s: &[u8]) -> Option<usize> {
         }
     }
 }
-
-// vim: set ft=rust:


Test patch:
diff --git a/src/tests/autolink.rs b/src/tests/autolink.rs
index 5f59ea6b..e78f3259 100644
--- a/src/tests/autolink.rs
+++ b/src/tests/autolink.rs
@@ -510,3 +510,27 @@ fn autolink_not_generated_inside_wikilinks() {
         "<p><a href=\"http://example.com\" data-wikilink=\"true\">Check www.example.com</a></p>\n",
     );
 }
+
+#[test]
+fn ipv6_host_scoped() {
+    html_opts!(
+        [extension.autolink],
+        "scoped https://[fe80::1ff:fe23:4567:890a%25eth2]",
+        "<p>scoped <a href=\"https://[fe80::1ff:fe23:4567:890a%25eth2]\">https://[fe80::1ff:fe23:4567:890a%25eth2]</a></p>\n",
+    );
+}
+
+#[test]
+fn ipv6_relaxed() {
+    html_opts!(
+        [extension.autolink],
+        "hi: nex://[fe80::1ff:fe23:4567:890a%25eth2]/z/x",
+        "<p>hi: nex://[fe80::1ff:fe23:4567:890a%25eth2]/z/x</p>\n",
+    );
+
+    html_opts!(
+        [extension.autolink, parse.relaxed_autolinks],
+        "hi: nex://[fe80::1ff:fe23:4567:890a%25eth2]/z/x",
+        "<p>hi: <a href=\"nex://[fe80::1ff:fe23:4567:890a%25eth2]/z/x\">nex://[fe80::1ff:fe23:4567:890a%25eth2]/z/x</a></p>\n",
+    );
+}
diff --git a/src/tests/core.rs b/src/tests/core.rs
index d8df80f9..710788a7 100644
--- a/src/tests/core.rs
+++ b/src/tests/core.rs
@@ -750,3 +750,16 @@ fn emphasis_sourcepos_double_4() {
         ])
     );
 }
+
+#[test]
+fn ipv6_host_unescaped() {
+    html(
+        "test <http://[319:3cf0:dd1d:47b9:20c:29ff:fe2c:39be]/test>",
+        "<p>test <a href=\"http://[319:3cf0:dd1d:47b9:20c:29ff:fe2c:39be]/test\">http://[319:3cf0:dd1d:47b9:20c:29ff:fe2c:39be]/test</a></p>\n",
+    );
+
+    html(
+        "[henwo](https://[2402:1f00:89aa:300::5%25eth0]:9443?target=<yes>)",
+        "<p><a href=\"https://[2402:1f00:89aa:300::5%25eth0]:9443?target=%3Cyes%3E\">henwo</a></p>\n",
+    );
+}