@tabnas/proto 0.6.7 → 0.6.9

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.9";
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.9';
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"}
package/dist/translate.js CHANGED
@@ -43,8 +43,8 @@ const TRANSLATION = Object.freeze({
43
43
  "A label the descriptor does not keep is not written as the file spelled it: an optional field of a proto2 file is written optional whether or not the file said so, and an optional field of a proto3 file that the descriptor does not mark proto3Optional, or of an edition file, is written with no label.",
44
44
  "A map field is written as map<K, V> from the entry message the reader made for it, a group as a group block from its nested message, and a proto3 optional field's synthetic oneof is not written, since the reader makes each of them again.",
45
45
  "A number is written as the descriptor holds it: a hexadecimal or octal field number, enum value or range bound is written in decimal, an exclusive range end as its last number or max, and a number the reader read as NaN (a digit separator, 1_0, which the lexer takes and JavaScript's Number refuses) as 0_0, which reads back as NaN.",
46
- "A string literal is written between double quotes, or single quotes where its text holds a double quote and no single quote, its escapes as written, since the descriptor keeps a literal's text between its quotes; a text that holds both quotes is written between double quotes, and reads back as written only where each double quote in it is escaped.",
47
- "An option's value is one string to the descriptor whether the file wrote it as an identifier, a string literal or an aggregate in braces, so it is written as a string literal, as an aggregate only where no literal can hold its text (a line break, or both quotes), and as an identifier for the options descriptor.proto types as enums (optimize_for, ctype, jstype, retention, targets, idempotency_level and the features) when its text is made of identifier characters; protoc reads an enum-valued or message-valued option written as a literal as a string, and a quoted value of such an option that begins with a digit, which protoc refuses, does not read back.",
46
+ "A string literal is written between double quotes, or single quotes where its text holds a double quote and no single quote, its escapes as written, since the descriptor keeps a literal's text between its quotes; a text that holds both quotes is written between the quotes whose every occurrence in it is escaped; a backtick string holding a line break, which the descriptor keeps with its backticks and no quotes hold, is written as itself, but for a reserved name, which the reader takes only from a quoted literal or an identifier; and a text that nothing holds where it goes is refused: a control character that no literal or backtick string holds (a tab, a line break outside a backtick string, the other C0 controls), both quotes unescaped, or a backslash at its end that would escape the closing quote.",
47
+ "An option's value is one string to the descriptor whether the file wrote it as an identifier, a string literal or an aggregate in braces, so it is written as a string literal, as the backtick string it is where it is one, as an aggregate only where neither can hold its text (a line break, or both quotes unescaped), and as an identifier for the options descriptor.proto types as enums (optimize_for, ctype, jstype, retention, targets, idempotency_level and the features) when its text is made of identifier characters; protoc reads an enum-valued or message-valued option written as a literal as a string, and a quoted value of such an option that begins with a digit, which protoc refuses, does not read back; a value no literal or backtick string holds that is no aggregate's body, which no file's descriptor holds, is written in braces too and does not read back; and a value holding a control character that no literal, backtick string or aggregate holds is refused.",
48
48
  "A default is written bare for a field that is not a string or bytes when its text is made of a constant's characters, and as a string literal otherwise; a quoted default of such a field whose text is made of those characters but spells no one constant, which protoc refuses, does not read back.",
49
49
  "Reserved names are written as string literals in a proto2 or proto3 file and as identifiers in an edition file, a message's or an enum's reserved ranges in one reserved statement and its names in another, each extension range in an extensions statement of its own with its options, and each extension in an extend block of its own."
50
50
  ]
@@ -96,22 +96,30 @@ const TRANSLATION = Object.freeze({
96
96
  ; enum's inclusive end as it is, \`max\` for 2147483647.
97
97
  ; - A string from a string literal is kept as its text between the quotes,
98
98
  ; escapes as written, so it is written between double quotes, or single
99
- ; quotes where it holds a double quote and no single quote. An option's
100
- ; value the reader read as an identifier, a quoted literal or an
101
- ; aggregate in braces is one string to the tree: it is written quoted,
102
- ; as an aggregate (\`{...}\`) where no literal can hold it (a line break, or
103
- ; both quotes), and as an identifier for the options descriptor.proto
99
+ ; quotes where it holds a double quote and no single quote, and where it
100
+ ; holds both, between the quotes it escapes (an escaped backslash, \`\\\\\`,
101
+ ; escapes no quote after it). A backtick string, which the lexer reads
102
+ ; across lines too, is kept as written, backticks and all, so one
103
+ ; holding a line break, which no quotes hold, is written as itself. A
104
+ ; reserved name is not: the reader takes one from a quoted literal or an
105
+ ; identifier, and the words of a backtick string as names of their own.
106
+ ; An option's value the reader read as an identifier, a quoted literal,
107
+ ; a backtick string or an aggregate in braces is one string to the tree:
108
+ ; it is written quoted, as the backtick string it is, as an aggregate
109
+ ; (\`{...}\`) where neither can hold it (a line break, or both quotes
110
+ ; unescaped), and as an identifier for the options descriptor.proto
104
111
  ; types as enums (\`optimize_for\`, \`ctype\`, \`jstype\`, \`retention\`,
105
112
  ; \`targets\`, \`idempotency_level\` and the features) where it is made of
106
- ; identifier characters. A default is written bare for a field that is
107
- ; not a string or bytes, where it is made of the characters a constant
108
- ; is, and as a literal otherwise. alchemy cannot read a string a
109
- ; character at a time, so three texts a file can hold, each refused by
110
- ; protoc but one, do not read back: a literal's text holding both
111
- ; quotes with a double quote unescaped, a quoted value of an enum-typed
112
- ; option that begins with a digit, and a quoted default of a field that
113
- ; is not a string whose text is a constant's characters spelling no one
114
- ; constant.
113
+ ; identifier characters; a value a program made that no literal or
114
+ ; backtick string holds and that is no aggregate's body is written in
115
+ ; braces too, and the reader refuses it, since alchemy cannot parse an
116
+ ; aggregate. A default is written bare for a field that is not a string
117
+ ; or bytes, where it is made of the characters a constant is, and as a
118
+ ; literal otherwise. alchemy cannot read a string a character at a time,
119
+ ; so two texts a file can hold, each refused by protoc, do not read back:
120
+ ; a quoted value of an enum-typed option that begins with a digit, and a
121
+ ; quoted default of a field that is not a string whose text is a
122
+ ; constant's characters spelling no one constant.
115
123
  ; - A field or enum number the reader could not read (JavaScript's Number
116
124
  ; of \`1_0\`, the lexer's digit separator) is NaN, null in JSON, and is
117
125
  ; written \`0_0\`, which reads back the same; an option's number that is
@@ -121,7 +129,12 @@ const TRANSLATION = Object.freeze({
121
129
  ; met: a root that is not an object, a syntax or an edition a \`.proto\`
122
130
  ; file cannot declare, a member a descriptor holds as a string that is not
123
131
  ; one, a field type that is no scalar's, an option's value that is not a
124
- ; string, a number or a boolean.
132
+ ; string, a number or a boolean, and a string that nothing a \`.proto\` file
133
+ ; writes holds where the string goes: a control character (no literal
134
+ ; holds one, a backtick string only a line break, and an option's
135
+ ; aggregate only the tab and line breaks between its tokens), and outside
136
+ ; an option's value, both quotes unescaped or a backslash at the end,
137
+ ; which would escape the closing quote.
125
138
  ;
126
139
  ; The tree is materialized whole before a line is written, since the
127
140
  ; syntax comes first in the file and last in the tree, and a field's place
@@ -255,23 +268,123 @@ def proto-ident-chars [[48 57] [65 90] [95 95] [97 122]]
255
268
  ; The characters of a bare constant: a number, a signed one, an identifier.
256
269
  def proto-bare-chars [[43 43] [45 46] [48 57] [65 90] [95 95] [97 122]]
257
270
 
258
- ; A string literal whose text between its quotes is s, as written.
259
- def proto-literal [s]
260
- match (chars-within proto-no-dq s)
261
- case true (string-join "" ["\\"" s "\\""])
262
- case false
263
- match (chars-within proto-no-sq s)
264
- case true (string-join "" ["'" s "'"])
265
- case false (string-join "" ["\\"" s "\\""])
271
+ ; The characters a backtick string holds, as the lexer reads one: the
272
+ ; line feed and the carriage return that carry it across lines, and none
273
+ ; of the other controls.
274
+ def proto-in-backtick [[10 10] [13 13] [32 1114111]]
275
+
276
+ ; The characters an aggregate holds: its tokens, and the tab, the line
277
+ ; feed and the carriage return as the whitespace between them.
278
+ def proto-in-aggregate [[9 10] [13 13] [32 1114111]]
279
+
280
+ ; Whether s is a backtick string's text as the reader keeps it, the
281
+ ; backticks and all: the lexer reads a backtick string across lines too,
282
+ ; a backslash escaping the character after it, and the reader keeps a
283
+ ; constant that is not quoted as it was written. So s has a backtick at
284
+ ; each end and none unescaped between them, no backslash before the last
285
+ ; that would escape it, and no control but a line break.
286
+ def proto-backtick [s]
287
+ let [parts (split "\`" s)]
288
+ match (proto-less 2 (count parts))
289
+ case false false
290
+ case true
291
+ match (match (proto-at parts 0) (case "" (match (top parts) (case "" true) (case _ false))) (case _ false))
292
+ case false false
293
+ case true
294
+ match (chars-within proto-in-backtick s)
295
+ case false false
296
+ case true (proto-escaped-inside "\`" (proto-after "\`" (string-join "\`" (pop parts))))
297
+
298
+ ; Whether text could stand between quotes q as the lexer reads them: every
299
+ ; q in it escaped, and no backslash at its end to escape the closing one.
300
+ def proto-escaped-inside [q text]
301
+ match (proto-quotes-escaped q text)
302
+ case false false
303
+ case true
304
+ match (proto-open-escape text)
305
+ case true false
306
+ case false true
307
+
308
+ ; Whether every quote q in s is escaped, as in the text of a literal
309
+ ; between those quotes. The lexer reads an escape as a backslash and the
310
+ ; character after it, so an escaped backslash (\`\\\\\`) escapes nothing
311
+ ; after it: s is read in the runs between its escaped backslashes, found
312
+ ; from the front as the lexer finds them, and in a run a quote is escaped
313
+ ; where a backslash comes right before it.
314
+ def proto-quotes-escaped [q s]
315
+ match (count (filter (fn [run] (proto-bare-quote q run)) (split "\\\\\\\\" s)))
316
+ case 0 true
317
+ case _ false
318
+
319
+ ; Whether a run of a literal's text that holds no escaped backslash holds
320
+ ; the quote q with no backslash right before it.
321
+ def proto-bare-quote [q run]
322
+ match (count (filter (fn [piece] (proto-less 1 (count (split q piece)))) (split (string-join "" ["\\\\" q]) run)))
323
+ case 0 false
324
+ case _ true
325
+
326
+ ; Whether s ends with a backslash that escapes nothing in it, which would
327
+ ; escape the quote written after it: its last run between escaped
328
+ ; backslashes ends with one.
329
+ def proto-open-escape [s]
330
+ let [pieces (split "\\\\" (top (split "\\\\\\\\" s)))]
331
+ match (count pieces)
332
+ case 1 false
333
+ case _
334
+ match (top pieces)
335
+ case "" true
336
+ case _ false
266
337
 
267
- ; An option's string: a literal, or an aggregate where no literal holds it.
338
+ ; A literal's text between quotes that hold it, or "" where none do:
339
+ ; double quotes where it holds no double quote, single quotes where it
340
+ ; holds no single quote, and where it holds both, the quotes it escapes.
341
+ ; No quotes hold a control character, both quotes unescaped, or a
342
+ ; backslash at the end that would escape the closing quote.
343
+ def proto-quoted [s]
344
+ match (chars-within [[32 1114111]] s)
345
+ case false ""
346
+ case true
347
+ match (proto-open-escape s)
348
+ case true ""
349
+ case false
350
+ match (chars-within proto-no-dq s)
351
+ case true (string-join "" ["\\"" s "\\""])
352
+ case false
353
+ match (chars-within proto-no-sq s)
354
+ case true (string-join "" ["'" s "'"])
355
+ case false
356
+ match (proto-quotes-escaped "\\"" s)
357
+ case true (string-join "" ["\\"" s "\\""])
358
+ case false
359
+ match (proto-quotes-escaped "'" s)
360
+ case true (string-join "" ["'" s "'"])
361
+ case false ""
362
+
363
+ ; A string literal whose text is s, as written: between quotes, or as the
364
+ ; backtick string it is, which holds a line break no quotes hold. A text
365
+ ; neither holds fails.
366
+ def proto-literal [s]
367
+ match (proto-quoted s)
368
+ case ""
369
+ match (proto-backtick s)
370
+ case true s
371
+ case false (fail :unrepresentable "a string holds what no literal or backtick string holds: a control character, both quotes unescaped, or a backslash that would escape the closing quote")
372
+ case q q
373
+
374
+ ; An option's string: a literal, the backtick string it is, or the
375
+ ; aggregate it is, whose braces hold what no literal holds (a line break,
376
+ ; or both quotes unescaped). A control that no literal, backtick string
377
+ ; or aggregate holds fails.
268
378
  def proto-option-string [s]
269
- match (chars-within proto-no-dq s)
270
- case true (string-join "" ["\\"" s "\\""])
271
- case false
272
- match (chars-within proto-no-sq s)
273
- case true (string-join "" ["'" s "'"])
274
- case false (string-join "" ["{" s "}"])
379
+ match (proto-quoted s)
380
+ case ""
381
+ match (proto-backtick s)
382
+ case true s
383
+ case false
384
+ match (chars-within proto-in-aggregate s)
385
+ case true (string-join "" ["{" s "}"])
386
+ case false (fail :unrepresentable "an option's value holds a control character that no literal, backtick string or aggregate holds")
387
+ case q q
275
388
 
276
389
  ; The options descriptor.proto types as enums, whose values are identifiers.
277
390
  def proto-enum-option [k]
@@ -523,8 +636,16 @@ def proto-reserved-name [syntax n]
523
636
  case "editions"
524
637
  match (proto-identifier n)
525
638
  case true n
526
- case false (proto-literal n)
527
- case _ (proto-literal n)
639
+ case false (proto-name-literal n)
640
+ case _ (proto-name-literal n)
641
+
642
+ ; A reserved name between quotes. The reader takes a reserved name from a
643
+ ; quoted literal or an identifier, and reads the words of a backtick
644
+ ; string as names of their own, so a name no quotes hold fails.
645
+ def proto-name-literal [n]
646
+ match (proto-quoted n)
647
+ case "" (fail :unrepresentable "a reserved name holds what no quotes hold: a control character, both quotes unescaped, or a backslash that would escape the closing quote")
648
+ case q q
528
649
 
529
650
  def proto-reserved-names [syntax indent names]
530
651
  match (count names)
@@ -1 +1 @@
1
- {"version":3,"file":"translate.js","sourceRoot":"","sources":["../src/translate.ts"],"names":[],"mappings":";AAAA,2EAA2E;;QAkiCzE,SAAS;AAjhCX,MAAM,WAAW,GAAqB,MAAM,CAAC,MAAM,CAAC;IAClD,QAAQ,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+CX;IACC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAw9BxD,EAAE,CAAC;CACH,CAAC,CAAA;AAEF,SAAS,SAAS;IAChB,OAAO,WAAW,CAAA;AACpB,CAAC"}
1
+ {"version":3,"file":"translate.js","sourceRoot":"","sources":["../src/translate.ts"],"names":[],"mappings":";AAAA,2EAA2E;;QA2pCzE,SAAS;AA1oCX,MAAM,WAAW,GAAqB,MAAM,CAAC,MAAM,CAAC;IAClD,QAAQ,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+CX;IACC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAilCxD,EAAE,CAAC;CACH,CAAC,CAAA;AAEF,SAAS,SAAS;IAChB,OAAO,WAAW,CAAA;AACpB,CAAC"}
@@ -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.9",
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.9'
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'
package/src/translate.ts CHANGED
@@ -56,8 +56,8 @@ const TRANSLATION: TranslationParts = Object.freeze({
56
56
  "A label the descriptor does not keep is not written as the file spelled it: an optional field of a proto2 file is written optional whether or not the file said so, and an optional field of a proto3 file that the descriptor does not mark proto3Optional, or of an edition file, is written with no label.",
57
57
  "A map field is written as map<K, V> from the entry message the reader made for it, a group as a group block from its nested message, and a proto3 optional field's synthetic oneof is not written, since the reader makes each of them again.",
58
58
  "A number is written as the descriptor holds it: a hexadecimal or octal field number, enum value or range bound is written in decimal, an exclusive range end as its last number or max, and a number the reader read as NaN (a digit separator, 1_0, which the lexer takes and JavaScript's Number refuses) as 0_0, which reads back as NaN.",
59
- "A string literal is written between double quotes, or single quotes where its text holds a double quote and no single quote, its escapes as written, since the descriptor keeps a literal's text between its quotes; a text that holds both quotes is written between double quotes, and reads back as written only where each double quote in it is escaped.",
60
- "An option's value is one string to the descriptor whether the file wrote it as an identifier, a string literal or an aggregate in braces, so it is written as a string literal, as an aggregate only where no literal can hold its text (a line break, or both quotes), and as an identifier for the options descriptor.proto types as enums (optimize_for, ctype, jstype, retention, targets, idempotency_level and the features) when its text is made of identifier characters; protoc reads an enum-valued or message-valued option written as a literal as a string, and a quoted value of such an option that begins with a digit, which protoc refuses, does not read back.",
59
+ "A string literal is written between double quotes, or single quotes where its text holds a double quote and no single quote, its escapes as written, since the descriptor keeps a literal's text between its quotes; a text that holds both quotes is written between the quotes whose every occurrence in it is escaped; a backtick string holding a line break, which the descriptor keeps with its backticks and no quotes hold, is written as itself, but for a reserved name, which the reader takes only from a quoted literal or an identifier; and a text that nothing holds where it goes is refused: a control character that no literal or backtick string holds (a tab, a line break outside a backtick string, the other C0 controls), both quotes unescaped, or a backslash at its end that would escape the closing quote.",
60
+ "An option's value is one string to the descriptor whether the file wrote it as an identifier, a string literal or an aggregate in braces, so it is written as a string literal, as the backtick string it is where it is one, as an aggregate only where neither can hold its text (a line break, or both quotes unescaped), and as an identifier for the options descriptor.proto types as enums (optimize_for, ctype, jstype, retention, targets, idempotency_level and the features) when its text is made of identifier characters; protoc reads an enum-valued or message-valued option written as a literal as a string, and a quoted value of such an option that begins with a digit, which protoc refuses, does not read back; a value no literal or backtick string holds that is no aggregate's body, which no file's descriptor holds, is written in braces too and does not read back; and a value holding a control character that no literal, backtick string or aggregate holds is refused.",
61
61
  "A default is written bare for a field that is not a string or bytes when its text is made of a constant's characters, and as a string literal otherwise; a quoted default of such a field whose text is made of those characters but spells no one constant, which protoc refuses, does not read back.",
62
62
  "Reserved names are written as string literals in a proto2 or proto3 file and as identifiers in an edition file, a message's or an enum's reserved ranges in one reserved statement and its names in another, each extension range in an extensions statement of its own with its options, and each extension in an extend block of its own."
63
63
  ]
@@ -109,22 +109,30 @@ const TRANSLATION: TranslationParts = Object.freeze({
109
109
  ; enum's inclusive end as it is, \`max\` for 2147483647.
110
110
  ; - A string from a string literal is kept as its text between the quotes,
111
111
  ; escapes as written, so it is written between double quotes, or single
112
- ; quotes where it holds a double quote and no single quote. An option's
113
- ; value the reader read as an identifier, a quoted literal or an
114
- ; aggregate in braces is one string to the tree: it is written quoted,
115
- ; as an aggregate (\`{...}\`) where no literal can hold it (a line break, or
116
- ; both quotes), and as an identifier for the options descriptor.proto
112
+ ; quotes where it holds a double quote and no single quote, and where it
113
+ ; holds both, between the quotes it escapes (an escaped backslash, \`\\\\\`,
114
+ ; escapes no quote after it). A backtick string, which the lexer reads
115
+ ; across lines too, is kept as written, backticks and all, so one
116
+ ; holding a line break, which no quotes hold, is written as itself. A
117
+ ; reserved name is not: the reader takes one from a quoted literal or an
118
+ ; identifier, and the words of a backtick string as names of their own.
119
+ ; An option's value the reader read as an identifier, a quoted literal,
120
+ ; a backtick string or an aggregate in braces is one string to the tree:
121
+ ; it is written quoted, as the backtick string it is, as an aggregate
122
+ ; (\`{...}\`) where neither can hold it (a line break, or both quotes
123
+ ; unescaped), and as an identifier for the options descriptor.proto
117
124
  ; types as enums (\`optimize_for\`, \`ctype\`, \`jstype\`, \`retention\`,
118
125
  ; \`targets\`, \`idempotency_level\` and the features) where it is made of
119
- ; identifier characters. A default is written bare for a field that is
120
- ; not a string or bytes, where it is made of the characters a constant
121
- ; is, and as a literal otherwise. alchemy cannot read a string a
122
- ; character at a time, so three texts a file can hold, each refused by
123
- ; protoc but one, do not read back: a literal's text holding both
124
- ; quotes with a double quote unescaped, a quoted value of an enum-typed
125
- ; option that begins with a digit, and a quoted default of a field that
126
- ; is not a string whose text is a constant's characters spelling no one
127
- ; constant.
126
+ ; identifier characters; a value a program made that no literal or
127
+ ; backtick string holds and that is no aggregate's body is written in
128
+ ; braces too, and the reader refuses it, since alchemy cannot parse an
129
+ ; aggregate. A default is written bare for a field that is not a string
130
+ ; or bytes, where it is made of the characters a constant is, and as a
131
+ ; literal otherwise. alchemy cannot read a string a character at a time,
132
+ ; so two texts a file can hold, each refused by protoc, do not read back:
133
+ ; a quoted value of an enum-typed option that begins with a digit, and a
134
+ ; quoted default of a field that is not a string whose text is a
135
+ ; constant's characters spelling no one constant.
128
136
  ; - A field or enum number the reader could not read (JavaScript's Number
129
137
  ; of \`1_0\`, the lexer's digit separator) is NaN, null in JSON, and is
130
138
  ; written \`0_0\`, which reads back the same; an option's number that is
@@ -134,7 +142,12 @@ const TRANSLATION: TranslationParts = Object.freeze({
134
142
  ; met: a root that is not an object, a syntax or an edition a \`.proto\`
135
143
  ; file cannot declare, a member a descriptor holds as a string that is not
136
144
  ; one, a field type that is no scalar's, an option's value that is not a
137
- ; string, a number or a boolean.
145
+ ; string, a number or a boolean, and a string that nothing a \`.proto\` file
146
+ ; writes holds where the string goes: a control character (no literal
147
+ ; holds one, a backtick string only a line break, and an option's
148
+ ; aggregate only the tab and line breaks between its tokens), and outside
149
+ ; an option's value, both quotes unescaped or a backslash at the end,
150
+ ; which would escape the closing quote.
138
151
  ;
139
152
  ; The tree is materialized whole before a line is written, since the
140
153
  ; syntax comes first in the file and last in the tree, and a field's place
@@ -268,23 +281,123 @@ def proto-ident-chars [[48 57] [65 90] [95 95] [97 122]]
268
281
  ; The characters of a bare constant: a number, a signed one, an identifier.
269
282
  def proto-bare-chars [[43 43] [45 46] [48 57] [65 90] [95 95] [97 122]]
270
283
 
271
- ; A string literal whose text between its quotes is s, as written.
272
- def proto-literal [s]
273
- match (chars-within proto-no-dq s)
274
- case true (string-join "" ["\\"" s "\\""])
275
- case false
276
- match (chars-within proto-no-sq s)
277
- case true (string-join "" ["'" s "'"])
278
- case false (string-join "" ["\\"" s "\\""])
284
+ ; The characters a backtick string holds, as the lexer reads one: the
285
+ ; line feed and the carriage return that carry it across lines, and none
286
+ ; of the other controls.
287
+ def proto-in-backtick [[10 10] [13 13] [32 1114111]]
288
+
289
+ ; The characters an aggregate holds: its tokens, and the tab, the line
290
+ ; feed and the carriage return as the whitespace between them.
291
+ def proto-in-aggregate [[9 10] [13 13] [32 1114111]]
292
+
293
+ ; Whether s is a backtick string's text as the reader keeps it, the
294
+ ; backticks and all: the lexer reads a backtick string across lines too,
295
+ ; a backslash escaping the character after it, and the reader keeps a
296
+ ; constant that is not quoted as it was written. So s has a backtick at
297
+ ; each end and none unescaped between them, no backslash before the last
298
+ ; that would escape it, and no control but a line break.
299
+ def proto-backtick [s]
300
+ let [parts (split "\`" s)]
301
+ match (proto-less 2 (count parts))
302
+ case false false
303
+ case true
304
+ match (match (proto-at parts 0) (case "" (match (top parts) (case "" true) (case _ false))) (case _ false))
305
+ case false false
306
+ case true
307
+ match (chars-within proto-in-backtick s)
308
+ case false false
309
+ case true (proto-escaped-inside "\`" (proto-after "\`" (string-join "\`" (pop parts))))
310
+
311
+ ; Whether text could stand between quotes q as the lexer reads them: every
312
+ ; q in it escaped, and no backslash at its end to escape the closing one.
313
+ def proto-escaped-inside [q text]
314
+ match (proto-quotes-escaped q text)
315
+ case false false
316
+ case true
317
+ match (proto-open-escape text)
318
+ case true false
319
+ case false true
320
+
321
+ ; Whether every quote q in s is escaped, as in the text of a literal
322
+ ; between those quotes. The lexer reads an escape as a backslash and the
323
+ ; character after it, so an escaped backslash (\`\\\\\`) escapes nothing
324
+ ; after it: s is read in the runs between its escaped backslashes, found
325
+ ; from the front as the lexer finds them, and in a run a quote is escaped
326
+ ; where a backslash comes right before it.
327
+ def proto-quotes-escaped [q s]
328
+ match (count (filter (fn [run] (proto-bare-quote q run)) (split "\\\\\\\\" s)))
329
+ case 0 true
330
+ case _ false
331
+
332
+ ; Whether a run of a literal's text that holds no escaped backslash holds
333
+ ; the quote q with no backslash right before it.
334
+ def proto-bare-quote [q run]
335
+ match (count (filter (fn [piece] (proto-less 1 (count (split q piece)))) (split (string-join "" ["\\\\" q]) run)))
336
+ case 0 false
337
+ case _ true
338
+
339
+ ; Whether s ends with a backslash that escapes nothing in it, which would
340
+ ; escape the quote written after it: its last run between escaped
341
+ ; backslashes ends with one.
342
+ def proto-open-escape [s]
343
+ let [pieces (split "\\\\" (top (split "\\\\\\\\" s)))]
344
+ match (count pieces)
345
+ case 1 false
346
+ case _
347
+ match (top pieces)
348
+ case "" true
349
+ case _ false
279
350
 
280
- ; An option's string: a literal, or an aggregate where no literal holds it.
351
+ ; A literal's text between quotes that hold it, or "" where none do:
352
+ ; double quotes where it holds no double quote, single quotes where it
353
+ ; holds no single quote, and where it holds both, the quotes it escapes.
354
+ ; No quotes hold a control character, both quotes unescaped, or a
355
+ ; backslash at the end that would escape the closing quote.
356
+ def proto-quoted [s]
357
+ match (chars-within [[32 1114111]] s)
358
+ case false ""
359
+ case true
360
+ match (proto-open-escape s)
361
+ case true ""
362
+ case false
363
+ match (chars-within proto-no-dq s)
364
+ case true (string-join "" ["\\"" s "\\""])
365
+ case false
366
+ match (chars-within proto-no-sq s)
367
+ case true (string-join "" ["'" s "'"])
368
+ case false
369
+ match (proto-quotes-escaped "\\"" s)
370
+ case true (string-join "" ["\\"" s "\\""])
371
+ case false
372
+ match (proto-quotes-escaped "'" s)
373
+ case true (string-join "" ["'" s "'"])
374
+ case false ""
375
+
376
+ ; A string literal whose text is s, as written: between quotes, or as the
377
+ ; backtick string it is, which holds a line break no quotes hold. A text
378
+ ; neither holds fails.
379
+ def proto-literal [s]
380
+ match (proto-quoted s)
381
+ case ""
382
+ match (proto-backtick s)
383
+ case true s
384
+ case false (fail :unrepresentable "a string holds what no literal or backtick string holds: a control character, both quotes unescaped, or a backslash that would escape the closing quote")
385
+ case q q
386
+
387
+ ; An option's string: a literal, the backtick string it is, or the
388
+ ; aggregate it is, whose braces hold what no literal holds (a line break,
389
+ ; or both quotes unescaped). A control that no literal, backtick string
390
+ ; or aggregate holds fails.
281
391
  def proto-option-string [s]
282
- match (chars-within proto-no-dq s)
283
- case true (string-join "" ["\\"" s "\\""])
284
- case false
285
- match (chars-within proto-no-sq s)
286
- case true (string-join "" ["'" s "'"])
287
- case false (string-join "" ["{" s "}"])
392
+ match (proto-quoted s)
393
+ case ""
394
+ match (proto-backtick s)
395
+ case true s
396
+ case false
397
+ match (chars-within proto-in-aggregate s)
398
+ case true (string-join "" ["{" s "}"])
399
+ case false (fail :unrepresentable "an option's value holds a control character that no literal, backtick string or aggregate holds")
400
+ case q q
288
401
 
289
402
  ; The options descriptor.proto types as enums, whose values are identifiers.
290
403
  def proto-enum-option [k]
@@ -536,8 +649,16 @@ def proto-reserved-name [syntax n]
536
649
  case "editions"
537
650
  match (proto-identifier n)
538
651
  case true n
539
- case false (proto-literal n)
540
- case _ (proto-literal n)
652
+ case false (proto-name-literal n)
653
+ case _ (proto-name-literal n)
654
+
655
+ ; A reserved name between quotes. The reader takes a reserved name from a
656
+ ; quoted literal or an identifier, and reads the words of a backtick
657
+ ; string as names of their own, so a name no quotes hold fails.
658
+ def proto-name-literal [n]
659
+ match (proto-quoted n)
660
+ case "" (fail :unrepresentable "a reserved name holds what no quotes hold: a control character, both quotes unescaped, or a backslash that would escape the closing quote")
661
+ case q q
541
662
 
542
663
  def proto-reserved-names [syntax indent names]
543
664
  match (count names)