Skip to content

Commit

Permalink
Fixed #503, prevent infinite loops in parser and runaway code examples.
Browse files Browse the repository at this point in the history
  • Loading branch information
amyjko committed Jun 29, 2024
1 parent 587c413 commit 4dfab5b
Show file tree
Hide file tree
Showing 8 changed files with 459 additions and 337 deletions.
14 changes: 14 additions & 0 deletions src/parser/Parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,3 +356,17 @@ test('docs in docs', () => {
expect(doc.markup.paragraphs[0].segments[2]).toBeInstanceOf(Token);
expect(doc.markup.paragraphs[0].segments.length).toBe(3);
});

test('unparsables in docs', () => {
const doc = parseDoc(
toTokens(
"``This is a broken example ina doc: \\∆\\. Don't you see it?``",
),
);
expect(doc).toBeInstanceOf(Doc);
expect(doc.markup.paragraphs[0]).toBeInstanceOf(Paragraph);
expect(doc.markup.paragraphs[0].segments[0]).toBeInstanceOf(Token);
expect(doc.markup.paragraphs[0].segments[1]).toBeInstanceOf(Example);
expect(doc.markup.paragraphs[0].segments[2]).toBeInstanceOf(Token);
expect(doc.markup.paragraphs[0].segments.length).toBe(3);
});
45 changes: 37 additions & 8 deletions src/parser/Tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,23 +178,52 @@ export default class Tokens {
return this.nextIs(type) ? this.read() : undefined;
}

/** Used to read the remainder of a line, and at least one token, unless there are no more tokens. */
/** Used to read the remainder of a line, unless there are no more tokens, or we reach the end of a code example. */
readLine() {
const nodes: Token[] = [];

if (!this.hasNext()) return nodes;
// Read at least one token, then keep going until we reach a token with a line break.
do {
const next = this.read();
nodes.push(next);
} while (
this.hasNext() &&
this.nextHasPrecedingLineBreak() === false &&
this.nextIsnt(Sym.Code)
this.untilDo(
() =>
this.hasNext() &&
this.nextHasPrecedingLineBreak() === false &&
this.nextIsnt(Sym.Code),
() => {
const next = this.read();
nodes.push(next);
},
);
return nodes;
}

/**
* Checks a condition, if if true, does something, then checks again.
* If the action returns false, we stop.
* If the action doesn't consume a token, we stop, to prevent infinite loops.
**/
untilDo(condition: () => boolean, action: () => unknown) {
while (condition()) {
const currentToken = this.peek();
if (action() === false) break;
// If we didn't advance, then we're stuck in an infinite loop. Break.
if (currentToken === this.peek()) break;
}
}

/**
* Completes an action, then checks a condition, and stops if false, otherwise repeats. If the action returns false, we stop.
* If the action doesn't consume a token, we stop, to prevent infinite loops.
**/
doUntil(action: () => unknown, condition: () => boolean) {
do {
const currentToken = this.peek();
if (action() === false) break;
// If we didn't advance, then we're stuck in an infinite loop. Break.
if (currentToken === this.peek()) break;
} while (condition());
}

/** Rollback to the given token. */
unreadTo(token: Token) {
while (this.#read.length > 0 && this.#unread[0] !== token) {
Expand Down
50 changes: 26 additions & 24 deletions src/parser/parseBind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,31 +40,33 @@ export default function parseBind(tokens: Tokens): Bind {
export function parseNames(tokens: Tokens): Names {
const names: Name[] = [];

while (
(tokens.hasNext() &&
names.length > 0 &&
tokens.nextIs(Sym.Separator)) ||
(names.length === 0 &&
tokens.nextIsOneOf(Sym.Name, Sym.Placeholder, Sym.Operator))
) {
const comma = tokens.nextIs(Sym.Separator)
? tokens.read(Sym.Separator)
: undefined;
if (names.length > 0 && comma === undefined) break;
const name = tokens.nextIs(Sym.Name)
? tokens.read(Sym.Name)
: tokens.nextIs(Sym.Placeholder)
? tokens.read(Sym.Placeholder)
: tokens.nextIs(Sym.Operator)
? tokens.read(Sym.Operator)
tokens.untilDo(
() =>
(tokens.hasNext() &&
names.length > 0 &&
tokens.nextIs(Sym.Separator)) ||
(names.length === 0 &&
tokens.nextIsOneOf(Sym.Name, Sym.Placeholder, Sym.Operator)),
() => {
const comma = tokens.nextIs(Sym.Separator)
? tokens.read(Sym.Separator)
: undefined;
const lang = tokens.nextIs(Sym.Language)
? parseLanguage(tokens)
: undefined;
if (comma !== undefined || name !== undefined)
names.push(new Name(comma, name, lang));
else break;
}
if (names.length > 0 && comma === undefined) return false;
const name = tokens.nextIs(Sym.Name)
? tokens.read(Sym.Name)
: tokens.nextIs(Sym.Placeholder)
? tokens.read(Sym.Placeholder)
: tokens.nextIs(Sym.Operator)
? tokens.read(Sym.Operator)
: undefined;
const lang = tokens.nextIs(Sym.Language)
? parseLanguage(tokens)
: undefined;
if (comma !== undefined || name !== undefined)
names.push(new Name(comma, name, lang));
else return false;
},
);

return new Names(names);
}
Expand Down
Loading

0 comments on commit 4dfab5b

Please sign in to comment.