cesr-ts 0.2.3 → 0.3.1
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 +36 -3
- package/esm/src/adapters/async-iterable.js +9 -9
- package/esm/src/annotate/annotator.js +18 -10
- package/esm/src/annotate/comments.js +3 -6
- package/esm/src/annotate/render.js +123 -24
- package/esm/src/bench/parser-benchmark.js +134 -0
- package/esm/src/core/bytes.js +6 -0
- package/esm/src/core/errors.js +24 -0
- package/esm/src/core/parser-attachment-collector.js +154 -0
- package/esm/src/core/parser-constants.js +74 -0
- package/esm/src/core/parser-deferred-frames.js +73 -0
- package/esm/src/core/parser-engine.js +128 -505
- package/esm/src/core/parser-frame-parser.js +643 -0
- package/esm/src/core/parser-policy.js +137 -0
- package/esm/src/core/parser-stream-state.js +62 -0
- package/esm/src/core/recovery-diagnostics.js +25 -0
- package/esm/src/index.js +4 -0
- package/esm/src/parser/attachment-fallback-policy.js +142 -0
- package/esm/src/parser/group-dispatch.js +547 -233
- package/esm/src/primitives/counter.js +4 -5
- package/esm/src/primitives/mapper.js +126 -45
- package/esm/src/primitives/matter.js +1 -1
- package/esm/src/router/router-stub.js +6 -6
- package/esm/src/serder/serder.js +44 -7
- package/esm/src/serder/smell.js +2 -1
- package/esm/src/tables/counter-version-registry.js +201 -0
- package/esm/src/version.js +2 -2
- package/package.json +3 -1
- package/types/_dnt.polyfills.d.ts +5 -0
- package/types/_dnt.polyfills.d.ts.map +1 -1
- package/types/src/adapters/async-iterable.d.ts +2 -2
- package/types/src/adapters/async-iterable.d.ts.map +1 -1
- package/types/src/adapters/effection.d.ts +2 -2
- package/types/src/adapters/effection.d.ts.map +1 -1
- package/types/src/annotate/annotator.d.ts.map +1 -1
- package/types/src/annotate/comments.d.ts.map +1 -1
- package/types/src/annotate/render.d.ts +8 -2
- package/types/src/annotate/render.d.ts.map +1 -1
- package/types/src/annotate/types.d.ts +2 -2
- package/types/src/annotate/types.d.ts.map +1 -1
- package/types/src/bench/parser-benchmark.d.ts +70 -0
- package/types/src/bench/parser-benchmark.d.ts.map +1 -0
- package/types/src/core/bytes.d.ts +6 -0
- package/types/src/core/bytes.d.ts.map +1 -1
- package/types/src/core/errors.d.ts +10 -0
- package/types/src/core/errors.d.ts.map +1 -1
- package/types/src/core/parser-attachment-collector.d.ts +51 -0
- package/types/src/core/parser-attachment-collector.d.ts.map +1 -0
- package/types/src/core/parser-constants.d.ts +30 -0
- package/types/src/core/parser-constants.d.ts.map +1 -0
- package/types/src/core/parser-deferred-frames.d.ts +38 -0
- package/types/src/core/parser-deferred-frames.d.ts.map +1 -0
- package/types/src/core/parser-engine.d.ts +53 -44
- package/types/src/core/parser-engine.d.ts.map +1 -1
- package/types/src/core/parser-frame-parser.d.ts +89 -0
- package/types/src/core/parser-frame-parser.d.ts.map +1 -0
- package/types/src/core/parser-policy.d.ts +27 -0
- package/types/src/core/parser-policy.d.ts.map +1 -0
- package/types/src/core/parser-stream-state.d.ts +30 -0
- package/types/src/core/parser-stream-state.d.ts.map +1 -0
- package/types/src/core/recovery-diagnostics.d.ts +59 -0
- package/types/src/core/recovery-diagnostics.d.ts.map +1 -0
- package/types/src/core/types.d.ts +61 -7
- package/types/src/core/types.d.ts.map +1 -1
- package/types/src/index.d.ts +4 -0
- package/types/src/index.d.ts.map +1 -1
- package/types/src/parser/attachment-fallback-policy.d.ts +78 -0
- package/types/src/parser/attachment-fallback-policy.d.ts.map +1 -0
- package/types/src/parser/group-dispatch.d.ts +85 -15
- package/types/src/parser/group-dispatch.d.ts.map +1 -1
- package/types/src/primitives/aggor.d.ts +2 -2
- package/types/src/primitives/aggor.d.ts.map +1 -1
- package/types/src/primitives/blinder.d.ts +2 -2
- package/types/src/primitives/blinder.d.ts.map +1 -1
- package/types/src/primitives/counter.d.ts.map +1 -1
- package/types/src/primitives/mapper.d.ts +44 -1
- package/types/src/primitives/mapper.d.ts.map +1 -1
- package/types/src/primitives/mediar.d.ts +2 -2
- package/types/src/primitives/mediar.d.ts.map +1 -1
- package/types/src/primitives/sealer.d.ts +2 -2
- package/types/src/primitives/sealer.d.ts.map +1 -1
- package/types/src/router/router-stub.d.ts +5 -5
- package/types/src/router/router-stub.d.ts.map +1 -1
- package/types/src/serder/serder.d.ts +2 -2
- package/types/src/serder/serder.d.ts.map +1 -1
- package/types/src/serder/serdery.d.ts +2 -2
- package/types/src/serder/serdery.d.ts.map +1 -1
- package/types/src/serder/smell.d.ts.map +1 -1
- package/types/src/tables/counter-version-registry.d.ts +90 -0
- package/types/src/tables/counter-version-registry.d.ts.map +1 -0
- package/types/src/version.d.ts +2 -2
|
@@ -1,189 +1,198 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const NON_NATIVE_BODY_CODES = new Set([
|
|
23
|
-
CtrDexV1.NonNativeBodyGroup,
|
|
24
|
-
CtrDexV1.BigNonNativeBodyGroup,
|
|
25
|
-
CtrDexV2.NonNativeBodyGroup,
|
|
26
|
-
CtrDexV2.BigNonNativeBodyGroup,
|
|
27
|
-
]);
|
|
28
|
-
const FIX_BODY_CODES = new Set([
|
|
29
|
-
CtrDexV2.FixBodyGroup,
|
|
30
|
-
CtrDexV2.BigFixBodyGroup,
|
|
31
|
-
]);
|
|
32
|
-
const MAP_BODY_CODES = new Set([
|
|
33
|
-
CtrDexV2.MapBodyGroup,
|
|
34
|
-
CtrDexV2.BigMapBodyGroup,
|
|
35
|
-
]);
|
|
36
|
-
const GENUS_VERSION_CODE = CtrDexV2.KERIACDCGenusVersion;
|
|
37
|
-
function tokenSize(token, cold) {
|
|
38
|
-
return cold === "bny" ? token.fullSizeB2 : token.fullSize;
|
|
39
|
-
}
|
|
40
|
-
function quadletUnit(cold) {
|
|
41
|
-
return cold === "bny" ? 3 : 4;
|
|
42
|
-
}
|
|
43
|
-
function isAttachmentDomain(cold) {
|
|
44
|
-
return cold === "txt" || cold === "bny";
|
|
45
|
-
}
|
|
46
|
-
export class CesrParserCore {
|
|
1
|
+
import { createAttachmentVersionFallbackPolicy, } from "../parser/group-dispatch.js";
|
|
2
|
+
import { ParserError, ShortageError } from "./errors.js";
|
|
3
|
+
import { DEFAULT_VERSION } from "./parser-constants.js";
|
|
4
|
+
import { ParserStreamState } from "./parser-stream-state.js";
|
|
5
|
+
import { DeferredFrameLifecycle } from "./parser-deferred-frames.js";
|
|
6
|
+
import { FrameParser } from "./parser-frame-parser.js";
|
|
7
|
+
import { AttachmentCollector } from "./parser-attachment-collector.js";
|
|
8
|
+
import { createFrameBoundaryPolicy, } from "./parser-policy.js";
|
|
9
|
+
import { composeRecoveryDiagnosticObserver, } from "./recovery-diagnostics.js";
|
|
10
|
+
/**
|
|
11
|
+
* Streaming CESR parser for message-domain and CESR-native body-group streams.
|
|
12
|
+
* Handles chunk boundaries, pending frames, and attachment continuation.
|
|
13
|
+
*/
|
|
14
|
+
export class CesrParser {
|
|
15
|
+
/**
|
|
16
|
+
* Compose parser collaborators with injected policy strategies.
|
|
17
|
+
*
|
|
18
|
+
* Policy precedence:
|
|
19
|
+
* 1) explicit policy objects from `ParserOptions`
|
|
20
|
+
* 2) derived defaults from legacy `framed` and strict/compat options
|
|
21
|
+
*/
|
|
47
22
|
constructor(options = {}) {
|
|
48
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Parser state contract (normative):
|
|
25
|
+
* `docs/design-docs/CESR_PARSER_STATE_MACHINE_CONTRACT.md`
|
|
26
|
+
*/
|
|
27
|
+
Object.defineProperty(this, "frameBoundaryPolicy", {
|
|
28
|
+
enumerable: true,
|
|
29
|
+
configurable: true,
|
|
30
|
+
writable: true,
|
|
31
|
+
value: void 0
|
|
32
|
+
});
|
|
33
|
+
Object.defineProperty(this, "stream", {
|
|
49
34
|
enumerable: true,
|
|
50
35
|
configurable: true,
|
|
51
36
|
writable: true,
|
|
52
|
-
value:
|
|
37
|
+
value: void 0
|
|
53
38
|
});
|
|
54
|
-
Object.defineProperty(this, "
|
|
39
|
+
Object.defineProperty(this, "deferred", {
|
|
55
40
|
enumerable: true,
|
|
56
41
|
configurable: true,
|
|
57
42
|
writable: true,
|
|
58
43
|
value: void 0
|
|
59
44
|
});
|
|
60
|
-
Object.defineProperty(this, "
|
|
45
|
+
Object.defineProperty(this, "frameParser", {
|
|
61
46
|
enumerable: true,
|
|
62
47
|
configurable: true,
|
|
63
48
|
writable: true,
|
|
64
49
|
value: void 0
|
|
65
50
|
});
|
|
66
|
-
Object.defineProperty(this, "
|
|
51
|
+
Object.defineProperty(this, "attachmentCollector", {
|
|
67
52
|
enumerable: true,
|
|
68
53
|
configurable: true,
|
|
69
54
|
writable: true,
|
|
70
55
|
value: void 0
|
|
71
56
|
});
|
|
72
|
-
Object.defineProperty(this, "
|
|
57
|
+
Object.defineProperty(this, "recoveryDiagnosticObserver", {
|
|
73
58
|
enumerable: true,
|
|
74
59
|
configurable: true,
|
|
75
60
|
writable: true,
|
|
76
|
-
value:
|
|
61
|
+
value: void 0
|
|
62
|
+
});
|
|
63
|
+
this.frameBoundaryPolicy = options.frameBoundaryPolicy ??
|
|
64
|
+
createFrameBoundaryPolicy(options.framed ?? false);
|
|
65
|
+
this.recoveryDiagnosticObserver = composeRecoveryDiagnosticObserver({
|
|
66
|
+
onRecoveryDiagnostic: options.onRecoveryDiagnostic,
|
|
67
|
+
onAttachmentVersionFallback: options.attachmentVersionFallbackPolicy
|
|
68
|
+
? undefined
|
|
69
|
+
: options.onAttachmentVersionFallback,
|
|
70
|
+
});
|
|
71
|
+
const attachmentVersionFallbackPolicy = options.attachmentVersionFallbackPolicy ??
|
|
72
|
+
createAttachmentVersionFallbackPolicy({
|
|
73
|
+
mode: options.attachmentDispatchMode,
|
|
74
|
+
});
|
|
75
|
+
this.stream = new ParserStreamState(DEFAULT_VERSION);
|
|
76
|
+
this.deferred = new DeferredFrameLifecycle();
|
|
77
|
+
this.frameParser = new FrameParser({
|
|
78
|
+
frameBoundaryPolicy: this.frameBoundaryPolicy,
|
|
79
|
+
attachmentVersionFallbackPolicy,
|
|
80
|
+
onEnclosedFrames: (frames) => this.deferred.enqueueQueued(frames),
|
|
81
|
+
recoveryDiagnosticObserver: this.recoveryDiagnosticObserver,
|
|
82
|
+
});
|
|
83
|
+
this.attachmentCollector = new AttachmentCollector({
|
|
84
|
+
frameBoundaryPolicy: this.frameBoundaryPolicy,
|
|
85
|
+
attachmentVersionFallbackPolicy,
|
|
86
|
+
recoveryDiagnosticObserver: this.recoveryDiagnosticObserver,
|
|
87
|
+
isFrameBoundaryAhead: (input, version, cold) => this.frameParser.isFrameBoundaryAhead(input, version, cold),
|
|
77
88
|
});
|
|
78
|
-
this.framed = options.framed ?? false;
|
|
79
|
-
this.attachmentDispatchMode = options.attachmentDispatchMode ?? "compat";
|
|
80
|
-
this.onAttachmentVersionFallback = options.onAttachmentVersionFallback;
|
|
81
89
|
}
|
|
82
90
|
/** Append bytes and emit any complete parse events. */
|
|
83
91
|
feed(chunk) {
|
|
84
|
-
this.
|
|
92
|
+
this.stream.append(chunk);
|
|
85
93
|
return this.drain();
|
|
86
94
|
}
|
|
87
95
|
/** Flush pending state at end-of-stream, emitting frame/error if needed. */
|
|
88
96
|
flush() {
|
|
89
|
-
const out =
|
|
90
|
-
if (this.
|
|
91
|
-
out.push({ type: "frame", frame: this.pendingFrame.frame });
|
|
92
|
-
this.pendingFrame = null;
|
|
93
|
-
}
|
|
94
|
-
if (this.state.buffer.length === 0)
|
|
97
|
+
const out = this.deferred.flushDeferred();
|
|
98
|
+
if (this.stream.buffer.length === 0) {
|
|
95
99
|
return out;
|
|
100
|
+
}
|
|
101
|
+
const remainder = this.stream.buffer.length;
|
|
96
102
|
out.push({
|
|
97
103
|
type: "error",
|
|
98
|
-
error: new ShortageError(
|
|
104
|
+
error: new ShortageError(remainder + 1, remainder, this.stream.offset),
|
|
99
105
|
});
|
|
106
|
+
this.stream.consume(remainder);
|
|
100
107
|
return out;
|
|
101
108
|
}
|
|
102
109
|
reset() {
|
|
103
|
-
this.
|
|
104
|
-
this.
|
|
110
|
+
this.stream.reset(DEFAULT_VERSION);
|
|
111
|
+
this.deferred.reset();
|
|
105
112
|
}
|
|
106
113
|
/**
|
|
107
|
-
* Core streaming loop.
|
|
108
|
-
*
|
|
109
|
-
*
|
|
114
|
+
* Core streaming loop. Emit pending frames, then queued frames, then new base frame with trailing attachments.
|
|
115
|
+
* Contract invariants:
|
|
116
|
+
* - `pendingFrame` is always resumed before any queued enclosed frame emits.
|
|
117
|
+
* - `queuedFrames` are emitted before starting a new parse from `buffer`.
|
|
110
118
|
*/
|
|
111
119
|
drain() {
|
|
112
120
|
const out = [];
|
|
113
|
-
while (this.
|
|
121
|
+
while (this.stream.buffer.length > 0) {
|
|
114
122
|
try {
|
|
115
|
-
this.consumeLeadingAno();
|
|
116
|
-
if (this.
|
|
123
|
+
this.stream.consumeLeadingAno();
|
|
124
|
+
if (this.stream.buffer.length === 0) {
|
|
117
125
|
break;
|
|
118
126
|
}
|
|
119
|
-
|
|
120
|
-
|
|
127
|
+
const pending = this.deferred.pending;
|
|
128
|
+
if (pending) {
|
|
129
|
+
const resume = this.attachmentCollector.resumePendingFrame(this.stream.buffer, pending.frame, pending.version, this.stream.streamVersion);
|
|
130
|
+
if (resume.consumed > 0) {
|
|
131
|
+
this.stream.consume(resume.consumed);
|
|
132
|
+
}
|
|
133
|
+
if (resume.emitPending) {
|
|
134
|
+
out.push({ type: "frame", frame: pending.frame });
|
|
135
|
+
this.deferred.clearPending();
|
|
136
|
+
}
|
|
137
|
+
if (!resume.shouldContinue) {
|
|
121
138
|
break;
|
|
122
139
|
}
|
|
123
140
|
continue;
|
|
124
141
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
try {
|
|
131
|
-
while (this.state.buffer.length > 0) {
|
|
132
|
-
const nextCold = sniff(this.state.buffer);
|
|
133
|
-
if (nextCold === "msg")
|
|
142
|
+
if (this.deferred.hasQueued()) {
|
|
143
|
+
const frame = this.deferred.shiftQueued();
|
|
144
|
+
if (frame) {
|
|
145
|
+
out.push({ type: "frame", frame });
|
|
146
|
+
if (this.frameBoundaryPolicy.shouldStopAfterQueuedFrameEmission()) {
|
|
134
147
|
break;
|
|
135
|
-
if (nextCold === "ano") {
|
|
136
|
-
this.consumeLeadingAno();
|
|
137
|
-
continue;
|
|
138
148
|
}
|
|
139
|
-
if (!isAttachmentDomain(nextCold)) {
|
|
140
|
-
throw new ColdStartError(`Unsupported attachment cold code ${nextCold}`);
|
|
141
|
-
}
|
|
142
|
-
const { group, consumed } = parseAttachmentGroup(this.state.buffer, version, nextCold, {
|
|
143
|
-
mode: this.attachmentDispatchMode,
|
|
144
|
-
onVersionFallback: this.onAttachmentVersionFallback,
|
|
145
|
-
});
|
|
146
|
-
attachments.push(group);
|
|
147
|
-
this.consume(consumed);
|
|
148
|
-
if (this.framed)
|
|
149
|
-
break;
|
|
150
149
|
}
|
|
150
|
+
continue;
|
|
151
151
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
pausedForAttachmentShortage = true;
|
|
159
|
-
}
|
|
160
|
-
else {
|
|
161
|
-
throw error;
|
|
162
|
-
}
|
|
152
|
+
const base = this.frameParser.parseFrame(this.stream.buffer, this.stream.streamVersion);
|
|
153
|
+
this.stream.consume(base.consumed);
|
|
154
|
+
this.stream.streamVersion = base.streamVersion;
|
|
155
|
+
const collected = this.attachmentCollector.collectTrailingAttachments(this.stream.buffer, base.version, base.streamVersion, base.frame.attachments);
|
|
156
|
+
if (collected.consumed > 0) {
|
|
157
|
+
this.stream.consume(collected.consumed);
|
|
163
158
|
}
|
|
164
|
-
if (
|
|
159
|
+
if (collected.pausedForShortage) {
|
|
160
|
+
this.deferred.setPending({ body: base.frame.body, attachments: collected.attachments }, base.version);
|
|
165
161
|
break;
|
|
166
162
|
}
|
|
167
163
|
const completed = {
|
|
168
|
-
|
|
169
|
-
attachments,
|
|
164
|
+
body: base.frame.body,
|
|
165
|
+
attachments: collected.attachments,
|
|
170
166
|
};
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
167
|
+
// when in greedy attachment collection mode and a complete body has
|
|
168
|
+
// been received then set the current base frame as the pending frame to
|
|
169
|
+
// wait for attachments to arrive.
|
|
170
|
+
if (this.frameBoundaryPolicy.shouldDeferBodyOnlyFrame(completed.attachments.length, this.stream.buffer.length)) {
|
|
171
|
+
this.deferred.setPending(completed, base.version);
|
|
174
172
|
break;
|
|
175
173
|
}
|
|
174
|
+
// Once all attachments have arrived then emit the current completed frame
|
|
176
175
|
out.push({ type: "frame", frame: completed });
|
|
177
|
-
if (this.
|
|
176
|
+
if (this.frameBoundaryPolicy.shouldStopAfterCompletedFrameEmission()) {
|
|
178
177
|
break;
|
|
178
|
+
}
|
|
179
179
|
}
|
|
180
180
|
catch (error) {
|
|
181
|
+
// Shortage Errors are expected when waiting for bytes to arrive for
|
|
182
|
+
// either message bodies (base) frames or attachment frames.
|
|
181
183
|
if (error instanceof ShortageError) {
|
|
182
184
|
break;
|
|
183
185
|
}
|
|
186
|
+
// For any other error, push a ParserError, emit an error frame, and reset
|
|
184
187
|
const normalized = error instanceof ParserError
|
|
185
188
|
? error
|
|
186
|
-
: new ParserError(String(error), this.
|
|
189
|
+
: new ParserError(String(error), this.stream.offset);
|
|
190
|
+
this.recoveryDiagnosticObserver?.({
|
|
191
|
+
type: "parser-error-reset",
|
|
192
|
+
offset: this.stream.offset,
|
|
193
|
+
errorName: normalized.name,
|
|
194
|
+
reason: normalized.message,
|
|
195
|
+
});
|
|
187
196
|
out.push({ type: "error", error: normalized });
|
|
188
197
|
this.reset();
|
|
189
198
|
break;
|
|
@@ -191,398 +200,12 @@ export class CesrParserCore {
|
|
|
191
200
|
}
|
|
192
201
|
return out;
|
|
193
202
|
}
|
|
194
|
-
resumePendingFrame(out) {
|
|
195
|
-
if (!this.pendingFrame)
|
|
196
|
-
return false;
|
|
197
|
-
if (this.state.buffer.length === 0)
|
|
198
|
-
return false;
|
|
199
|
-
const nextCold = sniff(this.state.buffer);
|
|
200
|
-
if (nextCold === "ano") {
|
|
201
|
-
this.consumeLeadingAno();
|
|
202
|
-
return this.state.buffer.length > 0;
|
|
203
|
-
}
|
|
204
|
-
if (nextCold === "msg") {
|
|
205
|
-
out.push({ type: "frame", frame: this.pendingFrame.frame });
|
|
206
|
-
this.pendingFrame = null;
|
|
207
|
-
return true;
|
|
208
|
-
}
|
|
209
|
-
if (!isAttachmentDomain(nextCold)) {
|
|
210
|
-
throw new ColdStartError(`Unsupported pending-frame continuation cold code ${nextCold}`);
|
|
211
|
-
}
|
|
212
|
-
if (this.pendingFrame.frame.serder.kind === Kinds.cesr) {
|
|
213
|
-
const peek = parseCounter(this.state.buffer, this.pendingFrame.version, nextCold);
|
|
214
|
-
if (BODY_WITH_ATTACH_CODES.has(peek.code) ||
|
|
215
|
-
NON_NATIVE_BODY_CODES.has(peek.code) ||
|
|
216
|
-
FIX_BODY_CODES.has(peek.code) ||
|
|
217
|
-
MAP_BODY_CODES.has(peek.code) ||
|
|
218
|
-
peek.code === GENUS_VERSION_CODE) {
|
|
219
|
-
out.push({ type: "frame", frame: this.pendingFrame.frame });
|
|
220
|
-
this.pendingFrame = null;
|
|
221
|
-
return true;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
const { group, consumed } = parseAttachmentGroup(this.state.buffer, this.pendingFrame.version, nextCold, {
|
|
225
|
-
mode: this.attachmentDispatchMode,
|
|
226
|
-
onVersionFallback: this.onAttachmentVersionFallback,
|
|
227
|
-
});
|
|
228
|
-
this.pendingFrame.frame.attachments.push(group);
|
|
229
|
-
this.consume(consumed);
|
|
230
|
-
if (this.framed) {
|
|
231
|
-
out.push({ type: "frame", frame: this.pendingFrame.frame });
|
|
232
|
-
this.pendingFrame = null;
|
|
233
|
-
return false;
|
|
234
|
-
}
|
|
235
|
-
if (this.state.buffer.length === 0) {
|
|
236
|
-
return false;
|
|
237
|
-
}
|
|
238
|
-
const afterCold = sniff(this.state.buffer);
|
|
239
|
-
if (afterCold === "ano") {
|
|
240
|
-
this.consumeLeadingAno();
|
|
241
|
-
return this.state.buffer.length > 0;
|
|
242
|
-
}
|
|
243
|
-
if (afterCold === "msg") {
|
|
244
|
-
out.push({ type: "frame", frame: this.pendingFrame.frame });
|
|
245
|
-
this.pendingFrame = null;
|
|
246
|
-
return true;
|
|
247
|
-
}
|
|
248
|
-
return true;
|
|
249
|
-
}
|
|
250
|
-
consume(length) {
|
|
251
|
-
this.state.buffer = this.state.buffer.slice(length);
|
|
252
|
-
this.state.offset += length;
|
|
253
|
-
}
|
|
254
|
-
consumeLeadingAno() {
|
|
255
|
-
while (this.state.buffer.length > 0 && sniff(this.state.buffer) === "ano") {
|
|
256
|
-
this.consume(1);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
|
-
* Parse a single top-level frame start from current buffer head.
|
|
261
|
-
* Supports message-domain serders and CESR-native body groups.
|
|
262
|
-
*/
|
|
263
|
-
parseFrame(input, inheritedVersion = DEFAULT_VERSION) {
|
|
264
|
-
let offset = 0;
|
|
265
|
-
let activeVersion = inheritedVersion;
|
|
266
|
-
let cold = sniff(input.slice(offset));
|
|
267
|
-
while (cold === "ano") {
|
|
268
|
-
offset += 1;
|
|
269
|
-
if (input.length <= offset) {
|
|
270
|
-
throw new ShortageError(offset + 1, input.length);
|
|
271
|
-
}
|
|
272
|
-
cold = sniff(input.slice(offset));
|
|
273
|
-
}
|
|
274
|
-
if (cold === "txt" || cold === "bny") {
|
|
275
|
-
const peek = parseCounter(input.slice(offset), activeVersion, cold);
|
|
276
|
-
if (peek.code === GENUS_VERSION_CODE) {
|
|
277
|
-
offset += tokenSize(peek, cold);
|
|
278
|
-
activeVersion = this.decodeVersionCounter(peek);
|
|
279
|
-
if (input.length <= offset) {
|
|
280
|
-
throw new ShortageError(offset + 1, input.length);
|
|
281
|
-
}
|
|
282
|
-
cold = sniff(input.slice(offset));
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
if (cold === "msg") {
|
|
286
|
-
const { serder, consumed } = reapSerder(input.slice(offset));
|
|
287
|
-
return {
|
|
288
|
-
frame: { serder, attachments: [] },
|
|
289
|
-
consumed: offset + consumed,
|
|
290
|
-
version: serder.gvrsn ?? serder.pvrsn,
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
if (!isAttachmentDomain(cold)) {
|
|
294
|
-
throw new ColdStartError(`Expected message or CESR body group at frame start but got ${cold}`);
|
|
295
|
-
}
|
|
296
|
-
const counter = parseCounter(input.slice(offset), activeVersion, cold);
|
|
297
|
-
const headerSize = tokenSize(counter, cold);
|
|
298
|
-
const unit = quadletUnit(cold);
|
|
299
|
-
if (BODY_WITH_ATTACH_CODES.has(counter.code)) {
|
|
300
|
-
return this.parseBodyWithAttachmentGroup(input, offset, headerSize, counter.count, unit, activeVersion);
|
|
301
|
-
}
|
|
302
|
-
if (NON_NATIVE_BODY_CODES.has(counter.code)) {
|
|
303
|
-
return this.parseNonNativeBodyGroup(input, offset, headerSize, counter.count, unit, cold, activeVersion);
|
|
304
|
-
}
|
|
305
|
-
if (FIX_BODY_CODES.has(counter.code) || MAP_BODY_CODES.has(counter.code)) {
|
|
306
|
-
return this.parseNativeBodyGroup(input, offset, headerSize, counter.count, unit, cold, activeVersion, counter.code);
|
|
307
|
-
}
|
|
308
|
-
throw new ColdStartError(`Unsupported body-group counter at frame start: ${counter.code}`);
|
|
309
|
-
}
|
|
310
|
-
/** Parse wrapped body+attachments payloads as a complete nested frame. */
|
|
311
|
-
parseBodyWithAttachmentGroup(input, offset, headerSize, count, unit, version) {
|
|
312
|
-
const payloadSize = count * unit;
|
|
313
|
-
const total = headerSize + payloadSize;
|
|
314
|
-
if (input.length < offset + total) {
|
|
315
|
-
throw new ShortageError(offset + total, input.length);
|
|
316
|
-
}
|
|
317
|
-
const payload = input.slice(offset + headerSize, offset + total);
|
|
318
|
-
const nested = this.parseCompleteFrame(payload, version, false);
|
|
319
|
-
if (nested.consumed !== payload.length) {
|
|
320
|
-
throw new ColdStartError("BodyWithAttachmentGroup payload did not parse to a complete frame");
|
|
321
|
-
}
|
|
322
|
-
return {
|
|
323
|
-
frame: nested.frame,
|
|
324
|
-
consumed: offset + total,
|
|
325
|
-
version: nested.frame.serder.gvrsn ?? nested.frame.serder.pvrsn,
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
/** Parse a non-native body group; keep opaque fallback when serder reap fails. */
|
|
329
|
-
parseNonNativeBodyGroup(input, offset, headerSize, count, unit, cold, version) {
|
|
330
|
-
const matter = parseMatter(input.slice(offset + headerSize), cold);
|
|
331
|
-
const bodySize = tokenSize(matter, cold);
|
|
332
|
-
const payloadSize = count * unit;
|
|
333
|
-
if (payloadSize !== bodySize) {
|
|
334
|
-
throw new ColdStartError(`NonNativeBodyGroup payload size mismatch: expected=${payloadSize} actual=${bodySize}`);
|
|
335
|
-
}
|
|
336
|
-
try {
|
|
337
|
-
const { serder } = reapSerder(matter.raw);
|
|
338
|
-
return {
|
|
339
|
-
frame: { serder, attachments: [] },
|
|
340
|
-
consumed: offset + headerSize + bodySize,
|
|
341
|
-
version: serder.gvrsn ?? serder.pvrsn,
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
catch (_error) {
|
|
345
|
-
// Intentional recovery: NonNativeBodyGroup payload is opaque CESR body data.
|
|
346
|
-
// If it cannot be interpreted as a KERI/ACDC Serder, preserve bytes and
|
|
347
|
-
// continue with conservative metadata instead of failing the frame parse.
|
|
348
|
-
return {
|
|
349
|
-
frame: {
|
|
350
|
-
serder: {
|
|
351
|
-
raw: matter.raw,
|
|
352
|
-
ked: null,
|
|
353
|
-
proto: Protocols.keri,
|
|
354
|
-
kind: Kinds.cesr,
|
|
355
|
-
size: matter.raw.length,
|
|
356
|
-
pvrsn: version,
|
|
357
|
-
gvrsn: version,
|
|
358
|
-
ilk: null,
|
|
359
|
-
said: null,
|
|
360
|
-
},
|
|
361
|
-
attachments: [],
|
|
362
|
-
},
|
|
363
|
-
consumed: offset + headerSize + bodySize,
|
|
364
|
-
version,
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
/** Parse fixed/map native body groups and extract semantic native fields. */
|
|
369
|
-
parseNativeBodyGroup(input, offset, headerSize, count, unit, cold, version, bodyCode) {
|
|
370
|
-
const payloadSize = count * unit;
|
|
371
|
-
const total = headerSize + payloadSize;
|
|
372
|
-
if (input.length < offset + total) {
|
|
373
|
-
throw new ShortageError(offset + total, input.length);
|
|
374
|
-
}
|
|
375
|
-
const raw = input.slice(offset, offset + total);
|
|
376
|
-
const metadata = this.extractNativeMetadata(raw, cold, version);
|
|
377
|
-
const fields = this.extractNativeFields(raw, cold, version);
|
|
378
|
-
return {
|
|
379
|
-
frame: {
|
|
380
|
-
serder: {
|
|
381
|
-
raw,
|
|
382
|
-
ked: null,
|
|
383
|
-
proto: metadata.proto,
|
|
384
|
-
kind: Kinds.cesr,
|
|
385
|
-
size: raw.length,
|
|
386
|
-
pvrsn: metadata.pvrsn,
|
|
387
|
-
gvrsn: metadata.gvrsn,
|
|
388
|
-
ilk: metadata.ilk,
|
|
389
|
-
said: metadata.said,
|
|
390
|
-
native: {
|
|
391
|
-
bodyCode,
|
|
392
|
-
fields,
|
|
393
|
-
},
|
|
394
|
-
},
|
|
395
|
-
attachments: [],
|
|
396
|
-
},
|
|
397
|
-
consumed: offset + total,
|
|
398
|
-
version,
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
/** Decode genus-version counter suffix to active CESR version. */
|
|
402
|
-
decodeVersionCounter(counter) {
|
|
403
|
-
const triplet = counter.qb64.length >= 3
|
|
404
|
-
? counter.qb64.slice(-3)
|
|
405
|
-
: intToB64(counter.count, 3);
|
|
406
|
-
const majorRaw = b64ToInt(triplet[0] ?? "A");
|
|
407
|
-
const minorRaw = b64ToInt(triplet[1] ?? "A");
|
|
408
|
-
const major = majorRaw === 1 ? 1 : 2;
|
|
409
|
-
return {
|
|
410
|
-
major,
|
|
411
|
-
minor: minorRaw,
|
|
412
|
-
};
|
|
413
|
-
}
|
|
414
|
-
/**
|
|
415
|
-
* Best-effort native metadata extraction.
|
|
416
|
-
* This is intentionally advisory and may recover instead of failing hard.
|
|
417
|
-
*/
|
|
418
|
-
extractNativeMetadata(raw, cold, fallbackVersion) {
|
|
419
|
-
let offset = 0;
|
|
420
|
-
let proto = Protocols.keri;
|
|
421
|
-
let pvrsn = fallbackVersion;
|
|
422
|
-
let gvrsn = fallbackVersion;
|
|
423
|
-
let ilk = null;
|
|
424
|
-
let said = null;
|
|
425
|
-
try {
|
|
426
|
-
const bodyCounter = parseCounter(raw, fallbackVersion, cold);
|
|
427
|
-
offset += cold === "bny" ? bodyCounter.fullSizeB2 : bodyCounter.fullSize;
|
|
428
|
-
// In map-body mode, labels may appear between semantic fields.
|
|
429
|
-
// Metadata extraction skips those labels before reading core fields.
|
|
430
|
-
if (MAP_BODY_CODES.has(bodyCounter.code)) {
|
|
431
|
-
offset = this.skipNativeLabelers(raw, offset, cold);
|
|
432
|
-
}
|
|
433
|
-
const verser = parseVerser(raw.slice(offset), cold);
|
|
434
|
-
offset += tokenSize(verser, cold);
|
|
435
|
-
proto = verser.proto;
|
|
436
|
-
pvrsn = verser.pvrsn;
|
|
437
|
-
gvrsn = verser.gvrsn;
|
|
438
|
-
if (MAP_BODY_CODES.has(bodyCounter.code)) {
|
|
439
|
-
offset = this.skipNativeLabelers(raw, offset, cold);
|
|
440
|
-
}
|
|
441
|
-
const ilker = parseIlker(raw.slice(offset), cold);
|
|
442
|
-
offset += tokenSize(ilker, cold);
|
|
443
|
-
ilk = ilker.ilk;
|
|
444
|
-
if (MAP_BODY_CODES.has(bodyCounter.code)) {
|
|
445
|
-
offset = this.skipNativeLabelers(raw, offset, cold);
|
|
446
|
-
}
|
|
447
|
-
const saider = parseMatter(raw.slice(offset), cold);
|
|
448
|
-
if (saider.code.startsWith("E")) {
|
|
449
|
-
said = saider.qb64;
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
catch (_error) {
|
|
453
|
-
// Intentional recovery: native metadata extraction is advisory only.
|
|
454
|
-
// Strict token validation happens in extractNativeFields/mapper parsing.
|
|
455
|
-
}
|
|
456
|
-
return { proto, pvrsn, gvrsn, ilk, said };
|
|
457
|
-
}
|
|
458
|
-
/** Skip consecutive native label tokens (V/W) from an offset. */
|
|
459
|
-
skipNativeLabelers(raw, offset, cold) {
|
|
460
|
-
let out = offset;
|
|
461
|
-
while (out < raw.length) {
|
|
462
|
-
const item = parseMatter(raw.slice(out), cold);
|
|
463
|
-
if (!isLabelerCode(item.code)) {
|
|
464
|
-
break;
|
|
465
|
-
}
|
|
466
|
-
out += tokenSize(item, cold);
|
|
467
|
-
}
|
|
468
|
-
return out;
|
|
469
|
-
}
|
|
470
|
-
/** Strict native field extraction for annotation and higher-level processing. */
|
|
471
|
-
extractNativeFields(raw, cold, fallbackVersion) {
|
|
472
|
-
const bodyCounter = parseCounter(raw, fallbackVersion, cold);
|
|
473
|
-
if (MAP_BODY_CODES.has(bodyCounter.code)) {
|
|
474
|
-
const mapper = parseMapperBody(raw, fallbackVersion, cold);
|
|
475
|
-
return mapper.fields.map((field) => ({
|
|
476
|
-
label: field.label,
|
|
477
|
-
code: field.code,
|
|
478
|
-
qb64: field.qb64,
|
|
479
|
-
}));
|
|
480
|
-
}
|
|
481
|
-
const fields = [];
|
|
482
|
-
const total = tokenSize(bodyCounter, cold);
|
|
483
|
-
const payloadBytes = bodyCounter.count * quadletUnit(cold);
|
|
484
|
-
const start = total;
|
|
485
|
-
const end = start + payloadBytes;
|
|
486
|
-
let offset = start;
|
|
487
|
-
let pendingLabel = null;
|
|
488
|
-
while (offset < end) {
|
|
489
|
-
const at = raw.slice(offset, end);
|
|
490
|
-
const ctr = this.tryParseCounter(at, fallbackVersion, cold);
|
|
491
|
-
if (ctr) {
|
|
492
|
-
const size = tokenSize(ctr, cold);
|
|
493
|
-
offset += size;
|
|
494
|
-
fields.push({
|
|
495
|
-
label: pendingLabel,
|
|
496
|
-
code: ctr.code,
|
|
497
|
-
qb64: ctr.qb64,
|
|
498
|
-
});
|
|
499
|
-
pendingLabel = null;
|
|
500
|
-
continue;
|
|
501
|
-
}
|
|
502
|
-
const token = parseMatter(at, cold);
|
|
503
|
-
const size = tokenSize(token, cold);
|
|
504
|
-
offset += size;
|
|
505
|
-
if (isLabelerCode(token.code)) {
|
|
506
|
-
pendingLabel = parseLabeler(at, cold).label;
|
|
507
|
-
continue;
|
|
508
|
-
}
|
|
509
|
-
fields.push({
|
|
510
|
-
label: pendingLabel,
|
|
511
|
-
code: token.code,
|
|
512
|
-
qb64: token.qb64,
|
|
513
|
-
});
|
|
514
|
-
pendingLabel = null;
|
|
515
|
-
}
|
|
516
|
-
if (offset !== end) {
|
|
517
|
-
throw new ShortageError(end, offset);
|
|
518
|
-
}
|
|
519
|
-
if (pendingLabel !== null) {
|
|
520
|
-
throw new ColdStartError("Dangling native map label without value");
|
|
521
|
-
}
|
|
522
|
-
return fields;
|
|
523
|
-
}
|
|
524
|
-
/** Counter probe with ordered version fallback; throws on shortage. */
|
|
525
|
-
tryParseCounter(input, version, cold) {
|
|
526
|
-
const attempts = [
|
|
527
|
-
version,
|
|
528
|
-
{ major: 2, minor: 0 },
|
|
529
|
-
{ major: 1, minor: 0 },
|
|
530
|
-
];
|
|
531
|
-
for (const attempt of attempts) {
|
|
532
|
-
try {
|
|
533
|
-
return parseCounter(input, attempt, cold);
|
|
534
|
-
}
|
|
535
|
-
catch (error) {
|
|
536
|
-
if (error instanceof ShortageError) {
|
|
537
|
-
throw error;
|
|
538
|
-
}
|
|
539
|
-
if (error instanceof UnknownCodeError ||
|
|
540
|
-
error instanceof DeserializeError) {
|
|
541
|
-
continue;
|
|
542
|
-
}
|
|
543
|
-
throw error;
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
return null;
|
|
547
|
-
}
|
|
548
|
-
/** Parse one complete frame plus optional attachments from a standalone slice. */
|
|
549
|
-
parseCompleteFrame(input, inheritedVersion = DEFAULT_VERSION, stopAtNextMessage = true) {
|
|
550
|
-
const base = this.parseFrame(input, inheritedVersion);
|
|
551
|
-
const version = base.version;
|
|
552
|
-
const attachments = [];
|
|
553
|
-
let offset = base.consumed;
|
|
554
|
-
while (offset < input.length) {
|
|
555
|
-
const nextCold = sniff(input.slice(offset));
|
|
556
|
-
if (nextCold === "ano") {
|
|
557
|
-
offset += 1;
|
|
558
|
-
continue;
|
|
559
|
-
}
|
|
560
|
-
if (nextCold === "msg") {
|
|
561
|
-
if (stopAtNextMessage)
|
|
562
|
-
break;
|
|
563
|
-
throw new ColdStartError("Enclosed frame payload encountered unexpected nested message start");
|
|
564
|
-
}
|
|
565
|
-
if (!isAttachmentDomain(nextCold)) {
|
|
566
|
-
throw new ColdStartError(`Unsupported attachment cold code ${nextCold}`);
|
|
567
|
-
}
|
|
568
|
-
const { group, consumed } = parseAttachmentGroup(input.slice(offset), version, nextCold, {
|
|
569
|
-
mode: this.attachmentDispatchMode,
|
|
570
|
-
onVersionFallback: this.onAttachmentVersionFallback,
|
|
571
|
-
});
|
|
572
|
-
attachments.push(group);
|
|
573
|
-
offset += consumed;
|
|
574
|
-
if (this.framed)
|
|
575
|
-
break;
|
|
576
|
-
}
|
|
577
|
-
return {
|
|
578
|
-
frame: { serder: base.frame.serder, attachments },
|
|
579
|
-
consumed: offset,
|
|
580
|
-
};
|
|
581
|
-
}
|
|
582
203
|
}
|
|
204
|
+
/** CESR parser factory function. */
|
|
583
205
|
export function createParser(options = {}) {
|
|
584
|
-
return new
|
|
206
|
+
return new CesrParser(options);
|
|
585
207
|
}
|
|
208
|
+
/** Parse a buffer of bytes into a list of frames. */
|
|
586
209
|
export function parseBytes(bytes, options = {}) {
|
|
587
210
|
const parser = createParser(options);
|
|
588
211
|
return [...parser.feed(bytes), ...parser.flush()];
|