Repository: raphlinus/pulldown-cmark (rust)

Issue:
[fuzz result] tight, nested lists can wrap block structures within emphasis
```console
$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/pulldown-cmark`
- *foo
  - - 
  baz*
^D
<ul>
<li><em>foo
<ul>
<li>
<ul>
<li></li>
</ul>
</li>
</ul>
baz</em></li>
</ul>
```

That's not valid HTML. The block elements need to be siblings with the inlines, and that can only happen in a tight list.

<details>

Events from pulldown-cmark:

```rust
"- *--\n  *  *\n  -*\n" -> [
  Start(List(None))
    Start(Item)
      Start(Emphasis)
        Text(Borrowed("--"))
        Start(List(None))
          Start(Item)
            Start(List(None))
              Start(Item)
              End(Item)
            End(List(false))
          End(Item)
        End(List(false))
        Text(Borrowed("-"))
      End(Emphasis)
    End(Item)
  End(List(false))
]
```

Events from pandoc:

```rust
"- *--\n  *  *\n  -*\n" -> [
  Start(List(None))
    Start(Item)
      Text(Boxed("*--"))
      Start(List(None))
        Start(Item)
          Start(List(None))
            Start(Item)
            End(Item)
          End(List(false))
        End(Item)
      End(List(false))
      Text(Boxed("-*"))
    End(Item)
  End(List(false))
]
```

Events from commonmark.js:

```rust
"- *--\n  *  *\n  -*\n" -> [
  Start(List(None))
    Start(Item)
      Text(Boxed("*--"))
      Start(List(None))
        Start(Item)
          Start(List(None))
            Start(Item)
            End(Item)
          End(List(false))
        End(Item)
      End(List(false))
      Text(Boxed("-*"))
    End(Item)
  End(List(false))
]
```

</details>


Pull request:
Do not allow inlines to cross over block structures
Fixes #797

Patch:
diff --git a/specs/regression.txt b/specs/regression.txt
index e3c3e2f..1a33ac8 100644
--- a/specs/regression.txt
+++ b/specs/regression.txt
@@ -2026,6 +2026,7 @@ ISSUE 791
 ````````````````````````````````
 
 
+
 ISSUE 793
 
 ```````````````````````````````` example
@@ -2057,4 +2058,61 @@ ISSUE 795
 <p>text</p>
 </blockquote>
 </blockquote>
-````````````````````````````````
\ No newline at end of file
+````````````````````````````````
+
+
+ISSUE 797
+
+```````````````````````````````` example
+- *foo
+  - - 
+  baz*
+.
+<ul>
+<li>*foo
+<ul>
+<li>
+<ul>
+<li></li>
+</ul>
+</li>
+</ul>
+baz*</li>
+</ul>
+````````````````````````````````
+
+```````````````````````````````` example
+- `foo
+  - - 
+  baz`
+.
+<ul>
+<li>`foo
+<ul>
+<li>
+<ul>
+<li></li>
+</ul>
+</li>
+</ul>
+baz`</li>
+</ul>
+````````````````````````````````
+
+```````````````````````````````` example
+- [foo
+  - - 
+  baz](https://example.com)
+.
+<ul>
+<li>[foo
+<ul>
+<li>
+<ul>
+<li></li>
+</ul>
+</li>
+</ul>
+baz](https://example.com)</li>
+</ul>
+````````````````````````````````
diff --git a/src/parse.rs b/src/parse.rs
index 1678d4b..7ce4ae4 100644
--- a/src/parse.rs
+++ b/src/parse.rs
@@ -119,6 +119,20 @@ impl ItemBody {
                 | ItemBody::MaybeImage
         )
     }
+    fn is_block(&self) -> bool {
+        matches!(
+            *self,
+            ItemBody::Paragraph
+                | ItemBody::BlockQuote
+                | ItemBody::List(..)
+                | ItemBody::ListItem(..)
+                | ItemBody::HtmlBlock
+                | ItemBody::Table(..)
+                | ItemBody::TableHead
+                | ItemBody::TableRow
+                | ItemBody::TableCell
+        )
+    }
 }
 
 impl Default for ItemBody {
@@ -332,6 +346,12 @@ impl<'input, F: BrokenLinkCallback<'input>> Parser<'input, F> {
                                     code_delims.insert(delim_count, scan_ix);
                                 }
                             }
+                            if self.tree[scan_ix].item.body.is_block() {
+                                // If this is a tight list, blocks and inlines might be
+                                // siblings. Inlines can't cross the boundary like that.
+                                scan = None;
+                                break;
+                            }
                             scan = self.tree[scan_ix].next;
                         }
                         if scan == None {
@@ -569,7 +589,15 @@ impl<'input, F: BrokenLinkCallback<'input>> Parser<'input, F> {
                         }
                     }
                 }
-                _ => (),
+                _ => {
+                    // If this is a tight list, blocks and inlines might be
+                    // siblings. Inlines can't cross the boundary like that.
+                    if let Some(cur_ix) = cur {
+                        if self.tree[cur_ix].item.body.is_block() {
+                            self.link_stack.clear();
+                        }
+                    }
+                }
             }
             prev = cur;
             cur = self.tree[cur_ix].next;
@@ -706,6 +734,13 @@ impl<'input, F: BrokenLinkCallback<'input>> Parser<'input, F> {
                 _ => {
                     prev = cur;
                     cur = self.tree[cur_ix].next;
+                    // If this is a tight list, blocks and inlines might be
+                    // siblings. Inlines can't cross the boundary like that.
+                    if let Some(cur_ix) = cur {
+                        if self.tree[cur_ix].item.body.is_block() {
+                            self.inline_stack.pop_all(&mut self.tree);
+                        }
+                    }
                 }
             }
         }


Test patch:
diff --git a/tests/suite/regression.rs b/tests/suite/regression.rs
index 23a161d..607ea0e 100644
--- a/tests/suite/regression.rs
+++ b/tests/suite/regression.rs
@@ -2390,6 +2390,7 @@ fn regression_test_148() {
     test_markdown_html(original, expected, false, false, false);
 }
 
+
 #[test]
 fn regression_test_149() {
     let original = r##"> > ```
@@ -2411,7 +2412,6 @@ fn regression_test_149() {
     test_markdown_html(original, expected, false, false, false);
 }
 
-
 #[test]
 fn regression_test_150() {
       let original = r##"[
@@ -2427,3 +2427,70 @@ a]: https://example.com
 
     test_markdown_html(original, expected, false, false, false);
 }
+
+
+#[test]
+fn regression_test_151() {
+    let original = r##"- *foo
+  - - 
+  baz*
+"##;
+    let expected = r##"<ul>
+<li>*foo
+<ul>
+<li>
+<ul>
+<li></li>
+</ul>
+</li>
+</ul>
+baz*</li>
+</ul>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn regression_test_152() {
+    let original = r##"- `foo
+  - - 
+  baz`
+"##;
+    let expected = r##"<ul>
+<li>`foo
+<ul>
+<li>
+<ul>
+<li></li>
+</ul>
+</li>
+</ul>
+baz`</li>
+</ul>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
+
+#[test]
+fn regression_test_153() {
+    let original = r##"- [foo
+  - - 
+  baz](https://example.com)
+"##;
+    let expected = r##"<ul>
+<li>[foo
+<ul>
+<li>
+<ul>
+<li></li>
+</ul>
+</li>
+</ul>
+baz](https://example.com)</li>
+</ul>
+"##;
+
+    test_markdown_html(original, expected, false, false, false);
+}
\ No newline at end of file