Skip to content

Commit f40ff54

Browse files
authored
Merge pull request #422 from spirit-at-canva/spirit-add-node-depth-limit
add node depth limit
2 parents e62bca1 + 0308e34 commit f40ff54

File tree

3 files changed

+143
-5
lines changed

3 files changed

+143
-5
lines changed

commonmark/src/main/java/org/commonmark/internal/DocumentParser.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ public class DocumentParser implements ParserState {
7676
private final List<LinkProcessor> linkProcessors;
7777
private final Set<Character> linkMarkers;
7878
private final IncludeSourceSpans includeSourceSpans;
79+
private final int maxOpenBlockParsers;
7980
private final DocumentBlockParser documentBlockParser;
8081
private final Definitions definitions = new Definitions();
8182

@@ -84,14 +85,16 @@ public class DocumentParser implements ParserState {
8485

8586
public DocumentParser(List<BlockParserFactory> blockParserFactories, InlineParserFactory inlineParserFactory,
8687
List<InlineContentParserFactory> inlineContentParserFactories, List<DelimiterProcessor> delimiterProcessors,
87-
List<LinkProcessor> linkProcessors, Set<Character> linkMarkers, IncludeSourceSpans includeSourceSpans) {
88+
List<LinkProcessor> linkProcessors, Set<Character> linkMarkers,
89+
IncludeSourceSpans includeSourceSpans, int maxOpenBlockParsers) {
8890
this.blockParserFactories = blockParserFactories;
8991
this.inlineParserFactory = inlineParserFactory;
9092
this.inlineContentParserFactories = inlineContentParserFactories;
9193
this.delimiterProcessors = delimiterProcessors;
9294
this.linkProcessors = linkProcessors;
9395
this.linkMarkers = linkMarkers;
9496
this.includeSourceSpans = includeSourceSpans;
97+
this.maxOpenBlockParsers = maxOpenBlockParsers;
9598

9699
this.documentBlockParser = new DocumentBlockParser();
97100
activateBlockParser(new OpenBlockParser(documentBlockParser, 0));
@@ -461,6 +464,9 @@ private void addSourceSpans() {
461464
}
462465

463466
private BlockStartImpl findBlockStart(BlockParser blockParser) {
467+
if (openBlockParsers.size() > maxOpenBlockParsers) {
468+
return null;
469+
}
464470
MatchedBlockParser matchedBlockParser = new MatchedBlockParserImpl(blockParser);
465471
for (BlockParserFactory blockParserFactory : blockParserFactories) {
466472
BlockStart result = blockParserFactory.tryStart(this, matchedBlockParser);

commonmark/src/main/java/org/commonmark/parser/Parser.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public class Parser {
3737
private final InlineParserFactory inlineParserFactory;
3838
private final List<PostProcessor> postProcessors;
3939
private final IncludeSourceSpans includeSourceSpans;
40+
private final int maxOpenBlockParsers;
4041

4142
private Parser(Builder builder) {
4243
this.blockParserFactories = DocumentParser.calculateBlockParserFactories(builder.blockParserFactories, builder.enabledBlockTypes);
@@ -47,6 +48,7 @@ private Parser(Builder builder) {
4748
this.linkProcessors = builder.linkProcessors;
4849
this.linkMarkers = builder.linkMarkers;
4950
this.includeSourceSpans = builder.includeSourceSpans;
51+
this.maxOpenBlockParsers = builder.maxOpenBlockParsers;
5052

5153
// Try to construct an inline parser. Invalid configuration might result in an exception, which we want to
5254
// detect as soon as possible.
@@ -106,7 +108,7 @@ public Node parseReader(Reader input) throws IOException {
106108

107109
private DocumentParser createDocumentParser() {
108110
return new DocumentParser(blockParserFactories, inlineParserFactory, inlineContentParserFactories,
109-
delimiterProcessors, linkProcessors, linkMarkers, includeSourceSpans);
111+
delimiterProcessors, linkProcessors, linkMarkers, includeSourceSpans, maxOpenBlockParsers);
110112
}
111113

112114
private Node postProcess(Node document) {
@@ -129,6 +131,7 @@ public static class Builder {
129131
private Set<Class<? extends Block>> enabledBlockTypes = DocumentParser.getDefaultBlockParserTypes();
130132
private InlineParserFactory inlineParserFactory;
131133
private IncludeSourceSpans includeSourceSpans = IncludeSourceSpans.NONE;
134+
private int maxOpenBlockParsers = Integer.MAX_VALUE;
132135

133136
/**
134137
* @return the configured {@link Parser}
@@ -200,6 +203,27 @@ public Builder includeSourceSpans(IncludeSourceSpans includeSourceSpans) {
200203
return this;
201204
}
202205

206+
/**
207+
* Limit how many block parsers may be open at once while parsing.
208+
* <p>
209+
* Once the limit is reached, additional block starts are treated as plain text instead of
210+
* creating deeper nested block structure.
211+
* <p>
212+
* The document root parser is not counted. The default is unlimited, so callers that keep
213+
* using {@code Parser.builder().build()} preserve behavior.
214+
*
215+
* @param maxOpenBlockParsers maximum number of open non-document block parsers, must be
216+
* zero or greater
217+
* @return {@code this}
218+
*/
219+
public Builder maxOpenBlockParsers(int maxOpenBlockParsers) {
220+
if (maxOpenBlockParsers < 0) {
221+
throw new IllegalArgumentException("maxOpenBlockParsers must be >= 0");
222+
}
223+
this.maxOpenBlockParsers = maxOpenBlockParsers;
224+
return this;
225+
}
226+
203227
/**
204228
* Add a custom block parser factory.
205229
* <p>

commonmark/src/test/java/org/commonmark/test/ParserTest.java

Lines changed: 111 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
import org.commonmark.node.*;
44
import org.commonmark.parser.*;
5-
import org.commonmark.parser.block.*;
65
import org.commonmark.renderer.html.HtmlRenderer;
6+
import org.commonmark.renderer.markdown.MarkdownRenderer;
77
import org.commonmark.testutil.TestResources;
88
import org.junit.jupiter.api.Test;
99

@@ -15,8 +15,6 @@
1515
import java.util.HashSet;
1616
import java.util.List;
1717
import java.util.Set;
18-
import java.util.concurrent.Callable;
19-
import java.util.concurrent.ExecutorService;
2018
import java.util.concurrent.Executors;
2119
import java.util.concurrent.Future;
2220

@@ -135,11 +133,121 @@ public void threading() throws Exception {
135133
}
136134
}
137135

136+
@Test
137+
public void maxOpenBlockParsersMustBeZeroOrGreater() {
138+
assertThatThrownBy(() ->
139+
Parser.builder().maxOpenBlockParsers(-1)).isInstanceOf(IllegalArgumentException.class);
140+
}
141+
142+
@Test
143+
public void maxOpenBlockParsersIsOptIn() {
144+
var parser = Parser.builder().build();
145+
146+
var document = parser.parse(alternatingNestedList(9));
147+
148+
assertThat(renderText(deepestStructuredParagraph(document, 9))).isEqualTo("level9");
149+
}
150+
151+
@Test
152+
public void maxOpenBlockParsersPreservesSevenLogicalListLevelsAtSeventeenBlocks() {
153+
var parser = Parser.builder().maxOpenBlockParsers(17).build();
154+
155+
var document = parser.parse(alternatingNestedList(7));
156+
157+
assertThat(renderText(deepestStructuredParagraph(document, 7))).isEqualTo("level7");
158+
}
159+
160+
@Test
161+
public void maxOpenBlockParsersPreservesEightLogicalListLevelsAtSeventeenBlocks() {
162+
var parser = Parser.builder().maxOpenBlockParsers(17).build();
163+
164+
var document = parser.parse(alternatingNestedList(8));
165+
166+
assertThat(renderText(deepestStructuredParagraph(document, 8))).isEqualTo("level8");
167+
}
168+
169+
@Test
170+
public void maxOpenBlockParsersDegradesTheNinthLogicalListLevelToPlainText() {
171+
var parser = Parser.builder().maxOpenBlockParsers(17).build();
172+
173+
var document = parser.parse(alternatingNestedList(9));
174+
var deepestParagraph = deepestStructuredParagraph(document, 8);
175+
176+
assertThat(renderText(deepestParagraph)).isEqualTo("level8\n\\- level9");
177+
assertThat(deepestParagraph.getNext()).isNull();
178+
}
179+
180+
@Test
181+
public void maxOpenBlockParsersAlsoLimitsMixedListAndBlockQuoteNesting() {
182+
var parser = Parser.builder().maxOpenBlockParsers(5).build();
183+
184+
var document = parser.parse(String.join("\n",
185+
"- level1",
186+
" > level2",
187+
" > > level3",
188+
" > > > level4"));
189+
190+
var listBlock = document.getFirstChild();
191+
assertThat(listBlock).isInstanceOf(BulletList.class);
192+
193+
var listItem = listBlock.getFirstChild();
194+
var blockQuote1 = listItem.getLastChild();
195+
assertThat(blockQuote1).isInstanceOf(BlockQuote.class);
196+
197+
var blockQuote2 = blockQuote1.getLastChild();
198+
assertThat(blockQuote2).isInstanceOf(BlockQuote.class);
199+
200+
var deepestParagraph = blockQuote2.getLastChild();
201+
assertThat(deepestParagraph).isInstanceOf(Paragraph.class);
202+
assertThat(renderText(deepestParagraph)).isEqualTo("level3\n\\> level4");
203+
assertThat(deepestParagraph.getNext()).isNull();
204+
}
205+
138206
private String firstText(Node n) {
139207
while (!(n instanceof Text)) {
140208
assertThat(n).isNotNull();
141209
n = n.getFirstChild();
142210
}
143211
return ((Text) n).getLiteral();
144212
}
213+
214+
private Paragraph deepestStructuredParagraph(Node document, int levels) {
215+
Node node = document.getFirstChild();
216+
for (int level = 1; level <= levels; level++) {
217+
assertThat(node).isInstanceOf(ListBlock.class);
218+
var listItem = node.getFirstChild();
219+
assertThat(listItem).isNotNull();
220+
if (level == levels) {
221+
assertThat(listItem.getFirstChild()).isInstanceOf(Paragraph.class);
222+
return (Paragraph) listItem.getFirstChild();
223+
}
224+
node = listItem.getLastChild();
225+
}
226+
throw new AssertionError("unreachable");
227+
}
228+
229+
private String renderText(Node node) {
230+
return MarkdownRenderer.builder().build().render(node).trim();
231+
}
232+
233+
private String alternatingNestedList(int levels) {
234+
int indent = 0;
235+
var lines = new ArrayList<String>();
236+
for (int level = 1; level <= levels; level++) {
237+
var ordered = level % 2 == 0;
238+
var marker = ordered ? "1. " : "- ";
239+
lines.add(" ".repeat(indent) + marker + "level" + level);
240+
indent += marker.length();
241+
}
242+
return String.join("\n", lines);
243+
}
244+
245+
private int depth(Node node) {
246+
int depth = 0;
247+
while (node.getParent() != null) {
248+
node = node.getParent();
249+
depth++;
250+
}
251+
return depth;
252+
}
145253
}

0 commit comments

Comments
 (0)