@tabnas/proto 0.6.7 → 0.6.8

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 CHANGED
@@ -47,6 +47,9 @@ agree (`reconcile: true`, the default) or `parse` throws; set
47
47
  grammar so `tn.parse(src)` returns the raw `{rule, src, kids}` CST.
48
48
  - `toDescriptor(cst, options?)`. Turn a parsed CST into a
49
49
  FileDescriptorProto.
50
+ - `preflight(src)`. Refuse a source nesting deeper than
51
+ `MAX_NESTING_DEPTH`, which is 100 levels. `parse` runs it first, and a
52
+ caller who drives the engine runs it before `tn.parse`.
50
53
 
51
54
  Options: `{ version?: 'proto2'|'proto3'|'2023'|'2024' | null,
52
55
  reconcile?: boolean }`.
@@ -0,0 +1,2 @@
1
+ export declare const MAX_NESTING_DEPTH = 100;
2
+ export declare function preflight(src: string): void;
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+ /* Copyright (c) 2026 Richard Rodger and other contributors, MIT License */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.MAX_NESTING_DEPTH = void 0;
5
+ exports.preflight = preflight;
6
+ // The nesting preflight: a `.proto` document nesting deeper than
7
+ // MAX_NESTING_DEPTH is refused before the engine builds a tree that deep.
8
+ //
9
+ // The tree costs more than its nesting suggests. A CST node's `src` holds
10
+ // every token beneath it, so a document nested `d` levels deep builds
11
+ // source text that grows with d squared, and so do the parse's time and
12
+ // memory: the walk here overflows its stack from about 3,000 levels, after
13
+ // seconds and hundreds of megabytes; Go's parse grows past any memory a
14
+ // machine has; and a Rust stack that runs out aborts the process.
15
+ // DIVERGENCE.md section 2 has the measurements. So every runtime refuses
16
+ // the same documents, with the same message, at the same cap: the Rust
17
+ // port's, which sits more than an order of magnitude past anything a
18
+ // hand-written `.proto` nests and matches the recursion budget protoc's
19
+ // own parser carries.
20
+ //
21
+ // Port of `preflight` and `brace_depth` in the Rust crate (`rs/src/lib.rs`,
22
+ // `rs/src/build_descriptor.rs`); `go/preflight.go` is the Go one.
23
+ // How deep a document may nest, in braces. `parse` refuses a deeper one.
24
+ exports.MAX_NESTING_DEPTH = 100;
25
+ // Refuse a `.proto` source that nests deeper than MAX_NESTING_DEPTH,
26
+ // before anything builds a tree that deep. `parse` runs it; a caller who
27
+ // drives the engine directly (`new Tabnas().use(Proto)` and `tn.parse`)
28
+ // runs it on the source first. Throws an Error whose message names the
29
+ // depth; like every refusal the plugin makes itself, it carries no code.
30
+ function preflight(src) {
31
+ const depth = braceDepth(src);
32
+ if (depth > exports.MAX_NESTING_DEPTH) {
33
+ throw new Error(`proto: document nests ${depth} levels deep, past the ` +
34
+ `${exports.MAX_NESTING_DEPTH} this parser accepts`);
35
+ }
36
+ }
37
+ // The grammar's keywords: its match tokens, each a word the lexer takes
38
+ // whole when no word character follows it. A token starts after one, so a
39
+ // quote straight after a keyword opens a string (`reserved"x";` reserves
40
+ // `x`). ts/test/preflight.test.ts holds this list to proto-grammar.json.
41
+ const KEYWORDS = new Set([
42
+ 'edition', 'enum', 'export', 'extend', 'extensions', 'group', 'import',
43
+ 'local', 'map', 'max', 'message', 'oneof', 'option', 'optional', 'package',
44
+ 'public', 'repeated', 'required', 'reserved', 'returns', 'rpc', 'service',
45
+ 'stream', 'syntax', 'to', 'weak',
46
+ ]);
47
+ // Where a token ends and the next starts: a space, a line break, or one of
48
+ // the grammar's fixed tokens other than the braces, which the scan counts
49
+ // on their own. The test holds these to proto-grammar.json too.
50
+ const SEPARATORS = ' \t\r\n[]:,;=()<>-.+';
51
+ // A word character, as the keywords' `(?![A-Za-z0-9_])` reads one.
52
+ function isWord(ch) {
53
+ return (0x30 <= ch && ch <= 0x39) || (0x41 <= ch && ch <= 0x5a) ||
54
+ (0x61 <= ch && ch <= 0x7a) || 0x5f === ch;
55
+ }
56
+ // The nesting depth of a `.proto` source, counted in braces, skipping the
57
+ // string literals and the comments the tabnas lexer skips, where it skips
58
+ // them:
59
+ //
60
+ // - A line comment, `//` or `#`, runs to the next line break, and the
61
+ // lexer breaks a line at a carriage return as well as a line feed.
62
+ // - A string is double, single or backtick quoted, a backslash escaping
63
+ // the character after it, and a backtick string runs across lines.
64
+ // - A quote opens a string only where the lexer starts a token: at the
65
+ // start, and after a space, a line break, a fixed token, a string, a
66
+ // comment or a keyword. Inside a word, `a"b`, the lexer reads the quote
67
+ // as part of the word, and the braces after it count.
68
+ //
69
+ // Over-counting is safe and under-counting is not, so an unterminated
70
+ // string or comment counts every brace inside it: the engine rejects that
71
+ // source anyway. Only ASCII characters decide anything here, so scanning
72
+ // UTF-16 code units counts exactly what the Rust and Go ports count
73
+ // scanning UTF-8 bytes.
74
+ function braceDepth(src) {
75
+ const length = src.length;
76
+ let at = 0;
77
+ let depth = 0;
78
+ let deepest = 0;
79
+ // Whether a token starts at `at`.
80
+ let start = true;
81
+ while (at < length) {
82
+ const ch = src.charCodeAt(at);
83
+ if (0x7b === ch) { // {
84
+ depth++;
85
+ if (depth > deepest)
86
+ deepest = depth;
87
+ at++;
88
+ start = true;
89
+ }
90
+ else if (0x7d === ch) { // }
91
+ if (0 < depth)
92
+ depth--;
93
+ at++;
94
+ start = true;
95
+ }
96
+ else if (SEPARATORS.includes(src[at])) {
97
+ at++;
98
+ start = true;
99
+ }
100
+ else if (0x23 === ch || (0x2f === ch && 0x2f === src.charCodeAt(at + 1))) {
101
+ // `#` or `//`. The shared tabnas lexer reads `#` as a line comment;
102
+ // `.proto` does not, which the leniency corpus records.
103
+ while (at < length && 0x0a !== src.charCodeAt(at) &&
104
+ 0x0d !== src.charCodeAt(at)) {
105
+ at++;
106
+ }
107
+ start = true;
108
+ }
109
+ else if (0x2f === ch && 0x2a === src.charCodeAt(at + 1)) { // /*
110
+ at += 2;
111
+ while (at < length &&
112
+ !(0x2a === src.charCodeAt(at) && 0x2f === src.charCodeAt(at + 1))) {
113
+ at++;
114
+ }
115
+ at += 2;
116
+ start = true;
117
+ }
118
+ else if (start && (0x22 === ch || 0x27 === ch || 0x60 === ch)) {
119
+ // " or ' or `
120
+ at++;
121
+ while (at < length && src.charCodeAt(at) !== ch) {
122
+ at += 0x5c === src.charCodeAt(at) ? 2 : 1; // a backslash escapes one
123
+ }
124
+ at++;
125
+ start = true;
126
+ }
127
+ else if (start && isWord(ch)) {
128
+ const from = at;
129
+ while (at < length && isWord(src.charCodeAt(at)))
130
+ at++;
131
+ start = KEYWORDS.has(src.slice(from, at));
132
+ }
133
+ else {
134
+ at++;
135
+ start = false;
136
+ }
137
+ }
138
+ return deepest;
139
+ }
140
+ //# sourceMappingURL=preflight.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preflight.js","sourceRoot":"","sources":["../src/preflight.ts"],"names":[],"mappings":"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"}
package/dist/proto.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Tabnas } from '@tabnas/parser';
2
+ import { MAX_NESTING_DEPTH, preflight } from './preflight';
2
3
  import { ProtoVersion } from './detect-version';
3
4
  import { FileDescriptorProto } from './descriptor';
4
5
  export interface ProtoOptions {
@@ -11,8 +12,8 @@ declare const Proto: {
11
12
  };
12
13
  declare function toDescriptor(cst: any, options?: Partial<ProtoOptions>): FileDescriptorProto;
13
14
  declare function parse(src: string, options?: Partial<ProtoOptions>): FileDescriptorProto;
14
- declare const VERSION = "0.6.7";
15
- export { Proto, parse, toDescriptor, VERSION };
15
+ declare const VERSION = "0.6.8";
16
+ export { Proto, parse, toDescriptor, preflight, MAX_NESTING_DEPTH, VERSION };
16
17
  export type { ProtoVersion };
17
18
  export * from './descriptor';
18
19
  export { translate } from './translate';
package/dist/proto.js CHANGED
@@ -18,7 +18,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
18
18
  return (mod && mod.__esModule) ? mod : { "default": mod };
19
19
  };
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
- exports.translate = exports.VERSION = exports.Proto = void 0;
21
+ exports.translate = exports.VERSION = exports.MAX_NESTING_DEPTH = exports.preflight = exports.Proto = void 0;
22
22
  exports.parse = parse;
23
23
  exports.toDescriptor = toDescriptor;
24
24
  // @tabnas/proto — a Tabnas plugin that parses Protocol Buffers `.proto`
@@ -31,6 +31,9 @@ const parser_1 = require("@tabnas/parser");
31
31
  const proto_grammar_json_1 = __importDefault(require("./proto-grammar.json"));
32
32
  const build_descriptor_1 = require("./build-descriptor");
33
33
  const aggregate_1 = require("./aggregate");
34
+ const preflight_1 = require("./preflight");
35
+ Object.defineProperty(exports, "MAX_NESTING_DEPTH", { enumerable: true, get: function () { return preflight_1.MAX_NESTING_DEPTH; } });
36
+ Object.defineProperty(exports, "preflight", { enumerable: true, get: function () { return preflight_1.preflight; } });
34
37
  const detect_version_1 = require("./detect-version");
35
38
  // The engine's GrammarSpec type describes a match token as a RegExp; the
36
39
  // serialized form carries it as an `@~/…/` string, which `tn.grammar()`
@@ -72,8 +75,12 @@ function toDescriptor(cst, options) {
72
75
  // Convenience: parse a `.proto` source string to a FileDescriptorProto in
73
76
  // one call. Builds a fresh engine each time; for repeated parsing reuse an
74
77
  // engine via `const j = new Tabnas().use(Proto)` and call
75
- // `toDescriptor(j.parse(src), opts)`.
78
+ // `toDescriptor(j.parse(src), opts)`, running `preflight(src)` first.
79
+ //
80
+ // A document nesting deeper than MAX_NESTING_DEPTH is refused before the
81
+ // engine runs: see ./preflight.ts.
76
82
  function parse(src, options) {
83
+ (0, preflight_1.preflight)(src);
77
84
  const tn = new parser_1.Tabnas({ rewind: { history: 8192 } });
78
85
  Proto(tn, options);
79
86
  return toDescriptor(tn.parse(src), options);
@@ -81,7 +88,7 @@ function parse(src, options) {
81
88
  // VERSION is this package's version. It MUST equal package.json "version":
82
89
  // the release orchestrator rewrites both, and test/version.test.ts fails the
83
90
  // build if they drift. Mirrors `const VERSION` in go/proto.go.
84
- const VERSION = '0.6.7';
91
+ const VERSION = '0.6.8';
85
92
  exports.VERSION = VERSION;
86
93
  __exportStar(require("./descriptor"), exports);
87
94
  var translate_1 = require("./translate");
package/dist/proto.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"proto.js","sourceRoot":"","sources":["../src/proto.ts"],"names":[],"mappings":";AAAA,2EAA2E;;;;;;;;;;;;;;;;;;;;QAwF3D,KAAK;QAAE,YAAY;AAtFnC,wEAAwE;AACxE,0EAA0E;AAC1E,wEAAwE;AACxE,wDAAwD;AAExD,2CAAuC;AAGvC,wEAAwE;AACxE,+DAA+D;AAC/D,8EAAkD;AAClD,yDAA8C;AAC9C,2CAA6C;AAC7C,qDAEyB;AAYzB,yEAAyE;AACzE,wEAAwE;AACxE,uCAAuC;AACvC,MAAM,QAAQ,GAAG,4BAAyC,CAAA;AAE1D,wEAAwE;AACxE,yEAAyE;AACzE,4EAA4E;AAC5E,MAAM,KAAK,GAAG,CAAC,CAAC,EAAU,EAAE,QAAgC,EAAE,EAAE;IAC9D,wEAAwE;IACxE,mEAAmE;IACnE,uEAAuE;IACvE,iEAAiE;IACjE,qEAAqE;IACrE,wEAAwE;IACxE,gEAAgE;IAChE,mEAAmE;IACnE,gEAAgE;IAChE,uEAAuE;IACvE,4BAA4B;IAC5B,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;IACpB,wEAAwE;IACxE,qEAAqE;IACrE,sEAAsE;IACtE,0DAA0D;IAC1D,EAAE,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,EAAO,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,2BAAe,CAAC,CAAC,CAAA;AAC1D,CAAC,CAGA,CAAA;QA8BQ,KAAK;AA5Bd,KAAK,CAAC,QAAQ,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAA;AAEnD,oEAAoE;AACpE,gEAAgE;AAChE,SAAS,YAAY,CAAC,GAAQ,EAAE,OAA+B;IAC7D,MAAM,IAAI,GAAiB,EAAE,GAAG,KAAK,CAAC,QAAQ,EAAE,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC,EAAE,CAAA;IACpE,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAA;IAC7E,MAAM,QAAQ,GACZ,KAAK,IAAI,iBAAiB,KAAK,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,IAAA,gCAAe,EAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IAC3E,MAAM,OAAO,GAAG,IAAA,+BAAc,EAAC,QAAQ,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,CAAA;IACtE,OAAO,IAAA,4BAAS,EAAC,GAAG,EAAE,OAAO,CAAC,CAAA;AAChC,CAAC;AAED,0EAA0E;AAC1E,2EAA2E;AAC3E,0DAA0D;AAC1D,sCAAsC;AACtC,SAAS,KAAK,CAAC,GAAW,EAAE,OAA+B;IACzD,MAAM,EAAE,GAAG,IAAI,eAAM,CAAC,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;IACpD,KAAK,CAAC,EAAE,EAAE,OAAO,CAAC,CAAA;IAClB,OAAO,YAAY,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,CAAA;AAC7C,CAAC;AAED,2EAA2E;AAC3E,6EAA6E;AAC7E,+DAA+D;AAC/D,MAAM,OAAO,GAAG,OAAO,CAAA;QAEc,OAAO;AAE5C,+CAA4B;AAC5B,yCAAuC;AAA9B,sGAAA,SAAS,OAAA"}
1
+ {"version":3,"file":"proto.js","sourceRoot":"","sources":["../src/proto.ts"],"names":[],"mappings":";AAAA,2EAA2E;;;;;;;;;;;;;;;;;;;;QA6F3D,KAAK;QAAE,YAAY;AA3FnC,wEAAwE;AACxE,0EAA0E;AAC1E,wEAAwE;AACxE,wDAAwD;AAExD,2CAAuC;AAGvC,wEAAwE;AACxE,+DAA+D;AAC/D,8EAAkD;AAClD,yDAA8C;AAC9C,2CAA6C;AAC7C,2CAA0D;kGAAjD,6BAAiB;0FAAE,qBAAS;AACrC,qDAEyB;AAYzB,yEAAyE;AACzE,wEAAwE;AACxE,uCAAuC;AACvC,MAAM,QAAQ,GAAG,4BAAyC,CAAA;AAE1D,wEAAwE;AACxE,yEAAyE;AACzE,4EAA4E;AAC5E,MAAM,KAAK,GAAG,CAAC,CAAC,EAAU,EAAE,QAAgC,EAAE,EAAE;IAC9D,wEAAwE;IACxE,mEAAmE;IACnE,uEAAuE;IACvE,iEAAiE;IACjE,qEAAqE;IACrE,wEAAwE;IACxE,gEAAgE;IAChE,mEAAmE;IACnE,gEAAgE;IAChE,uEAAuE;IACvE,4BAA4B;IAC5B,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;IACpB,wEAAwE;IACxE,qEAAqE;IACrE,sEAAsE;IACtE,0DAA0D;IAC1D,EAAE,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,EAAO,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,2BAAe,CAAC,CAAC,CAAA;AAC1D,CAAC,CAGA,CAAA;QAkCQ,KAAK;AAhCd,KAAK,CAAC,QAAQ,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAA;AAEnD,oEAAoE;AACpE,gEAAgE;AAChE,SAAS,YAAY,CAAC,GAAQ,EAAE,OAA+B;IAC7D,MAAM,IAAI,GAAiB,EAAE,GAAG,KAAK,CAAC,QAAQ,EAAE,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC,EAAE,CAAA;IACpE,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAA;IAC7E,MAAM,QAAQ,GACZ,KAAK,IAAI,iBAAiB,KAAK,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,IAAA,gCAAe,EAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IAC3E,MAAM,OAAO,GAAG,IAAA,+BAAc,EAAC,QAAQ,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,CAAA;IACtE,OAAO,IAAA,4BAAS,EAAC,GAAG,EAAE,OAAO,CAAC,CAAA;AAChC,CAAC;AAED,0EAA0E;AAC1E,2EAA2E;AAC3E,0DAA0D;AAC1D,sEAAsE;AACtE,EAAE;AACF,yEAAyE;AACzE,mCAAmC;AACnC,SAAS,KAAK,CAAC,GAAW,EAAE,OAA+B;IACzD,IAAA,qBAAS,EAAC,GAAG,CAAC,CAAA;IACd,MAAM,EAAE,GAAG,IAAI,eAAM,CAAC,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;IACpD,KAAK,CAAC,EAAE,EAAE,OAAO,CAAC,CAAA;IAClB,OAAO,YAAY,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,CAAA;AAC7C,CAAC;AAED,2EAA2E;AAC3E,6EAA6E;AAC7E,+DAA+D;AAC/D,MAAM,OAAO,GAAG,OAAO,CAAA;QAE4C,OAAO;AAE1E,+CAA4B;AAC5B,yCAAuC;AAA9B,sGAAA,SAAS,OAAA"}
@@ -1 +1 @@
1
- {"version":"7.0.2","root":["../src/aggregate.ts","../src/build-descriptor.ts","../src/descriptor.ts","../src/detect-version.ts","../src/grammar.ts","../src/proto.ts","../src/translate.ts"],"packageJsons":["../node_modules/@tabnas/parser/package.json","../package.json"],"missingPackageJsons":["../node_modules/@tabnas/parser/dist/package.json"]}
1
+ {"version":"7.0.2","root":["../src/aggregate.ts","../src/build-descriptor.ts","../src/descriptor.ts","../src/detect-version.ts","../src/grammar.ts","../src/preflight.ts","../src/proto.ts","../src/translate.ts"],"packageJsons":["../node_modules/@tabnas/parser/package.json","../package.json"],"missingPackageJsons":["../node_modules/@tabnas/parser/dist/package.json"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tabnas/proto",
3
- "version": "0.6.7",
3
+ "version": "0.6.8",
4
4
  "description": "Parse Protocol Buffers .proto IDL (proto2, proto3, edition 2023/2024) into FileDescriptorProto-shaped JSON, using the Tabnas parser and an ABNF grammar.",
5
5
  "main": "dist/proto.js",
6
6
  "type": "commonjs",
@@ -0,0 +1,134 @@
1
+ /* Copyright (c) 2026 Richard Rodger and other contributors, MIT License */
2
+
3
+ // The nesting preflight: a `.proto` document nesting deeper than
4
+ // MAX_NESTING_DEPTH is refused before the engine builds a tree that deep.
5
+ //
6
+ // The tree costs more than its nesting suggests. A CST node's `src` holds
7
+ // every token beneath it, so a document nested `d` levels deep builds
8
+ // source text that grows with d squared, and so do the parse's time and
9
+ // memory: the walk here overflows its stack from about 3,000 levels, after
10
+ // seconds and hundreds of megabytes; Go's parse grows past any memory a
11
+ // machine has; and a Rust stack that runs out aborts the process.
12
+ // DIVERGENCE.md section 2 has the measurements. So every runtime refuses
13
+ // the same documents, with the same message, at the same cap: the Rust
14
+ // port's, which sits more than an order of magnitude past anything a
15
+ // hand-written `.proto` nests and matches the recursion budget protoc's
16
+ // own parser carries.
17
+ //
18
+ // Port of `preflight` and `brace_depth` in the Rust crate (`rs/src/lib.rs`,
19
+ // `rs/src/build_descriptor.rs`); `go/preflight.go` is the Go one.
20
+
21
+ // How deep a document may nest, in braces. `parse` refuses a deeper one.
22
+ export const MAX_NESTING_DEPTH = 100
23
+
24
+ // Refuse a `.proto` source that nests deeper than MAX_NESTING_DEPTH,
25
+ // before anything builds a tree that deep. `parse` runs it; a caller who
26
+ // drives the engine directly (`new Tabnas().use(Proto)` and `tn.parse`)
27
+ // runs it on the source first. Throws an Error whose message names the
28
+ // depth; like every refusal the plugin makes itself, it carries no code.
29
+ export function preflight(src: string): void {
30
+ const depth = braceDepth(src)
31
+ if (depth > MAX_NESTING_DEPTH) {
32
+ throw new Error(
33
+ `proto: document nests ${depth} levels deep, past the ` +
34
+ `${MAX_NESTING_DEPTH} this parser accepts`,
35
+ )
36
+ }
37
+ }
38
+
39
+ // The grammar's keywords: its match tokens, each a word the lexer takes
40
+ // whole when no word character follows it. A token starts after one, so a
41
+ // quote straight after a keyword opens a string (`reserved"x";` reserves
42
+ // `x`). ts/test/preflight.test.ts holds this list to proto-grammar.json.
43
+ const KEYWORDS = new Set([
44
+ 'edition', 'enum', 'export', 'extend', 'extensions', 'group', 'import',
45
+ 'local', 'map', 'max', 'message', 'oneof', 'option', 'optional', 'package',
46
+ 'public', 'repeated', 'required', 'reserved', 'returns', 'rpc', 'service',
47
+ 'stream', 'syntax', 'to', 'weak',
48
+ ])
49
+
50
+ // Where a token ends and the next starts: a space, a line break, or one of
51
+ // the grammar's fixed tokens other than the braces, which the scan counts
52
+ // on their own. The test holds these to proto-grammar.json too.
53
+ const SEPARATORS = ' \t\r\n[]:,;=()<>-.+'
54
+
55
+ // A word character, as the keywords' `(?![A-Za-z0-9_])` reads one.
56
+ function isWord(ch: number): boolean {
57
+ return (0x30 <= ch && ch <= 0x39) || (0x41 <= ch && ch <= 0x5a) ||
58
+ (0x61 <= ch && ch <= 0x7a) || 0x5f === ch
59
+ }
60
+
61
+ // The nesting depth of a `.proto` source, counted in braces, skipping the
62
+ // string literals and the comments the tabnas lexer skips, where it skips
63
+ // them:
64
+ //
65
+ // - A line comment, `//` or `#`, runs to the next line break, and the
66
+ // lexer breaks a line at a carriage return as well as a line feed.
67
+ // - A string is double, single or backtick quoted, a backslash escaping
68
+ // the character after it, and a backtick string runs across lines.
69
+ // - A quote opens a string only where the lexer starts a token: at the
70
+ // start, and after a space, a line break, a fixed token, a string, a
71
+ // comment or a keyword. Inside a word, `a"b`, the lexer reads the quote
72
+ // as part of the word, and the braces after it count.
73
+ //
74
+ // Over-counting is safe and under-counting is not, so an unterminated
75
+ // string or comment counts every brace inside it: the engine rejects that
76
+ // source anyway. Only ASCII characters decide anything here, so scanning
77
+ // UTF-16 code units counts exactly what the Rust and Go ports count
78
+ // scanning UTF-8 bytes.
79
+ function braceDepth(src: string): number {
80
+ const length = src.length
81
+ let at = 0
82
+ let depth = 0
83
+ let deepest = 0
84
+ // Whether a token starts at `at`.
85
+ let start = true
86
+ while (at < length) {
87
+ const ch = src.charCodeAt(at)
88
+ if (0x7b === ch) { // {
89
+ depth++
90
+ if (depth > deepest) deepest = depth
91
+ at++
92
+ start = true
93
+ } else if (0x7d === ch) { // }
94
+ if (0 < depth) depth--
95
+ at++
96
+ start = true
97
+ } else if (SEPARATORS.includes(src[at])) {
98
+ at++
99
+ start = true
100
+ } else if (0x23 === ch || (0x2f === ch && 0x2f === src.charCodeAt(at + 1))) {
101
+ // `#` or `//`. The shared tabnas lexer reads `#` as a line comment;
102
+ // `.proto` does not, which the leniency corpus records.
103
+ while (at < length && 0x0a !== src.charCodeAt(at) &&
104
+ 0x0d !== src.charCodeAt(at)) {
105
+ at++
106
+ }
107
+ start = true
108
+ } else if (0x2f === ch && 0x2a === src.charCodeAt(at + 1)) { // /*
109
+ at += 2
110
+ while (at < length &&
111
+ !(0x2a === src.charCodeAt(at) && 0x2f === src.charCodeAt(at + 1))) {
112
+ at++
113
+ }
114
+ at += 2
115
+ start = true
116
+ } else if (start && (0x22 === ch || 0x27 === ch || 0x60 === ch)) {
117
+ // " or ' or `
118
+ at++
119
+ while (at < length && src.charCodeAt(at) !== ch) {
120
+ at += 0x5c === src.charCodeAt(at) ? 2 : 1 // a backslash escapes one
121
+ }
122
+ at++
123
+ start = true
124
+ } else if (start && isWord(ch)) {
125
+ const from = at
126
+ while (at < length && isWord(src.charCodeAt(at))) at++
127
+ start = KEYWORDS.has(src.slice(from, at))
128
+ } else {
129
+ at++
130
+ start = false
131
+ }
132
+ }
133
+ return deepest
134
+ }
package/src/proto.ts CHANGED
@@ -13,6 +13,7 @@ import type { GrammarSpec } from '@tabnas/parser'
13
13
  import compiledGrammar from './proto-grammar.json'
14
14
  import { buildFile } from './build-descriptor'
15
15
  import { recordAggregate } from './aggregate'
16
+ import { MAX_NESTING_DEPTH, preflight } from './preflight'
16
17
  import {
17
18
  ProtoVersion, declaredVersion, resolveVersion,
18
19
  } from './detect-version'
@@ -74,8 +75,12 @@ function toDescriptor(cst: any, options?: Partial<ProtoOptions>): FileDescriptor
74
75
  // Convenience: parse a `.proto` source string to a FileDescriptorProto in
75
76
  // one call. Builds a fresh engine each time; for repeated parsing reuse an
76
77
  // engine via `const j = new Tabnas().use(Proto)` and call
77
- // `toDescriptor(j.parse(src), opts)`.
78
+ // `toDescriptor(j.parse(src), opts)`, running `preflight(src)` first.
79
+ //
80
+ // A document nesting deeper than MAX_NESTING_DEPTH is refused before the
81
+ // engine runs: see ./preflight.ts.
78
82
  function parse(src: string, options?: Partial<ProtoOptions>): FileDescriptorProto {
83
+ preflight(src)
79
84
  const tn = new Tabnas({ rewind: { history: 8192 } })
80
85
  Proto(tn, options)
81
86
  return toDescriptor(tn.parse(src), options)
@@ -84,9 +89,9 @@ function parse(src: string, options?: Partial<ProtoOptions>): FileDescriptorProt
84
89
  // VERSION is this package's version. It MUST equal package.json "version":
85
90
  // the release orchestrator rewrites both, and test/version.test.ts fails the
86
91
  // build if they drift. Mirrors `const VERSION` in go/proto.go.
87
- const VERSION = '0.6.7'
92
+ const VERSION = '0.6.8'
88
93
 
89
- export { Proto, parse, toDescriptor, VERSION }
94
+ export { Proto, parse, toDescriptor, preflight, MAX_NESTING_DEPTH, VERSION }
90
95
  export type { ProtoVersion }
91
96
  export * from './descriptor'
92
97
  export { translate } from './translate'