links-notation 0.21.3 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -3
- package/README.ru.md +17 -0
- package/dist/index.js +182 -111
- package/index.d.ts +15 -1
- package/package.json +1 -1
- package/src/ParseError.js +8 -1
- package/src/Parser.js +12 -3
- package/src/grammar.pegjs +40 -3
- package/src/index.js +1 -1
- package/src/parser-generated.js +179 -108
- package/tests/NestingLimit.test.js +132 -0
- package/tests/support/nestingDepthWorker.js +3 -1
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Links nested too deeply are refused with an error rather than recursed into
|
|
2
|
+
// until the stack overflows
|
|
3
|
+
// (https://github.com/link-foundation/links-notation/issues/315).
|
|
4
|
+
//
|
|
5
|
+
// Every parenthesized group and every indentation level is one level, and the
|
|
6
|
+
// lines of a document start at level 0. The positions asserted here are the
|
|
7
|
+
// ones the Rust port reports for the same input.
|
|
8
|
+
|
|
9
|
+
import { test, expect } from 'bun:test';
|
|
10
|
+
import {
|
|
11
|
+
Parser,
|
|
12
|
+
ParseError,
|
|
13
|
+
StreamParser,
|
|
14
|
+
StreamParseError,
|
|
15
|
+
DEFAULT_MAX_DEPTH,
|
|
16
|
+
} from '../src/index.js';
|
|
17
|
+
|
|
18
|
+
const parens = (depth) => '('.repeat(depth) + 'a' + ')'.repeat(depth);
|
|
19
|
+
|
|
20
|
+
const values = (depth) => '(a '.repeat(depth) + 'b' + ')'.repeat(depth);
|
|
21
|
+
|
|
22
|
+
const indentation = (depth) =>
|
|
23
|
+
Array.from(
|
|
24
|
+
{ length: depth + 1 },
|
|
25
|
+
(_, level) => ' '.repeat(level) + 'a\n'
|
|
26
|
+
).join('');
|
|
27
|
+
|
|
28
|
+
function tooDeep(document, maxDepth) {
|
|
29
|
+
try {
|
|
30
|
+
const links = new Parser({ maxDepth }).parse(document);
|
|
31
|
+
throw new Error(
|
|
32
|
+
`expected ${JSON.stringify(document)} to be too deep, got ${links.length} links`
|
|
33
|
+
);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
expect(error).toBeInstanceOf(ParseError);
|
|
36
|
+
expect(error.maxDepth).toBe(maxDepth);
|
|
37
|
+
return error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const accepted = (document, maxDepth) =>
|
|
42
|
+
new Parser({ maxDepth }).parse(document).length > 0;
|
|
43
|
+
|
|
44
|
+
test('the default limit is shared by every implementation', () => {
|
|
45
|
+
expect(DEFAULT_MAX_DEPTH).toBe(64);
|
|
46
|
+
expect(new Parser().maxDepth).toBe(DEFAULT_MAX_DEPTH);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('parentheses up to the limit are accepted', () => {
|
|
50
|
+
expect(accepted(parens(3), 3)).toBe(true);
|
|
51
|
+
expect(accepted(values(3), 3)).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('parentheses past the limit are refused at the group that is too deep', () => {
|
|
55
|
+
const error = tooDeep(parens(4), 3);
|
|
56
|
+
|
|
57
|
+
expect([error.line, error.column, error.offset]).toEqual([1, 4, 3]);
|
|
58
|
+
expect(error.message).toBe(
|
|
59
|
+
'Nesting too deep at line 1, column 4: nesting depth exceeds the maximum of 3\n' +
|
|
60
|
+
'1 | ((((a))))\n' +
|
|
61
|
+
' | ^'
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('groups in value position count like any other group', () => {
|
|
66
|
+
const error = tooDeep(values(4), 3);
|
|
67
|
+
|
|
68
|
+
expect([error.line, error.column]).toEqual([1, 10]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('indentation up to the limit is accepted', () => {
|
|
72
|
+
expect(accepted(indentation(3), 3)).toBe(true);
|
|
73
|
+
expect(accepted(indentation(DEFAULT_MAX_DEPTH), undefined)).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('indentation past the limit is refused at the line that is too deep', () => {
|
|
77
|
+
const error = tooDeep(indentation(4), 3);
|
|
78
|
+
|
|
79
|
+
expect([error.line, error.column]).toEqual([5, 5]);
|
|
80
|
+
expect(error.lineText).toBe(' a');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('groups and indentation add up', () => {
|
|
84
|
+
// `(b)` on the line indented once is at level 2.
|
|
85
|
+
expect(accepted('a\n (b)\n', 2)).toBe(true);
|
|
86
|
+
const error = tooDeep('a\n (b)\n', 1);
|
|
87
|
+
|
|
88
|
+
expect([error.line, error.column]).toEqual([2, 3]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('trailing spaces on a deep line are not a deeper line', () => {
|
|
92
|
+
expect(accepted('a\n b\n c \n', 2)).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('a syntax error is not mistaken for nesting that is too deep', () => {
|
|
96
|
+
try {
|
|
97
|
+
new Parser().parse('a: b: c');
|
|
98
|
+
throw new Error('expected a syntax error');
|
|
99
|
+
} catch (error) {
|
|
100
|
+
expect(error).toBeInstanceOf(ParseError);
|
|
101
|
+
expect(error.maxDepth).toBeNull();
|
|
102
|
+
expect(error.message).toStartWith('Syntax error at ');
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('refuses a document far past the limit instead of overflowing the stack', () => {
|
|
107
|
+
// Before the limit existed each of these ran out of stack.
|
|
108
|
+
for (const document of [
|
|
109
|
+
parens(100_000),
|
|
110
|
+
values(100_000),
|
|
111
|
+
indentation(2_000),
|
|
112
|
+
]) {
|
|
113
|
+
const error = tooDeep(document, DEFAULT_MAX_DEPTH);
|
|
114
|
+
expect(error.message).toStartWith('Nesting too deep at ');
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('the stream parser reports where the nesting is too deep', () => {
|
|
119
|
+
const stream = new StreamParser({ maxDepth: 1 });
|
|
120
|
+
stream.write('a\nb ((c))\n');
|
|
121
|
+
|
|
122
|
+
let error;
|
|
123
|
+
try {
|
|
124
|
+
stream.end();
|
|
125
|
+
} catch (thrown) {
|
|
126
|
+
error = thrown;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
expect(error).toBeInstanceOf(StreamParseError);
|
|
130
|
+
expect(error.cause.maxDepth).toBe(1);
|
|
131
|
+
expect([error.line, error.column, error.offset]).toEqual([2, 4, 5]);
|
|
132
|
+
});
|
|
@@ -4,7 +4,9 @@ import { parentPort } from 'node:worker_threads';
|
|
|
4
4
|
import { Parser } from '../../src/Parser.js';
|
|
5
5
|
import { formatLinks } from '../../src/Link.js';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
// The documents nest deeper than the default limit on purpose; only the time
|
|
8
|
+
// they take is measured here.
|
|
9
|
+
const parser = new Parser({ maxDepth: Infinity });
|
|
8
10
|
|
|
9
11
|
parentPort.on('message', (source) => {
|
|
10
12
|
try {
|