@cosyte/astm 0.0.11 → 0.0.14
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/CHANGELOG.md +196 -0
- package/README.md +19 -2
- package/dist/index.cjs +47 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +206 -45
- package/dist/index.d.ts +206 -45
- package/dist/index.mjs +47 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,202 @@ this file is maintained by hand (Changesets handles the version bump and publish
|
|
|
11
11
|
|
|
12
12
|
### Fixed
|
|
13
13
|
|
|
14
|
+
- **`composeAstmFrames` wrote an out-of-range `startFrameNumber` straight into the frame-number
|
|
15
|
+
position, so a value that was not a frame number produced a stream whose records the decoder
|
|
16
|
+
refused to emit** (`ASTM-FRAME-RESIDUALS`, defect 13). `PRE-EXISTING`, byte-identical on `64c2fd5`.
|
|
17
|
+
The option is now checked before `composeAstmFrames` reads a record, and refused with the new code
|
|
18
|
+
`ASTM_FRAME_INVALID_START_FRAME_NUMBER` (a fourth member of `AstmFrameEncodeErrorCode`) unless it is
|
|
19
|
+
a whole number from `0` to `7`.
|
|
20
|
+
|
|
21
|
+
**What went out instead, measured on this package's own round trip.** A frame's number is one ASCII
|
|
22
|
+
digit, written as `FN_ZERO + n`, and nothing checked `n`. `-1` put a `/` in the frame-number
|
|
23
|
+
position, and `NaN`, `Infinity` and `-Infinity` each put a `NUL` there in **every** frame of the
|
|
24
|
+
stream, after which `decodeAstmFrames` recognised no frame number at all, warned
|
|
25
|
+
`ASTM_FRAME_SEQUENCE_GAP` on every frame, and emitted **none** of the records: a whole four-record
|
|
26
|
+
message in, zero out, and `parseFramedAstm` throwing on empty input. **The recorded defect said an
|
|
27
|
+
out-of-range value writes a non-digit byte and named a space for `NaN`. It is a `NUL`**, which is
|
|
28
|
+
the difference between an ordinary inter-frame byte the decoder skips and a byte that costs the
|
|
29
|
+
stream every one of its records.
|
|
30
|
+
|
|
31
|
+
**The quieter half is why the check covers the whole domain rather than the ends.** Values that
|
|
32
|
+
truncated back onto a digit were accepted silently and behaved as some other start value: `1.5` and
|
|
33
|
+
`257` each produced the byte-for-byte stream a `startFrameNumber` of `1` produces. The option
|
|
34
|
+
documented `0`-`7` and enforced neither bound nor integrality in either direction.
|
|
35
|
+
|
|
36
|
+
**The domain is derived from what a frame can carry, and the refusal is not a clamp.** The writable
|
|
37
|
+
numbers are exactly the whole numbers `0`-`7`, the modulo-8 sequence `decodeAstmFrames` reads and
|
|
38
|
+
rolls over. Clamping or taking a modulo would pick a frame number the caller did not ask for, and
|
|
39
|
+
the frame number is the decoder's only evidence that no frame was dropped, so a stream numbered from
|
|
40
|
+
a value nobody chose is a stream whose sequence check certifies the wrong thing. The refusal is
|
|
41
|
+
raised before `composeAstmFrames` reads a record, so on that entry point it never depends on the
|
|
42
|
+
caller's data (`serializeFramedAstm` serializes every record first, so a record that cannot be
|
|
43
|
+
serialized at all is refused ahead of it on that route). Its message names the value received: that
|
|
44
|
+
value is the caller's own option, not stream content, and nothing from the records reaches it.
|
|
45
|
+
|
|
46
|
+
**The whole `0`-`7` range is still accepted, because a non-default start has a real use that was
|
|
47
|
+
measured rather than assumed.** It composes a **continuation** of a transfer already in progress:
|
|
48
|
+
`composeAstmFrames(head)` joined with `composeAstmFrames(tail, { startFrameNumber: n })`, where `n`
|
|
49
|
+
is the number after the last frame `head` used, is **byte-identical** to composing the whole list in
|
|
50
|
+
one call and decodes with an empty warnings array, across the `7 → 0` rollover included. Narrowing
|
|
51
|
+
the option to `1` would have removed that.
|
|
52
|
+
|
|
53
|
+
**What a continuation is not, now stated on the option rather than guarded against.** Read on its
|
|
54
|
+
own, a stream that starts anywhere but `1` opens on a sequence gap; the decoder never bridges a gap
|
|
55
|
+
silently, so it warns and does not emit that first record. **What `parseFramedAstm` does after that
|
|
56
|
+
varies with the message shape, and no rule is offered for it:** it may throw, under more than one
|
|
57
|
+
code (`ASTM_RECORD_NO_HEADER`, `EMPTY_INPUT` and `ASTM_RECORD_UNDECLARED_DELIMITERS` were each
|
|
58
|
+
measured), and it may return a message one record short. **The one statement that generalizes is
|
|
59
|
+
that the record layer never reports the loss**, because `parseFramedAstm` hands the record parser
|
|
60
|
+
only the frames the codec vouched for, so `message.warnings` carries what the surviving records
|
|
61
|
+
warrant and nothing about the record that did not survive. Read `frameWarnings`. Nothing returns the number to continue a sequence from either, and the frame count
|
|
62
|
+
is not it once a record splits, so the supported computation is named on the option. The
|
|
63
|
+
documented-valid `0` has always behaved that way. It is the cost of the option, not a defect in the
|
|
64
|
+
caller's records, and it is why `1` is the default.
|
|
65
|
+
|
|
66
|
+
**This is a narrowing on a published package, in a small population.** `1.5` and `257` used to
|
|
67
|
+
produce a working stream, the byte-for-byte start-at-`1` one, and now throw. Every value the check
|
|
68
|
+
turns away was already outside what the option documented: a frame number, a whole number `0`-`7`.
|
|
69
|
+
|
|
70
|
+
Both the old bytes and the new refusal are pinned in `test/frames/start-frame-number.test.ts`,
|
|
71
|
+
asserted on what the old encoder produced (rebuilt with the test-only `frame()` builder, so nothing
|
|
72
|
+
is transcribed twice), with a biconditional property: a value is refused **if and only if** it is
|
|
73
|
+
not a whole number in `0`-`7`. 14 of the 32 new tests are red against `64c2fd5`.
|
|
74
|
+
|
|
75
|
+
- **Three `{@link}` targets in the published `.d.ts` did not name a symbol declared there**
|
|
76
|
+
(`ASTM-FRAME-RESIDUALS`, the sibling minors). `QuirkTolerance` is `AstmQuirkTolerance` (twice, in
|
|
77
|
+
`src/profiles/types.ts`); the `startFrameNumber` doc linked `FIRST_FRAME_NUMBER`, which this
|
|
78
|
+
package does not export, and that sentence was rewritten and no longer links it; and
|
|
79
|
+
`AstmMessage.profile` linked a bare `warnings`, meaning its sibling member, which resolves only for
|
|
80
|
+
a tool that resolves against the enclosing declaration. Every `{@link}` target in
|
|
81
|
+
`dist/index.d.ts` now names a symbol declared in that file.
|
|
82
|
+
|
|
83
|
+
- **A delimiter set colliding with a record's type letter emitted a stream that read back as
|
|
84
|
+
DIFFERENT records, and in its silent branch it fabricated a final lab result out of patient
|
|
85
|
+
identifiers** (`ASTM-FRAME-RESIDUALS`, defect 5). `PRE-EXISTING`, byte-identical on `7253098`,
|
|
86
|
+
before any of `ASTM-EMIT-RESIDUALS`. `serializeAstmRecords` / `serializeAstmRecord` now throw
|
|
87
|
+
`AstmSerializeError` with the new code `ASTM_EMIT_TYPE_LETTER_COLLISION` (a third member of
|
|
88
|
+
`AstmSerializeErrorCode`), carrying the `recordIndex` and quoting nothing from the record: not the
|
|
89
|
+
value, and not the type letter either, which on an unsupported record is a byte off the wire.
|
|
90
|
+
|
|
91
|
+
**The recorded defect said the record "re-reads as an unsupported record, one result in, zero out".
|
|
92
|
+
That describes one branch of two, and the branch it does not describe is silent.** A record's type
|
|
93
|
+
letter is just another leaf to the escape encoder, so a set naming that letter escapes it away: an
|
|
94
|
+
`R` record emitted with `field` = `R` goes out as `&F&R1R…`, reads back as `unsupported`, and
|
|
95
|
+
`ASTM_RECORD_UNKNOWN_TYPE` does fire. But the encoder writes an escaped character as
|
|
96
|
+
escape + mnemonic + escape, so **when the escape character is itself a record type letter the
|
|
97
|
+
escaped type letter begins with a real letter** and no unknown-type warning fires at all. Measured:
|
|
98
|
+
a `P` record emitted with `field` = `P` and `escape` = `R` comes back as an `R` record whose
|
|
99
|
+
`value` is the patient's laboratory ID, whose `units` are the practice-assigned identifier, and
|
|
100
|
+
whose `resultStatus` is `F`, so `results()` returns a fabricated **final** result built out of
|
|
101
|
+
patient identifiers. The stream's only warning is `ASTM_NONSTANDARD_DELIMITERS`, which is what a
|
|
102
|
+
**clean** non-canonical stream carries too and which is on the profile safety gate's tolerable
|
|
103
|
+
allow-list, so `{ strict: true }` accepts it under a profile the gate permits. That allow-list
|
|
104
|
+
entry reasons that "a record's type letter is the first character of its line" is read before any
|
|
105
|
+
delimiter-driven tokenization: true of the parse, and no protection when the **emit** is what chose
|
|
106
|
+
that character.
|
|
107
|
+
|
|
108
|
+
**The rule is derived from the reader and checked on the bytes, not on the delimiter set.**
|
|
109
|
+
`parseAstmRecords` takes a record's type from `line.charAt(0)`, so the condition emit has to meet
|
|
110
|
+
is that the first character it writes is the letter the record models. Testing the output rather
|
|
111
|
+
than enumerating dangerous roles is what makes a type letter equal to the **escape** character fall
|
|
112
|
+
out as accepted without a special case: it is written starting with that letter, so the record
|
|
113
|
+
re-reads as its own type. The familiar `letter` + `E` + `letter` shape is not general and the guard
|
|
114
|
+
does not rely on it: the encoder protects the escape character it introduces but not the `E`/`F`/`S`
|
|
115
|
+
mnemonics, so under `{ field: "E", escape: "R" }` an `R` encodes to `RRFRR` and its type field
|
|
116
|
+
decodes back to `RER`, not `R`. What holds across every such set is the only thing checked, the
|
|
117
|
+
first character written. Measured over 137,632
|
|
118
|
+
delimiter sets (two roles at a time, every printable ASCII character each, against a stream
|
|
119
|
+
carrying one record of every modeled type plus an unsupported one), the refusal is
|
|
120
|
+
**biconditional** with the old serializer losing a type letter: zero over-refusals and zero
|
|
121
|
+
under-refusals. Over a second sweep of 3,690 emits across nine record-set shapes, 750 streams
|
|
122
|
+
previously read back as something other than the records that produced them, **303 of them accepted
|
|
123
|
+
by `{ strict: true }`**; all 750 are now refused and the remaining 2,940 are byte-unchanged.
|
|
124
|
+
|
|
125
|
+
This is a **narrowing on a published package**, in exactly the cases that were being corrupted.
|
|
126
|
+
**It is a transcoding condition, not a judgement on the set the caller passed, so it fires with no
|
|
127
|
+
delimiter argument at all** and "emit against the canonical delimiters" is not a remedy for it: a
|
|
128
|
+
stream whose header declares a vendor set and which carries one garbled line beginning `|` parses
|
|
129
|
+
to an unsupported record whose type letter is `|`, and the canonical set escapes that `|` away, so
|
|
130
|
+
`serializeAstmRecords(msg)`, `serializeAstmRecord(record)` and `serializeFramedAstm(msg)` all
|
|
131
|
+
refuse it where the base emitted a record whose `rawType` came back as `&`. Callers passing a set
|
|
132
|
+
explicitly (`serializeAstmRecords(msg, msg.delimiters)`) reach it too. The three
|
|
133
|
+
set-level conditions are unchanged and still raise `ASTM_EMIT_INVALID_DELIMITERS`; this is a
|
|
134
|
+
separate, per-record check because whether a set collides depends on which record is being written.
|
|
135
|
+
**What it does not promise, stated rather than left to be found:** it guarantees a record re-reads
|
|
136
|
+
as its own **type**, not that every field lands where it did (an escape sequence whose body is a
|
|
137
|
+
delimiter is still an opaque atom, reported on the parse side as `ASTM_UNKNOWN_ESCAPE_SEQUENCE`),
|
|
138
|
+
and `encodeComponent` / `serializeField` take no record, so a caller assembling a line out of them
|
|
139
|
+
is outside the check. Pinned in `test/records/type-letter-collision.test.ts`, asserted on what the
|
|
140
|
+
old serializer produced, rebuilt from the shipped `serializeField` so nothing is transcribed twice.
|
|
141
|
+
**No clause is claimed**: LIS02-A2 §5.4/§6.2 stay withheld from CLSI's free sample and the
|
|
142
|
+
paywalled editions were not read; the grounding is this package's own reader.
|
|
143
|
+
|
|
144
|
+
- **A raw `STX`, `ETB` or `ETX` byte in a value was framed as given, and in its worst branch a whole
|
|
145
|
+
result record disappeared into the previous record's text with `warnings: []` at BOTH layers**
|
|
146
|
+
(`ASTM-RAW-ETX-SWALLOWS-A-RECORD`). `PRE-EXISTING`, shipped since the frame encoder did.
|
|
147
|
+
`composeAstmFrames` now throws `AstmFrameEncodeError` with the new code `ASTM_FRAME_RESERVED_BYTE`
|
|
148
|
+
(a third member of `AstmFrameEncodeErrorCode`), carrying `recordIndex` and `characterIndex` and
|
|
149
|
+
never the bytes.
|
|
150
|
+
|
|
151
|
+
**The recorded reason for deferring this said it "fails loudly". That was false, and the
|
|
152
|
+
correction is what re-ranked it.** A frame's text ends at the first `ETB`/`ETX` after its `STX`, so
|
|
153
|
+
an embedded one truncates the frame there, and the two bytes that follow are then read as that
|
|
154
|
+
short frame's checksum. When they match, the truncated frame **verifies**, `decode.ts` resumes at
|
|
155
|
+
`termIndex + 3` and skips the rest of the record as inter-frame bytes, and the next frame number is
|
|
156
|
+
still in sequence. Measured on this package's own round trip: a `C` comment ending `…HEMOLYZED` +
|
|
157
|
+
`ETX` + the two matching characters reassembled without its terminating `CR`, so the following `R`
|
|
158
|
+
merged into the comment's free text (`"SPECIMEN SLIGHTLY HEMOLYZEDR"`) and a `28.6 U/L` result
|
|
159
|
+
vanished, `results()` returning `[]` where the input carried one. **An embedded `ETB` reaches the
|
|
160
|
+
same silence by the other door**, which the defect record did not have: it leaves the record open,
|
|
161
|
+
so the _next_ record's text is appended to the truncated one and two records read back as one, with
|
|
162
|
+
every field of the result hanging off the comment. The precondition is a coincidence in the two
|
|
163
|
+
bytes that follow: they must both read as hex digits and must equal the truncated frame's checksum,
|
|
164
|
+
which is about 1 in 34,600 for uniform-random bytes (1 in 256 once they are hex digits, which is
|
|
165
|
+
where the earlier 1-in-256 figure came from), and trivially constructible on purpose. That bounds
|
|
166
|
+
the likelihood, not the harm.
|
|
167
|
+
Both silent branches, and the loud ones, are pinned in `test/frames/reserved-structure-byte.test.ts`
|
|
168
|
+
asserted on what the old encoder produced, so a weakened guard reds a test rather than passing
|
|
169
|
+
unnoticed.
|
|
170
|
+
|
|
171
|
+
**The refusal has no bytes-instead escape hatch, unlike the `U+00FF` one beside it.** A
|
|
172
|
+
`Uint8Array` is checked too, because the byte is unframable however it arrives: framing has no
|
|
173
|
+
escape sequence for it, and emit has no warning channel, so the only alternatives to refusing are
|
|
174
|
+
substituting a byte the sender did not send or dropping one they did. The three bytes are derived
|
|
175
|
+
from what `decodeAstmFrames` actually reads as structure, not from a control-character class:
|
|
176
|
+
`CR`/`LF` are deliberately absent (they are read only _after_ a frame's checksum, and a record's own
|
|
177
|
+
`CR` sits inside frame text on every stream this encoder writes), and `ENQ`/`ACK`/`NAK`/`EOT` are
|
|
178
|
+
absent too (structure only _between_ frames; measured to round-trip byte-exactly inside one).
|
|
179
|
+
**No clause is claimed:** LIS02-A2 §5.4/§6.2 are withheld from CLSI's free sample and the paywalled
|
|
180
|
+
editions were not read. The grounding is this library's own decoder, in this repo.
|
|
181
|
+
|
|
182
|
+
**The record layer is deliberately UNCHANGED, and that was the cost this slice turned on.**
|
|
183
|
+
Refusing the byte in `serializeAstmRecord` would refuse a byte the caller genuinely supplied, for
|
|
184
|
+
consumers who never frame anything. Measured, in every modeled value: all three
|
|
185
|
+
bytes round-trip through parse → serialize → parse byte for byte, value, units and status intact,
|
|
186
|
+
byte-stable, `warnings: []` on both generations. The byte becomes structure only when a frame is
|
|
187
|
+
built, and `composeAstmFrames` is the total gate on that route, including through
|
|
188
|
+
`serializeFramedAstm`. `CR`/`LF` remain the record layer's own refusal, because they end a _record_.
|
|
189
|
+
|
|
190
|
+
**That claim is about VALUES, and one position on a record line is not a value.** The surplus of a
|
|
191
|
+
header's delimiter declaration drops any control character silently on emit, so a header that
|
|
192
|
+
arrived as `H|\^&` + `ETX` goes back out without it, `warnings: []`, byte-stable, and the
|
|
193
|
+
round-trip is not byte-exact there. `PRE-EXISTING` and deliberately unchanged: that rule is argued
|
|
194
|
+
at `declarationResidual` and long predates the frame-layer refusal, and it is the better of the two
|
|
195
|
+
dispositions now, since carrying the byte through would turn a spec-clean header into a refused
|
|
196
|
+
stream. Pinned in `test/frames/reserved-structure-byte.test.ts` so the scoped sentence stays
|
|
197
|
+
measured. Found by the `conformance-refuter` grading this slice.
|
|
198
|
+
|
|
199
|
+
**What this does not close.** A delimiter colliding with a record's type letter still frames and
|
|
200
|
+
de-frames byte-exactly and re-reads as a different record; framing integrity is not record-layer
|
|
201
|
+
readback. And a caller reaching `composeAstmFrames` from JavaScript with some other typed array is
|
|
202
|
+
still outside the declared signature, and **this guard does not reach that route either**: the
|
|
203
|
+
check compares elements against the three byte values, so a `Uint16Array` element of `0x0103` is
|
|
204
|
+
not one of them and is not refused, while `Uint8Array.from` later takes its low byte and writes an
|
|
205
|
+
`ETX` after all. Measured on all three (`0x0102`/`0x0103`/`0x0117`): framed, then lost at decode
|
|
206
|
+
with a bad checksum or an unterminated frame. That is the same out-of-signature residue already
|
|
207
|
+
recorded for the low-byte truncation, unchanged here in either direction, and stated rather than
|
|
208
|
+
left to be found. Pass a `Uint8Array` or a string.
|
|
209
|
+
|
|
14
210
|
- **A symbolic link under a scan root read CLEAN on BOTH of the PHI scanner's enumerating routes, so
|
|
15
211
|
a link pointing at a file full of real identifiers passed the gate twice over**
|
|
16
212
|
(`PHI-SCAN-SYMLINK-BLIND-ON-BOTH-ROUTES`). Development tooling only: `scripts/phi-scan.ts` ships in
|
package/README.md
CHANGED
|
@@ -39,10 +39,27 @@ reference parser, [`@cosyte/hl7`](https://github.com/cosyte/hl7).
|
|
|
39
39
|
frame them with computed checksums, frame numbers, and the 240-byte split. Both layers round-trip by
|
|
40
40
|
construction, and a delimiter set that fails any of the three conditions readback requires (one
|
|
41
41
|
character per separator, no `CR`/`LF`, no two the same) is a typed error rather than bytes written
|
|
42
|
-
and lost.
|
|
42
|
+
and lost. Those three read the set alone, so each record is additionally checked against the set it
|
|
43
|
+
is written with: a separator equal to a record's own type letter escapes that letter away and the
|
|
44
|
+
record re-reads as a different record, which is `ASTM_EMIT_TYPE_LETTER_COLLISION` rather than
|
|
45
|
+
output. That second check is about **transcoding**, not about which set you asked for, so it can
|
|
46
|
+
fire with no delimiter argument at all: a stream read under a vendor's own set may carry a record
|
|
47
|
+
whose type letter the canonical set escapes away, and `serializeFramedAstm` refuses it for the same
|
|
48
|
+
reason. A frame carries **bytes**, so a record handed to `composeAstmFrames` as a `string` is one
|
|
43
49
|
byte per character and a character above `U+00FF` is a typed error too: the encoder will not pick a
|
|
44
50
|
character encoding for you, and it never quietly writes a different character than the one you gave
|
|
45
|
-
it. Encode such content yourself and pass the `Uint8Array`.
|
|
51
|
+
it. Encode such content yourself and pass the `Uint8Array`. A raw `STX`, `ETB` or `ETX` **byte** in a
|
|
52
|
+
record is a typed error as well, in either form: those three are what the decoder reads as the shape
|
|
53
|
+
of a frame, framing has no escape sequence for them, and writing one through truncated the frame at
|
|
54
|
+
that byte, silently losing a whole record whenever the two bytes after it happened to be the short
|
|
55
|
+
frame's checksum. The **record** layer still carries them, because a returned string is not yet on a
|
|
56
|
+
wire and a raw-transport consumer round-trips such a value exactly. `startFrameNumber` lets you
|
|
57
|
+
compose one transfer across several calls, and `composeAstmFrames` checks it before it reads a
|
|
58
|
+
record: a frame's number is a single ASCII digit, so a value that is not a whole number from `0` to
|
|
59
|
+
`7` is a typed error rather than whatever byte the arithmetic truncated to. The round trip above is
|
|
60
|
+
the **default** start; a non-default one writes a continuation of a sequence already in progress,
|
|
61
|
+
and a continuation read on its own opens on a frame-sequence gap, so its first record is warned
|
|
62
|
+
about and not emitted.
|
|
46
63
|
- **Vendor profiles.** `defineAstmProfile()` builds a provenance-backed profile whose tolerances
|
|
47
64
|
downgrade _expected_, non-safety-critical deviations to a `PROFILE_QUIRK_APPLIED` warning without
|
|
48
65
|
ever altering a value, behind a safety gate that refuses to tolerate any result value, flag,
|
package/dist/index.cjs
CHANGED
|
@@ -1387,7 +1387,8 @@ var AstmSerializeError = class extends Error {
|
|
|
1387
1387
|
/**
|
|
1388
1388
|
* Stable discriminant. `ASTM_EMIT_UNENCODABLE_VALUE` for a `CR`/`LF` in a value;
|
|
1389
1389
|
* `ASTM_EMIT_INVALID_DELIMITERS` for a delimiter set that cannot be emitted
|
|
1390
|
-
* reversibly
|
|
1390
|
+
* reversibly; `ASTM_EMIT_TYPE_LETTER_COLLISION` for a record whose own type
|
|
1391
|
+
* letter the set being emitted with would escape away.
|
|
1391
1392
|
*/
|
|
1392
1393
|
code;
|
|
1393
1394
|
/** 0-based ordinal of the record within the message, when known. */
|
|
@@ -1451,10 +1452,24 @@ function serializeAstmRecord(record, d = CANONICAL_DELIMITERS) {
|
|
|
1451
1452
|
return serializeRecordChecked(record, d);
|
|
1452
1453
|
}
|
|
1453
1454
|
function serializeRecordChecked(record, d) {
|
|
1455
|
+
return assertTypeLetterSurvives(serializeRecordText(record, d), record);
|
|
1456
|
+
}
|
|
1457
|
+
function serializeRecordText(record, d) {
|
|
1454
1458
|
if (record.type === "M" || record.type === "S") return serializeVerbatimRecord(record, d);
|
|
1455
1459
|
if (record.type === "H") return serializeHeader(record, d);
|
|
1456
1460
|
return record.fields.map((f) => encodeField(f.repeats, d, record.recordIndex)).join(d.field);
|
|
1457
1461
|
}
|
|
1462
|
+
function recordTypeLetter(record) {
|
|
1463
|
+
return record.type === "unsupported" ? record.rawType : record.type;
|
|
1464
|
+
}
|
|
1465
|
+
function assertTypeLetterSurvives(text, record) {
|
|
1466
|
+
if (text.charAt(0) === recordTypeLetter(record)) return text;
|
|
1467
|
+
throw new AstmSerializeError(
|
|
1468
|
+
"A record's type letter is escaped away by the delimiters being emitted with, so the record would read back as a different record. The record's index is on the error; the letter itself is not quoted, because on an unsupported record it is a byte off the wire.",
|
|
1469
|
+
record.recordIndex,
|
|
1470
|
+
"ASTM_EMIT_TYPE_LETTER_COLLISION"
|
|
1471
|
+
);
|
|
1472
|
+
}
|
|
1458
1473
|
function serializeVerbatimRecord(record, d) {
|
|
1459
1474
|
if (recoversVerbatim(record, d)) return record.rawLine;
|
|
1460
1475
|
return record.fields.map((f) => encodeField(f.repeats, d, record.recordIndex)).join(d.field);
|
|
@@ -2229,7 +2244,9 @@ var AstmFrameEncodeError = class extends Error {
|
|
|
2229
2244
|
/**
|
|
2230
2245
|
* Position of the offending character within that record, when applicable:
|
|
2231
2246
|
* its index in the string, never the character itself and never its code
|
|
2232
|
-
* point. Enough to find it in the caller's own data.
|
|
2247
|
+
* point. Enough to find it in the caller's own data. For a record supplied as
|
|
2248
|
+
* a `Uint8Array` this is the offending byte's index, which is the same
|
|
2249
|
+
* position: a record string is a byte string, so the two indices coincide.
|
|
2233
2250
|
*/
|
|
2234
2251
|
characterIndex;
|
|
2235
2252
|
/** @internal */
|
|
@@ -2259,6 +2276,29 @@ function toBytes(input, recordIndex) {
|
|
|
2259
2276
|
}
|
|
2260
2277
|
return out;
|
|
2261
2278
|
}
|
|
2279
|
+
var RESERVED_STRUCTURE_BYTES = /* @__PURE__ */ new Set([STX, ETB, ETX]);
|
|
2280
|
+
function assertFramable(bytes, recordIndex) {
|
|
2281
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
2282
|
+
const byte = bytes[i];
|
|
2283
|
+
if (byte !== void 0 && RESERVED_STRUCTURE_BYTES.has(byte)) {
|
|
2284
|
+
throw new AstmFrameEncodeError(
|
|
2285
|
+
"A record contains a byte the framing layer reads as structure (STX, ETB or ETX). A frame has no escape sequence for one, so it cannot be carried inside a frame: remove it from the record before framing.",
|
|
2286
|
+
recordIndex,
|
|
2287
|
+
"ASTM_FRAME_RESERVED_BYTE",
|
|
2288
|
+
i
|
|
2289
|
+
);
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
function assertStartFrameNumber(value) {
|
|
2294
|
+
if (!Number.isInteger(value) || value < 0 || value > FRAME_NUMBER_MODULUS - 1) {
|
|
2295
|
+
throw new AstmFrameEncodeError(
|
|
2296
|
+
`startFrameNumber must be a whole number from 0 to 7: a frame's number is one ASCII digit and the sequence is modulo 8. Received: ${String(value)}.`,
|
|
2297
|
+
void 0,
|
|
2298
|
+
"ASTM_FRAME_INVALID_START_FRAME_NUMBER"
|
|
2299
|
+
);
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2262
2302
|
function encodeFrame(text, frameNumber, isFinal) {
|
|
2263
2303
|
const term = isFinal ? ETX : ETB;
|
|
2264
2304
|
const bytes = [STX, FN_ZERO + frameNumber, ...text, term];
|
|
@@ -2267,11 +2307,13 @@ function encodeFrame(text, frameNumber, isFinal) {
|
|
|
2267
2307
|
return [...bytes, hex.charCodeAt(0), hex.charCodeAt(1), CR, LF];
|
|
2268
2308
|
}
|
|
2269
2309
|
function composeAstmFrames(records, options = {}) {
|
|
2310
|
+
const start = options.startFrameNumber ?? FIRST_FRAME_NUMBER;
|
|
2311
|
+
assertStartFrameNumber(start);
|
|
2270
2312
|
if (records.length === 0) {
|
|
2271
2313
|
throw new AstmFrameEncodeError("Cannot frame an empty record list: nothing to transmit.");
|
|
2272
2314
|
}
|
|
2273
2315
|
const out = [];
|
|
2274
|
-
let frameNumber =
|
|
2316
|
+
let frameNumber = start;
|
|
2275
2317
|
records.forEach((record, recordIndex) => {
|
|
2276
2318
|
const bytes = toBytes(record, recordIndex);
|
|
2277
2319
|
if (bytes.length === 0) {
|
|
@@ -2280,6 +2322,7 @@ function composeAstmFrames(records, options = {}) {
|
|
|
2280
2322
|
recordIndex
|
|
2281
2323
|
);
|
|
2282
2324
|
}
|
|
2325
|
+
assertFramable(bytes, recordIndex);
|
|
2283
2326
|
for (let offset = 0; offset < bytes.length; offset += MAX_FRAME_TEXT) {
|
|
2284
2327
|
const chunk = bytes.subarray(offset, offset + MAX_FRAME_TEXT);
|
|
2285
2328
|
const isFinal = offset + MAX_FRAME_TEXT >= bytes.length;
|
|
@@ -2315,7 +2358,7 @@ function concatBytes3(chunks) {
|
|
|
2315
2358
|
}
|
|
2316
2359
|
|
|
2317
2360
|
// src/index.ts
|
|
2318
|
-
var VERSION = "0.0.
|
|
2361
|
+
var VERSION = "0.0.14";
|
|
2319
2362
|
|
|
2320
2363
|
exports.ALL_ASTM_WARNING_CODES = ALL_ASTM_WARNING_CODES;
|
|
2321
2364
|
exports.AMBIGUOUS_CODES = AMBIGUOUS_CODES;
|