Repository: raphlinus/pulldown-cmark (rust)

Issue:
[fuzz result] unbalanced parens are parsed as a link title
This markdown should not produce a link:

```markdown
[link]: test (()

[link]
```

The [commonmark.js dingus](<https://spec.commonmark.org/dingus/?text=%5Blink%5D%3A%20test%20(()%0A%0A%5Blink%5D>) does this right. As the spec sayeth:

https://spec.commonmark.org/0.30/#link-reference-definitions

> A [link reference definition](https://spec.commonmark.org/0.30/#link-reference-definition) consists of a [link label](https://spec.commonmark.org/0.30/#link-label), optionally preceded by up to three spaces of indentation, followed by a colon (:), optional spaces or tabs (including up to one [line ending](https://spec.commonmark.org/0.30/#line-ending)), a [link destination](https://spec.commonmark.org/0.30/#link-destination), optional spaces or tabs (including up to one [line ending](https://spec.commonmark.org/0.30/#line-ending)), and an optional [link title](https://spec.commonmark.org/0.30/#link-title), which if it is present must be separated from the [link destination](https://spec.commonmark.org/0.30/#link-destination) by spaces or tabs. No further character may occur.

https://spec.commonmark.org/0.30/#link-title

> A [link title](https://spec.commonmark.org/0.30/#link-title) consists of either
> - a sequence of zero or more characters between straight double-quote characters ("), including a " character only if it is backslash-escaped, or
> - a sequence of zero or more characters between straight single-quote characters ('), including a ' character only if it is backslash-escaped, or
> - a sequence of zero or more characters between *matching parentheses* ((...)), including a ( or ) character only if it is backslash-escaped.

<details>

```rust
"[+]:\\\n(()" -> [
]
```

Events from commonmark.js:

```rust
"[+]:\\\n(()" -> [
  Start(Paragraph)
    Text(Boxed("(()"))
  End(Paragraph)
]
```


Pull request description:
Reject opening parens within (/) link titles
Fixes #840


Patch:
diff --git a/pulldown-cmark/specs/regression.txt b/pulldown-cmark/specs/regression.txt
index 241f32b..7290d58 100644
--- a/pulldown-cmark/specs/regression.txt
+++ b/pulldown-cmark/specs/regression.txt
@@ -2361,3 +2361,65 @@ ISSUE 662
 </li>
 </ul>
 ````````````````````````````````
+
+ISSUE 840
+
+```````````````````````````````` example
+[link]: test (()
+
+[link]
+.
+<p>[link]: test (()</p>
+<p>[link]</p>
+````````````````````````````````
+
+```````````````````````````````` example
+[link]: test (())
+
+[link]
+.
+<p>[link]: test (())</p>
+<p>[link]</p>
+````````````````````````````````
+
+```````````````````````````````` example
+[link]: test (\(\))
+
+[link]
+.
+<p><a href="test" title="()">link</a></p>
+````````````````````````````````
+
+```````````````````````````````` example
+[link]: test """
+
+[link]
+.
+<p>[link]: test """</p>
+<p>[link]</p>
+````````````````````````````````
+
+```````````````````````````````` example
+[link]: test "\""
+
+[link]
+.
+<p><a href="test" title="&quot;">link</a></p>
+````````````````````````````````
+
+```````````````````````````````` example
+[link]: test '''
+
+[link]
+.
+<p>[link]: test '''</p>
+<p>[link]</p>
+````````````````````````````````
+
+```````````````````````````````` example
+[link]: test '\''
+
+[link]
+.
+<p><a href="test" title="&#39;">link</a></p>
+````````````````````````````````
diff --git a/pulldown-cmark/src/firstpass.rs b/pulldown-cmark/src/firstpass.rs
index 412deae..71607e3 100644
--- a/pulldown-cmark/src/firstpass.rs
+++ b/pulldown-cmark/src/firstpass.rs
@@ -1596,6 +1596,12 @@ impl<'a, 'b> FirstPass<'a, 'b> {
 
         while let Some(&c) = bytes.get(bytecount) {
             match c {
+                b'(' if closing_delim == b')' => {
+                    // https://spec.commonmark.org/0.30/#link-title
+                    // a sequence of zero or more characters between matching parentheses ((...)),
+                    // including a ( or ) character only if it is backslash-escaped.
+                    return None;
+                }
                 b'\n' | b'\r' => {
                     // push text to line buffer
                     // this is used to strip the block formatting:


Test patch:
diff --git a/pulldown-cmark/tests/suite/regression.rs b/pulldown-cmark/tests/suite/regression.rs
index e2af9b5..1d0dac0 100644
--- a/pulldown-cmark/tests/suite/regression.rs
+++ b/pulldown-cmark/tests/suite/regression.rs
@@ -2802,3 +2802,91 @@ fn regression_test_175() {
 
     test_markdown_html(original, expected, false, false, false);
 }
+
+#[test]
+fn regression_test_176() {
+    let original = r##"[link]: test (()
+
+[link]
+"##;
+    let expected = r##"<p>[link]: test (()</p>
+<p>[link]</p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn regression_test_177() {
+    let original = r##"[link]: test (())
+
+[link]
+"##;
+    let expected = r##"<p>[link]: test (())</p>
+<p>[link]</p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn regression_test_178() {
+    let original = r##"[link]: test (\(\))
+
+[link]
+"##;
+    let expected = r##"<p><a href="test" title="()">link</a></p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn regression_test_179() {
+    let original = r##"[link]: test """
+
+[link]
+"##;
+    let expected = r##"<p>[link]: test """</p>
+<p>[link]</p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn regression_test_180() {
+    let original = r##"[link]: test "\""
+
+[link]
+"##;
+    let expected = r##"<p><a href="test" title="&quot;">link</a></p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn regression_test_181() {
+    let original = r##"[link]: test '''
+
+[link]
+"##;
+    let expected = r##"<p>[link]: test '''</p>
+<p>[link]</p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn regression_test_182() {
+    let original = r##"[link]: test '\''
+
+[link]
+"##;
+    let expected = r##"<p><a href="test" title="&#39;">link</a></p>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}