links-notation 0.21.3 → 0.23.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/src/ParseError.js CHANGED
@@ -12,6 +12,9 @@ const ELLIPSIS = '...';
12
12
  * message it writes says what it expected without saying where. This error puts
13
13
  * the line and the column in the message as well, and quotes the offending line
14
14
  * with a caret under it, the way the Rust and C# ports do.
15
+ *
16
+ * A document nested deeper than the parser's maxDepth is refused with this
17
+ * error too; then maxDepth says how deep the nesting may go.
15
18
  */
16
19
  export class ParseError extends Error {
17
20
  /**
@@ -24,7 +27,9 @@ export class ParseError extends Error {
24
27
  const lineText = lineAt(input, start.offset);
25
28
  const summary = `line ${start.line}, column ${start.column}: ${error.message}`;
26
29
  const snippet = quote(start.line, lineText, start.column);
27
- super(`Syntax error at ${summary}\n${snippet}`);
30
+ const maxDepth = error?.maxDepth ?? null;
31
+ const kind = maxDepth === null ? 'Syntax error' : 'Nesting too deep';
32
+ super(`${kind} at ${summary}\n${snippet}`);
28
33
 
29
34
  this.name = 'ParseError';
30
35
  /** @type {Error} The error the generated parser threw */
@@ -43,6 +48,8 @@ export class ParseError extends Error {
43
48
  this.lineText = lineText;
44
49
  /** @type {string} The offending line with a caret under the offending column */
45
50
  this.snippet = snippet;
51
+ /** @type {number|null} The deepest nesting allowed, when the document is nested deeper; null for any other error */
52
+ this.maxDepth = maxDepth;
46
53
  }
47
54
  }
48
55
 
package/src/Parser.js CHANGED
@@ -3,17 +3,24 @@ import { ParseError } from './ParseError.js';
3
3
  import { stripComments } from './comments.js';
4
4
  import * as parserModule from './parser-generated.js';
5
5
 
6
+ /**
7
+ * How deep links may nest when nothing says otherwise. The parser recurses once
8
+ * per level, and the same limit applies in every Links Notation implementation,
9
+ * so a document one of them reads is not too deep for another.
10
+ */
11
+ export const DEFAULT_MAX_DEPTH = 64;
12
+
6
13
  export class Parser {
7
14
  /**
8
15
  * Create a new Parser instance
9
16
  * @param {Object} options - Parser options
10
17
  * @param {number} options.maxInputSize - Maximum input size in bytes (default: 10MB)
11
- * @param {number} options.maxDepth - Maximum nesting depth (default: 1000)
18
+ * @param {number} options.maxDepth - How deep links may nest: every parenthesized group and every indentation level is one level (default: 64)
12
19
  * @param {boolean} options.comments - If false, read `#` as an ordinary character instead of the start of a comment (default: true)
13
20
  */
14
21
  constructor(options = {}) {
15
22
  this.maxInputSize = options.maxInputSize || 10 * 1024 * 1024; // 10MB default
16
- this.maxDepth = options.maxDepth || 1000;
23
+ this.maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
17
24
  this.comments = options.comments ?? true;
18
25
  }
19
26
 
@@ -40,7 +47,9 @@ export class Parser {
40
47
  const prepared = this.comments ? stripComments(input) : input;
41
48
 
42
49
  try {
43
- const rawResult = parserModule.parse(prepared);
50
+ const rawResult = parserModule.parse(prepared, {
51
+ maxDepth: this.maxDepth,
52
+ });
44
53
  return this.transformResult(rawResult);
45
54
  } catch (error) {
46
55
  // A syntax error knows where it stopped; anything else is passed on with
package/src/grammar.pegjs CHANGED
@@ -9,6 +9,12 @@
9
9
  // group opens a nested context that starts fresh at indentation level zero,
10
10
  // so line breaks and indentation mean the same thing at every depth.
11
11
  let contextStack = [];
12
+ // How deeply the current context is nested: the enclosing groups and the
13
+ // indentation levels around them. Every level is a level of recursion, so
14
+ // links nested deeper than maxDepth are refused rather than recursed into
15
+ // until the stack overflows.
16
+ let contextDepth = 0;
17
+ const maxDepth = options.maxDepth ?? Infinity;
12
18
  // Lines already found unreadable, keyed by where they start and whether they
13
19
  // are inside a parenthesised group. Reading a line depends on nothing else: a
14
20
  // group starts a fresh indentation context, and indentation only decides
@@ -22,6 +28,7 @@
22
28
  indentationStack = [0];
23
29
  baseIndentation = null;
24
30
  contextStack = [];
31
+ contextDepth = 0;
25
32
  unreadableLines = new Set();
26
33
  return true;
27
34
  }
@@ -31,7 +38,8 @@
31
38
  }
32
39
 
33
40
  function enterNestedContext() {
34
- contextStack.push({ indentationStack, baseIndentation });
41
+ contextStack.push({ indentationStack, baseIndentation, contextDepth });
42
+ contextDepth = depth() + 1;
35
43
  indentationStack = [0];
36
44
  baseIndentation = null;
37
45
  return true;
@@ -42,10 +50,32 @@
42
50
  if (saved) {
43
51
  indentationStack = saved.indentationStack;
44
52
  baseIndentation = saved.baseIndentation;
53
+ contextDepth = saved.contextDepth;
45
54
  }
46
55
  return true;
47
56
  }
48
57
 
58
+ // The nesting depth of the line being read: every enclosing group and every
59
+ // indentation level is one level, and the lines of a document are at level 0.
60
+ function depth() {
61
+ return contextDepth + indentationStack.length - 1;
62
+ }
63
+
64
+ // Refuses links at the given depth when it is deeper than maxDepth. The error
65
+ // is thrown rather than returned as a failed match, so no alternative is
66
+ // tried in its place, and says where the nesting got too deep.
67
+ function checkDepth(levels, where) {
68
+ if (levels <= maxDepth) {
69
+ return true;
70
+ }
71
+ try {
72
+ error(`nesting depth exceeds the maximum of ${maxDepth}`, where);
73
+ } catch (tooDeep) {
74
+ tooDeep.maxDepth = maxDepth;
75
+ throw tooDeep;
76
+ }
77
+ }
78
+
49
79
  function isInsideNestedContext() {
50
80
  return contextStack.length > 0;
51
81
  }
@@ -115,8 +145,11 @@ line = CHECK_INDENTATION l:element { return l; }
115
145
 
116
146
  // A line is read once, whether or not indented children follow it: reading it
117
147
  // again after looking for children doubled the work at every level of nesting.
148
+ // Only a line that parsed counts towards the depth, so trailing spaces indented
149
+ // past the limit are still read as the whitespace they are.
118
150
  element = &{ return !unreadableLines.has(lineKey(offset())); }
119
- e:anyLink saved:SAVE_INDENTATION l:(PUSH_INDENTATION @links)? {
151
+ start:HERE e:anyLink &{ return checkDepth(depth(), start); }
152
+ saved:SAVE_INDENTATION l:(PUSH_INDENTATION @links)? {
120
153
  if (l === null) {
121
154
  // No child line followed the indentation. Forget what looking for one
122
155
  // pushed, so the next line is compared with this line's indentation.
@@ -155,7 +188,8 @@ singleLineAnyLink = fl:singleLineLink eol { return fl; }
155
188
  // A parenthesised group opens a nested context that follows exactly the same
156
189
  // rules as the root of the document: line breaks separate links and
157
190
  // indentation nests them, starting fresh at indentation level zero.
158
- nestedGroup = "(" ENTER_NESTED_CONTEXT body:nestedGroupBody {
191
+ nestedGroup = start:HERE "(" &{ return checkDepth(depth() + 1, start); }
192
+ ENTER_NESTED_CONTEXT body:nestedGroupBody {
159
193
  exitNestedContext();
160
194
  return body;
161
195
  }
@@ -168,6 +202,9 @@ nestedGroupBody = skipEmptyLines l:links _ ")" { return { nested: l }; }
168
202
 
169
203
  ENTER_NESTED_CONTEXT = &{ return enterNestedContext(); }
170
204
 
205
+ // Where the parser is, to say where nesting got too deep.
206
+ HERE = "" { return location(); }
207
+
171
208
  singleLineValueAndWhitespace = __ value:referenceOrLink { return value; }
172
209
 
173
210
  singleLineValues = list:singleLineValueAndWhitespace+ { return list; }
package/src/index.js CHANGED
@@ -1,8 +1,20 @@
1
1
  export { Link, formatLinks } from './Link.js';
2
2
  export { LinksGroup } from './LinksGroup.js';
3
- export { Parser } from './Parser.js';
3
+ export { Parser, DEFAULT_MAX_DEPTH } from './Parser.js';
4
4
  export { StreamParseError, StreamParser } from './StreamParser.js';
5
5
  export { ParseError } from './ParseError.js';
6
6
  export { FormatConfig } from './FormatConfig.js';
7
7
  export { FormatOptions } from './FormatOptions.js';
8
8
  export { stripComments } from './comments.js';
9
+ export {
10
+ ArityRange,
11
+ BinaryLinoCodec,
12
+ BinaryLinoOptions,
13
+ DecodeLimits,
14
+ External,
15
+ LinksPacket,
16
+ PacketReader,
17
+ Section,
18
+ formatBinaryDocument,
19
+ formatBinaryReference,
20
+ } from './Binary.js';