Repository navigation
fix(parser): make reset() safe inside callbacks #2539
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -553,10 +553,11 @@ export class Parser implements Callbacks { | |
| if (!this.isVoidElement(name)) { | ||
| const pos = this.stack.lastIndexOf(name); | ||
| if (pos !== -1) { | ||
| for (let index = this.stack.length - 1; index > pos; index--) { | ||
| // A callback may have reset the stack. | ||
| while (this.stack.length > pos + 1) { | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '495,585p' src/Parser.ts
sed -n '745,815p' src/Parser.ts
sed -n '280,330p' src/Parser.spec.ts
rg -n 'reset\(|onclosetag\(|writeIndex|onopentag' src/Parser.tsRepository: fb55/htmlparser2 Length of output: 7856 🏁 Script executed: set -o pipefail
printf '%s\n' '--- Parser methods ---'
sed -n '350,490p' src/Parser.ts
sed -n '690,755p' src/Parser.ts
printf '%s\n' '--- Tokenizer declarations and lifecycle ---'
rg -n -F --glob '*.ts' -- 'class Tokenizer' src
rg -n -F --glob '*.ts' -- 'reset()' src/Tokenizer.ts
rg -n -F --glob '*.ts' -- 'write(' src/Tokenizer.ts
sed -n '1,180p' src/Tokenizer.ts
sed -n '380,500p' src/Tokenizer.ts
printf '%s\n' '--- reset identity/reentrancy candidates ---'
rg -n -F --glob '*.ts' -- 'writeIndex' src
rg -n -F --glob '*.ts' -- 'tokenizer.running' src
rg -n -F --glob '*.ts' -- 'onclosetag' src/Parser.ts src/Tokenizer.tsRepository: fb55/htmlparser2 Length of output: 18885 🏁 Script executed: set -o pipefail
printf '%s\n' '--- Tokenizer state and lifecycle ---'
sed -n '180,250p' src/Tokenizer.ts
sed -n '1125,1195p' src/Tokenizer.ts
printf '%s\n' '--- Parser write drain and end ---'
sed -n '805,850p' src/Parser.ts
printf '%s\n' '--- Reset/write related tests ---'
rg -n -F --glob '*spec.ts' -- 'reset()' src test 2>/dev/null || true
rg -n -F --glob '*spec.ts' -- 'write(' src/Parser.spec.ts | tail -30Repository: fb55/htmlparser2 Length of output: 5730 Stop the old close-tag unwind after When an Track reset identity and stop both the unwind and matched-element pop when it changes. 🐛 Suggested fix+ private resetCount = 0;
private writeIndex = 0; if (!this.isVoidElement(name)) {
const pos = this.stack.lastIndexOf(name);
if (pos !== -1) {
+ const resetCount = this.resetCount;
// A callback may have reset the stack.
- while (this.stack.length > pos + 1) {
+ while (
+ this.resetCount === resetCount &&
+ this.stack.length > pos + 1
+ ) {
this.popElement(true);
}
- if (this.stack.length === pos + 1) this.popElement(false);
+ if (
+ this.resetCount === resetCount &&
+ this.stack.length === pos + 1
+ ) {
+ this.popElement(false);
+ } reset(): void {
+ this.resetCount++;
this.cbs.onreset?.();🤖 Prompt for AI Agents |
||
| this.popElement(true); | ||
|
Comment on lines
+557
to
558
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The stack length does not establish that the elements still belong to the document whose closing tag is being processed. If an Useful? React with 👍 / 👎. |
||
| } | ||
| this.popElement(false); | ||
| if (this.stack.length === pos + 1) this.popElement(false); | ||
|
Comment on lines
+557
to
+560
Comment on lines
+557
to
+560
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While parsing
Comment on lines
+557
to
+560
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
| } else if (this.htmlMode && name === "p") { | ||
| // Implicit open before close | ||
| this.emitOpenTag("p"); | ||
|
|
@@ -790,8 +791,9 @@ export class Parser implements Callbacks { | |
|
|
||
| this.buffers.push(chunk); | ||
| if (this.tokenizer.running) { | ||
| this.tokenizer.write(chunk); | ||
| // Count the chunk first, as a callback may call `reset()`. | ||
| this.writeIndex++; | ||
| this.tokenizer.write(chunk); | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The close-callback reset fix still leaves the parser unable to read the next document correctly. After this test's
write("<a><b><c></a>"), a subsequentwrite("<x>")emits textx>instead of openingx. The tokenizer continues after the callback and changes its resetindexandsectionStartfrom zero to one. Extend this test to parse another chunk, and stop the old tokenizer work after a reset.