@cosyte/dicom 0.0.13 → 0.0.15
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 +458 -0
- package/README.md +6 -4
- package/dist/index.cjs +120 -90
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +126 -28
- package/dist/index.d.ts +126 -28
- package/dist/index.mjs +120 -90
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,463 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.0.15
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 9273a9d: Two Tier-3 fatal messages stop rendering the bytes left in the buffer, because in a Sequence Item
|
|
8
|
+
that count is the Item's own declared length (`DICOM-DIAGNOSTIC-PHI-RESIDUALS`)
|
|
9
|
+
|
|
10
|
+
The ninth instance of "a diagnostic about a PHI leak is itself a PHI surface", and the first whose
|
|
11
|
+
shift is not a constant.
|
|
12
|
+
|
|
13
|
+
WHAT SHIPS. Through `0.0.14` `ELEMENT_LENGTH_EXCEEDS_BUFFER` and `FILE_META_GROUP_LENGTH_OVERRUNS`
|
|
14
|
+
each filled an `{n}` slot from `buffer.length - cursor.position`. Both slots are gone, and so are the
|
|
15
|
+
parameters: `elementLengthExceedsBuffer(buffer, offset)` and `fileMetaGroupLengthOverruns(buffer,
|
|
16
|
+
offset)` take two arguments now, so no call site can put either number back. `{n}` still exists in the
|
|
17
|
+
registry and is filled by exactly two entries whose number nobody on the wire chose:
|
|
18
|
+
`SQ_NESTING_DEPTH_EXCEEDED` renders this library's own `NESTING_DEPTH_LIMIT`, and
|
|
19
|
+
`INFLATED_PAYLOAD_EXCEEDS_CAP` renders the cap the caller passed in. No parse moves, no file that
|
|
20
|
+
threw stops throwing, `err.code` is unchanged everywhere and `err.byteOffset` still locates the
|
|
21
|
+
element.
|
|
22
|
+
|
|
23
|
+
BREAKING FOR STRING-MATCHERS: the two message texts change. A consumer matching on either string
|
|
24
|
+
stops matching. Neither code changed.
|
|
25
|
+
|
|
26
|
+
WHY A COUNT BOUNDED BY BYTES PRESENT IS STILL THE SENDER'S NUMBER. The old defence was that
|
|
27
|
+
`buffer.length - cursor.position` cannot exceed the input, which bounds the number's MAGNITUDE and
|
|
28
|
+
says nothing about its CONTENT. `parseSequence` parses a defined-length Item from a SLICE, so inside
|
|
29
|
+
one the buffer IS that Item and `buffer.length` IS the Item's 32-bit Value (Item) Length off its own
|
|
30
|
+
header. The message publishes `byteOffset` beside the count and `cursor.position` is that offset plus
|
|
31
|
+
the header just read, so an addition returns the declared length. A raw 32-bit length field a sender
|
|
32
|
+
wrote is already the class this registry refuses everywhere else, and shifting one by an amount the
|
|
33
|
+
reader can compute does not change its class.
|
|
34
|
+
|
|
35
|
+
MEASURED, WITH THE DECLARED LENGTH READ BACK OFF THE WIRE RATHER THAN ASSERTED AGAINST A LITERAL. A
|
|
36
|
+
synthetic file declaring an Item Length of 21320: the message read 21312 with the over-declaring
|
|
37
|
+
element first in the Item, 21288 behind one 24-byte element, and 21272 behind one 40-byte element.
|
|
38
|
+
THE SHIFT IS `cursor.position`, SO IT IS VARIABLE, which is why the remedy is the factory signature
|
|
39
|
+
rather than a filter: a filter would have to know the frame, and the factory cannot.
|
|
40
|
+
|
|
41
|
+
WHAT THE DETECTOR CAN AND CANNOT SEE HERE, AND IT IS THE HALF THAT TRANSFERS. `#92` added a
|
|
42
|
+
`length-less-item-header` arm to the re-encoding detector in
|
|
43
|
+
`test/integration/fatal-diagnostic-surface.test.ts` that subtracts exactly 8, and said on the constant
|
|
44
|
+
that it covers ONE offset. Pointed at this leak it behaves exactly as documented: it returns the 21312
|
|
45
|
+
shape and reads CLEAN on 21288 and 21272, which are the same leak on the same fixture. Both results
|
|
46
|
+
are pinned as rows, beside a DIRECT render of 21320 that the `length` arm does catch, so the clean
|
|
47
|
+
results are the arms' limit rather than a payload carrying nothing. A ZERO FROM THIS DETECTOR IS A
|
|
48
|
+
GAP, NOT A CLEARANCE. The arm is still not widened to a range, for the reason `#92` gave: a hunt with
|
|
49
|
+
nothing to hunt has no non-vacuity control, and what clears the class is the signature.
|
|
50
|
+
|
|
51
|
+
THE COST IS STATED RATHER THAN GLOSSED. `FILE_META_GROUP_LENGTH_OVERRUNS` is raised at the root and
|
|
52
|
+
nowhere else, where `buffer.length` is the caller's own input and the count leaked nothing. It loses
|
|
53
|
+
the number anyway. A bound that holds only because of where a function happens to be called from is
|
|
54
|
+
not a bound, and the sibling that shares the expression is raised inside a slice, so leaving one slot
|
|
55
|
+
open is exactly the shape `#88` measured relocating a leak onto a sibling rendering the identical
|
|
56
|
+
fixture. Both diagnostics are correspondingly less informative: neither says how far the read got.
|
|
57
|
+
|
|
58
|
+
NOT IN SCOPE, AND NAMED SO IT IS NOT READ AS CLOSED. `report.removedPrivateTags`,
|
|
59
|
+
`report.unauditableSequences[].tag`, `uidMap`, `contextPath` and the two `byteLength` fields are model
|
|
60
|
+
fields on a type whose own docs say it is not a value-free surface, not messages, and a bound empties
|
|
61
|
+
them on every well-formed file. `DicomParseError.snippet` is still 16 raw source bytes and is still
|
|
62
|
+
documented as PHI. `ds.warnings[].message` is still NOT unconditionally safe to log.
|
|
63
|
+
|
|
64
|
+
- 2cf2ab0: `DICOM_ITEM_CROSSES_SEQUENCE_END` stops printing how many bytes remained inside the sequence, because
|
|
65
|
+
that count was the sequence's own declared length shifted by an amount the reader can compute
|
|
66
|
+
(`DICOM-DIAGNOSTIC-PHI-RESIDUALS`)
|
|
67
|
+
|
|
68
|
+
The eighth instance of "a diagnostic about a PHI leak is itself a PHI surface". Its defence and its
|
|
69
|
+
pinning test were both green, and both were green BY FIXTURE, which is the more valuable half.
|
|
70
|
+
|
|
71
|
+
WHAT SHIPS. Through `0.0.14` the message read "`{n2}` bytes remained inside the sequence", filled from
|
|
72
|
+
`Math.max(0, endLimit - cursor.position)`. `endLimit` is `valueStart + explicitLength`, the enclosing
|
|
73
|
+
SEQUENCE's declared Value Length read off its own header, and `cursor.position` is the crossing item's
|
|
74
|
+
header start plus 8, so the rendered count IS that declared length less the bytes of the sequence
|
|
75
|
+
already consumed. An addition the reader can compute reverses it: on the sequence's FIRST item the
|
|
76
|
+
shift is just the 8-byte Item header (PS3.5 2026c section 7.1.1's 4-byte tag plus section 7.5.1's
|
|
77
|
+
4-byte Item Length), and with one 24-byte item ahead of it the same fixture renders 20275 against the
|
|
78
|
+
same declared 20307, a shift of 32. Both shapes are pinned; "less 8" as a universal was refused by a
|
|
79
|
+
graded pass. `itemCrossesSequenceEnd` takes
|
|
80
|
+
`(position, tag)` now; the Item's own declared length was already bound out of that signature, and
|
|
81
|
+
this is the second number to go the same way. The tag slot still carries the constant `FFFEE000`, and
|
|
82
|
+
`position.byteOffset` still locates the item. Which codes fire is unchanged and no parse moves.
|
|
83
|
+
|
|
84
|
+
A RAW WIRE NUMBER SHIFTED BY A CONSTANT THE READER CAN COMPUTE IS THE RAW WIRE NUMBER. The registry's
|
|
85
|
+
own rule allowed a number this parser "derived - a count it kept, an offset it counted, a remainder
|
|
86
|
+
the buffer bounds". The third clause is deleted rather than reworded: the `endLimit < buffer.length`
|
|
87
|
+
conjunct at the emit site bounds the rendered number's MAGNITUDE and says nothing about its CONTENT,
|
|
88
|
+
and that reading is what admitted this leak for four releases.
|
|
89
|
+
|
|
90
|
+
GREEN BY FIXTURE, TWICE, ON THE SAME PAYLOAD CLASS. The comment and the pinning row both rested on
|
|
91
|
+
that conjunct, and the fixture behind them only ever fabricated the sequence length out of four
|
|
92
|
+
PRINTABLE bytes. Every such window exceeds 538,976,288, so `endLimit` landed past the buffer and the
|
|
93
|
+
conjunct refused - but a length that big is unreachable by construction, because the buffer has to
|
|
94
|
+
hold that many bytes for the parse to get there. The reachable class has zero high-order bytes and
|
|
95
|
+
therefore a SHORT decimal. A TEST THAT PASSES BECAUSE ITS FIXTURE CANNOT REACH THE FAILING CASE IS
|
|
96
|
+
NOT EVIDENCE. The row is kept, because what it measures is true; what it was read as concluding is
|
|
97
|
+
retracted.
|
|
98
|
+
|
|
99
|
+
AND THE DETECTOR READ CLEAN ON IT, WHICH IS THE GAP THIS RELEASE ALSO CLOSES. Every arm of the
|
|
100
|
+
re-encoding detector in `test/integration/fatal-diagnostic-surface.test.ts` hunted a rendering EQUAL
|
|
101
|
+
to a typed read of a payload window, so a rendering SHIFTED by a constant was invisible to all of
|
|
102
|
+
them even with the digit floor removed. Measured on the `"SO\0\0"` payload that file already carries:
|
|
103
|
+
the shipped template returned no findings under the whole detector, while a DIRECT render of the same
|
|
104
|
+
length returned the `length` hit, so the detector was working and the miss was structural. A
|
|
105
|
+
`length-less-item-header` arm returns `20299 == "SO\0\0"`. THAT ARM COVERS ONE OFFSET AND IS NOT A
|
|
106
|
+
GENERAL NET, which is stated on the constant rather than left to be discovered: it hunts the first-item
|
|
107
|
+
shift of 8, so on the 32-byte shape above it would still have read clean. It is not widened to
|
|
108
|
+
a range, for the same reason the missing 2-byte-as-`uint16` arm beside it is still named rather than
|
|
109
|
+
armed: a hunt with nothing to hunt has no non-vacuity control, and what clears this class is the
|
|
110
|
+
factory signature, not the arm. A GUARD WITH A FLOOR HAS NOT CLEARED ANYTHING BELOW THE FLOOR, AND A
|
|
111
|
+
DETECTOR WITH NO OFFSET ARM HAS NOT CLEARED A SHIFTED RENDERING.
|
|
112
|
+
|
|
113
|
+
THIS ONE REACHES `ds.warnings` ON A SURVIVING PARSE, unlike the three instances before it, which
|
|
114
|
+
reached `onWarning` on a file the parse then refused or `report.warnings` on the de-identify channel.
|
|
115
|
+
So it lands on the channel a consumer is most likely to log. Measured on a synthetic, name-bearing
|
|
116
|
+
fixture whose planted letters are read back OFF THE WIRE rather than asserted against a literal:
|
|
117
|
+
`"SO"` rendered 20299 of a declared 20307, `"ON"` 20039 of 20047, `"TH"` 18508 of 18516, and the two
|
|
118
|
+
low bytes of each declared length are two letters of the planted surname.
|
|
119
|
+
|
|
120
|
+
WHAT IT COSTS, STATED RATHER THAN MINIMISED. A consumer reading the message no longer learns how much
|
|
121
|
+
of the sequence was left when the item over-ran it. Unlike the two `deidentify()` codes closed
|
|
122
|
+
alongside it, this number has no model field, so nothing publishes it any more; the parsed sequence
|
|
123
|
+
and `position.byteOffset` are what remain. The warning still says the file's two length fields
|
|
124
|
+
disagree, which is the disclosure it exists for.
|
|
125
|
+
|
|
126
|
+
THE EXCEPTION LIST STOPS BEING COPIED, AND NO COUNT OF THE COPIES IS QUOTED. "Which numeric slots are
|
|
127
|
+
exempt from the signature bound" is stated where the strings are - the `WARNING_MESSAGES` docblock in
|
|
128
|
+
`src/parser/warnings.ts` - and `README.md`, `docs-content/limitations.md`,
|
|
129
|
+
`docs-content/troubleshooting.md`, `docs-content/spec-notes-tolerance.md` and `ParseOptions.strict`'s
|
|
130
|
+
JSDoc name that docblock instead of carrying their own. A numeral is deliberately absent: this lineage
|
|
131
|
+
has already corrected such a count twice, and the rule it wrote is that a count corrected twice is
|
|
132
|
+
deleted rather than incremented. The pending `dicom-membership-render-tag.md` keeps its own statement,
|
|
133
|
+
which is not false; only the single clause this collapse falsified was deleted from it, and
|
|
134
|
+
`documentation/agent-notes.md` still argues the same exception in full, which is the record it exists
|
|
135
|
+
to be. Name the sink, do not restate its cost.
|
|
136
|
+
|
|
137
|
+
STILL A PRODUCT CALL AND UNTOUCHED: `report.removedPrivateTags`, `report.unauditableSequences[].tag`,
|
|
138
|
+
`report.uidMap`, `contextPath` and the two `byteLength` fields are model fields, not messages. A bound
|
|
139
|
+
on any of them empties the field on every well-formed file.
|
|
140
|
+
|
|
141
|
+
FIGURES. Base `ce33ec4`. Head, whole suite: 73 files, 1,223 passing + 1 todo, 0 red. Head tests
|
|
142
|
+
against base `src/` (replaced by file copy, not overlaid): 7 of 1,224 red across 3 files. Split before
|
|
143
|
+
quoting. FOUR are behavioural, because base really put the number in a live message: the two closure
|
|
144
|
+
pins, whose digit runs on base contain `20299` and `20275` respectively; the row asserting the message
|
|
145
|
+
carries neither length, which on base contains the remaining `16`; and the printable-class row, whose
|
|
146
|
+
live control message on base differs from the frozen template. TWO assert the registry template has no
|
|
147
|
+
`{n2}` slot. ONE is the factory-arity row, which fails on its arity line before reaching any message
|
|
148
|
+
assertion and is not evidence that base leaked. Re-measured after the graded pass, which is what added
|
|
149
|
+
the multi-item row.
|
|
150
|
+
|
|
151
|
+
- b8a3fb5: A tag in a warning message is checked for membership in PS3.6's element registry now, and the
|
|
152
|
+
parser's own codes no longer render a raw number read off an element header. This closes the fifth
|
|
153
|
+
and sixth instances of "a diagnostic about a PHI leak is itself a PHI surface".
|
|
154
|
+
|
|
155
|
+
**⚠ BREAKING FOR STRING-MATCHERS, AND FOR ANYONE WHO PARSES A TAG BACK OUT OF A MESSAGE.**
|
|
156
|
+
`w.code` is unchanged everywhere, which codes fire is unchanged, and no parse moves. What changes is
|
|
157
|
+
the prose of six registry entries and what two of the slots contain.
|
|
158
|
+
|
|
159
|
+
**`renderTag` is a MEMBERSHIP test.** It renders a tag only when PS3.6's element registry carries a
|
|
160
|
+
**literal row** for it, and `<withheld>` otherwise. Through `0.0.14` it validated a tag's shape, and
|
|
161
|
+
a shape test admits all 2^32 tags, so it could not refuse a tag a lying Value Length composed out of
|
|
162
|
+
somebody's value. Measured on a synthetic, name-bearing payload: an `ST` carrying
|
|
163
|
+
`"MR BRAIN SMITHSON "` whose Value Length under-declares by 12 desynchronizes the Explicit VR LE
|
|
164
|
+
reader onto a fabricated header whose declared length is odd, and `DICOM_ODD_LENGTH_VALUE_PADDED`
|
|
165
|
+
rendered four bytes of the name as its tag (`4E495320`, `"IN S"` in wire order) **and four more as
|
|
166
|
+
its decimal length** - eight consecutive payload bytes in one message, each reversible with one typed
|
|
167
|
+
read. `renderVr` bounds two bytes against the 34 VRs PS3.5 2026c section 6.2 defines; this is the
|
|
168
|
+
same trade against a set of 5,221.
|
|
169
|
+
|
|
170
|
+
**A repeating-group family row does not count as membership, and that distinction is load-bearing.**
|
|
171
|
+
`(50xx,xxxx)` Curve Data leaves the whole 16-bit element number free, so a family test would admit
|
|
172
|
+
16 x 65,536 tags whose free bits are raw document bytes: `"\fPAR"` composes `500C5241` and returns
|
|
173
|
+
all four payload bytes with one typed read. Only tags the registry names one at a time are rendered.
|
|
174
|
+
|
|
175
|
+
**A raw wire number is bound out of the factory signature on the parser's codes, because there is
|
|
176
|
+
nothing to check.** A
|
|
177
|
+
declared Value Length has neither a shape nor a membership a renderer could test, so there is no
|
|
178
|
+
`renderLength` and there must not be one. `DICOM_ODD_LENGTH_VALUE_PADDED` no longer prints the odd
|
|
179
|
+
length. `DICOM_NONZERO_RESERVED_BYTES` no longer prints its two reserved bytes: that code already
|
|
180
|
+
withheld its **tag** on the reasoning that its trigger is "this header may not be a header", and it
|
|
181
|
+
then printed two bytes off the same header as decimals. Measured on the same payload, six
|
|
182
|
+
under-declare deltas each put two letters of the name into that message. **No detector in this
|
|
183
|
+
package had ever hunted a single byte rendered as a decimal**, which is why this instance had not
|
|
184
|
+
been filed. `DICOM_PIXEL_DATA_LENGTH_MISMATCH` loses its declared length too, although this build has
|
|
185
|
+
no call site for it: with none, the change costs nothing now and no later measurement would catch it
|
|
186
|
+
once a phase switches the code on. `DICOM_GROUP_LENGTH_IN_DATASET` loses its tag for a third reason,
|
|
187
|
+
which the membership rule produced rather than a judgement: PS3.6 carries exactly one literal row
|
|
188
|
+
ending `0000`, and it is File Meta, so that slot could never have rendered anything but `<withheld>`.
|
|
189
|
+
|
|
190
|
+
**The exceptions are named rather than counted, and rather than left as an unstated absolute.**
|
|
191
|
+
`(0002,0000)`'s own declared
|
|
192
|
+
File Meta group length is still printed, and that one is argued as well as measured: `parseFileMeta`
|
|
193
|
+
runs once per parse, from `parseDicom`, at the post-`DICM` offset, and is never nested, so those four
|
|
194
|
+
bytes are that attribute's own Value Field at a structurally determined offset that no Data Set value
|
|
195
|
+
can be read into. The desynchronized-read sweep reaches that code zero times.
|
|
196
|
+
|
|
197
|
+
**What it costs you, stated rather than minimised.** On any file, well-formed or not, a message about
|
|
198
|
+
a **private** element, a **Group Length** `(gggg,0000)`, or a **repeating-group member** such as
|
|
199
|
+
`(6000,3000)` Overlay Data no longer names its tag. The element is still in the Data Set under that
|
|
200
|
+
tag, and `position.byteOffset` locates the header, with the frame-of-reference caveat every offset in
|
|
201
|
+
this package carries. `DICOM_VR_MISMATCH` is unaffected by construction: it fires only where the
|
|
202
|
+
dictionary already has an entry for the tag.
|
|
203
|
+
|
|
204
|
+
**The "safe to log" sentence is deleted rather than reworded a third time**, in `README.md`,
|
|
205
|
+
`docs-content/limitations.md`, `docs-content/troubleshooting.md`,
|
|
206
|
+
`docs-content/spec-notes-tolerance.md`, `docs-content/cookbook.md` and `ParseOptions.strict`'s JSDoc.
|
|
207
|
+
It was written as "safe to log whole on any well-formed file", then corrected to "safe on a
|
|
208
|
+
well-formed file and not unconditionally safe", and this package deletes a disclosure it has reworded
|
|
209
|
+
twice. Every carrier now states the mechanism - which slot is a membership test, which is a signature
|
|
210
|
+
bound - and states no verdict.
|
|
211
|
+
|
|
212
|
+
**Not taken, deliberately.** `report.removedPrivateTags` is untouched: it is a private-tag field by
|
|
213
|
+
definition, so no closed table can ever vouch for its contents and a bound would empty it on every
|
|
214
|
+
well-formed file. That is a product call rather than a defect. `report.unauditableSequences[].tag`,
|
|
215
|
+
`report.uidMap` and `contextPath` are model fields rather than messages and are equally untouched.
|
|
216
|
+
A `DicomParseError` still carries `snippet`, 16 raw source bytes as hex.
|
|
217
|
+
|
|
218
|
+
- ce33ec4: The two `deidentify()` diagnostics stop rendering a raw wire length, and the PHI detector's digit
|
|
219
|
+
floor that hid it is gone (`DICOM-DIAGNOSTIC-PHI-RESIDUALS`)
|
|
220
|
+
|
|
221
|
+
The seventh instance of "a diagnostic about a PHI leak is itself a PHI surface", plus the tripwire gap
|
|
222
|
+
that made it invisible. The gap is the more valuable half.
|
|
223
|
+
|
|
224
|
+
THE DETECTOR WAS WIDENED BEFORE ANY CODE WAS TOUCHED. The `length` arm of the re-encoding detector in
|
|
225
|
+
`test/integration/fatal-diagnostic-surface.test.ts` skipped any rendering under seven digits, and the
|
|
226
|
+
sentence beside it stated the defect without seeing it: "every 4-byte window of a printable-ASCII
|
|
227
|
+
payload exceeds 1,000,000,000, so nothing in this fixture set is skipped by the floor." That is true
|
|
228
|
+
and it is the whole problem. A declared Value Length is only reachable through a parse if the buffer
|
|
229
|
+
really holds that many bytes, so every fabricated length a fixture can drive through this library has
|
|
230
|
+
zero high-order bytes and therefore a SHORT decimal. `"SO\0\0"` renders `20307`: five digits, two of
|
|
231
|
+
them letters of a surname, structurally under the floor. The floor was not a conservative filter on an
|
|
232
|
+
arm that worked. It excluded the entire class of length leak that can actually happen. A GUARD WITH A
|
|
233
|
+
FLOOR HAS NOT CLEARED ANYTHING BELOW THE FLOOR.
|
|
234
|
+
|
|
235
|
+
The floor is removed and the collision it was for is answered by matching instead of skipping, so the
|
|
236
|
+
widening is strictly additive: a rendering of seven digits or more keeps the original substring
|
|
237
|
+
search, and a shorter one must equal a whole maximal digit run of the message.
|
|
238
|
+
|
|
239
|
+
WHAT THE WIDENED DETECTOR FOUND, POINTED AT THE `deidentify()` CHANNEL. That channel is the one the
|
|
240
|
+
standing desync sweep states it can never reach, because both codes are emitted by `deidentify()` and
|
|
241
|
+
by nothing else while every desync fixture dies at a Tier-3 fatal first. It returned the two `{n}`
|
|
242
|
+
slots this release closes and nothing else. It also returned `00080008`, a literal PS3.6 row that
|
|
243
|
+
`renderTag`'s membership test renders on every file by design, which is scoped out of the sweep
|
|
244
|
+
explicitly rather than left to pass quietly.
|
|
245
|
+
|
|
246
|
+
THE REMEDY IS THE FACTORY SIGNATURE, AS IT WAS THE LAST FIVE TIMES.
|
|
247
|
+
`DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE` and `DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE` rendered `{n}`
|
|
248
|
+
from `Element.rawBytes.length`, which is not a count this parser invented: it EQUALS the declared
|
|
249
|
+
Value Length off the element header. `sequenceNotAuditable` now takes `(position, tag)` and
|
|
250
|
+
`undefinedVrNotAuditable` takes `(position)`. A raw length has neither a shape nor a membership for a
|
|
251
|
+
renderer to test, so there is no `renderLength` and there must not be one. The second code is the
|
|
252
|
+
sharper case: it withheld its tag and its VR on the stated ground that the header may be fabricated,
|
|
253
|
+
then printed the length off that same header.
|
|
254
|
+
|
|
255
|
+
WHAT IT COSTS, STATED RATHER THAN MINIMISED. A consumer reading only the message no longer sees how
|
|
256
|
+
many bytes were emptied. The number is still on `report.unauditableSequences[].byteLength` and
|
|
257
|
+
`report.undefinedVrElements[].byteLength`, and `{tag}` and the byte offset still locate the element.
|
|
258
|
+
Those two model fields JOINED the `DeidentifyReport` not-value-free list rather than always having
|
|
259
|
+
been on it: binding the message left them as the number's only publisher, which is a smaller surface
|
|
260
|
+
and not a closed one.
|
|
261
|
+
|
|
262
|
+
STILL A PRODUCT CALL AND UNTOUCHED: `report.removedPrivateTags`, `report.unauditableSequences[].tag`,
|
|
263
|
+
`report.uidMap`, `contextPath` and the two `byteLength` fields are model fields, not messages. A bound
|
|
264
|
+
on any of them empties the field on every well-formed file, where the content is exactly the audit
|
|
265
|
+
information it exists to carry.
|
|
266
|
+
|
|
267
|
+
A MIS-TITLED ROW IS CORRECTED. `deident-unauditable-sequence.test.ts` carried a row titled "carrying
|
|
268
|
+
no value" for the very code disclosed as printing a header-derived length; its body only asserted that
|
|
269
|
+
values planted elsewhere were absent, which a message built from a frozen registry cannot carry in any
|
|
270
|
+
case. The title now matches what the code does and the row asserts the number's absence, with a
|
|
271
|
+
non-vacuity control that rebuilds the shipped template.
|
|
272
|
+
|
|
273
|
+
AND ONE MORE FIXTURE THAT HAD NEVER RUN. The standing desync sweep built its fixtures inside the `try`
|
|
274
|
+
that swallows the parse failures they are designed to end in, so the `-20` delta, which under-declares
|
|
275
|
+
an 18-byte payload past zero, threw during construction on both syntaxes and was counted as swept.
|
|
276
|
+
Construction moved outside the `try`.
|
|
277
|
+
|
|
278
|
+
FIGURES. Base `b8a3fb5`. Head, whole suite: 73 files, 1,220 passing + 1 todo, 0 red. Head tests
|
|
279
|
+
against base `src/` (replaced, not overlaid): 5 of 1,221 red across 3 files. THREE are behavioural;
|
|
280
|
+
TWO are the factory-arity rows, which fail on the arity line and whose message assertions would have
|
|
281
|
+
passed on base, so they grade the new bound and are not evidence that base leaked.
|
|
282
|
+
|
|
283
|
+
## 0.0.14
|
|
284
|
+
|
|
285
|
+
### Patch Changes
|
|
286
|
+
|
|
287
|
+
- 0c53142: Test infrastructure: the generator suites prove their pins in a sandbox, so nothing mutates the
|
|
288
|
+
vendored standards while another worker is reading them.
|
|
289
|
+
|
|
290
|
+
**The pin is not relaxed. The mutation is relocated.** Both generator suites prove their vendored
|
|
291
|
+
DocBook pins are preconditions rather than comments, and the only way to prove that is to defeat one:
|
|
292
|
+
repoint `vendor/nema/<part>/SHA.txt` at a mutant document, and, for the check that re-hashes the
|
|
293
|
+
CONTENT rather than the pointer, overwrite the bytes at the pinned path itself. Vitest runs test files
|
|
294
|
+
in parallel, so for as long as a mutation was live every other worker saw it. The documentation
|
|
295
|
+
citation gate re-hashes PS3.5, PS3.6 and PS3.15 at **module load**, which made it a fresh concurrent
|
|
296
|
+
reader of two of the parts those suites mutate, and a reader that throws at module load takes its
|
|
297
|
+
whole file down rather than one case. (Two, not all of them: the gate does not read `part05-2004`,
|
|
298
|
+
which the repeating-groups suite mutates, and nothing mutates `part06`, which the gate does read.)
|
|
299
|
+
The generators now run against a `mkdtemp` copy of `scripts/`, `src/` and `vendor/`, so there is
|
|
300
|
+
nothing for a concurrent reader to observe.
|
|
301
|
+
|
|
302
|
+
**Neither generator took a new input, which is what makes this affordable.** Both resolve their own
|
|
303
|
+
repository root from `import.meta.url`, so relocating the script relocates the document it reads and
|
|
304
|
+
the artifact it writes. The byte-identical regen gate keeps depending on a script with no vendor-root
|
|
305
|
+
argument and no output-path argument; the new `root` option is on the test helper that spawns them,
|
|
306
|
+
not on the scripts. The artifact comparison still reads the **committed**
|
|
307
|
+
`src/dictionary/generated/` file as its baseline, so "regenerates what is committed" still means that.
|
|
308
|
+
|
|
309
|
+
**The window was measured, and so was the fix.** A probe running the citation gate's `readPinned` in
|
|
310
|
+
a loop over all four vendored part directories, against ten runs of the two generator suites, logged
|
|
311
|
+
1,542 read cycles and 945 anomalous observations in four classes: `SHA.txt` holding a non-hash,
|
|
312
|
+
`SHA.txt` naming a directory not on disk, the file at the pinned path not hashing to its pin, and the
|
|
313
|
+
one that matters most, **a pin that verified against a document that is not the committed one**. 742
|
|
314
|
+
of those 945 are on the two parts the citation gate actually reads. A mutant is written into a directory
|
|
315
|
+
named by its own hash, so re-hashing it succeeds: an integrity check cannot see that at all, and the
|
|
316
|
+
reader resolves clauses against a mutated standard and reports green. A SHA pin rewritten mid-run
|
|
317
|
+
means the thing being corrupted is the integrity check itself, which is why teaching a reader to
|
|
318
|
+
tolerate a writer was never on the table. Same probe after the change: 1,437 read cycles, zero
|
|
319
|
+
observations of any class. End to end, looping the reader file against ten runs of the two mutating
|
|
320
|
+
suites: ten of 31 reader runs failed before, zero of 30 after. Both controls were run, because a clean
|
|
321
|
+
after-figure is also what a dead probe prints.
|
|
322
|
+
|
|
323
|
+
**This also closes a flake the repeating-groups suite had disclosed and declined to fix**, and its
|
|
324
|
+
disclosure is deleted rather than reworded: the two generator suites raced each other through
|
|
325
|
+
`src/dictionary/generated/`, which reaches the whole test run and not just those two files, because
|
|
326
|
+
the shipped library imports both artifacts. It was declined then because the obvious fix was an
|
|
327
|
+
output-path override on a script the regen gate depends on. Relocating the tree costs the generators
|
|
328
|
+
no new input at all, so that trade is not the one on the table any more.
|
|
329
|
+
|
|
330
|
+
No library code changed, and no public surface moved.
|
|
331
|
+
|
|
332
|
+
- 023b22d: A diagnostic about a PHI leak is itself a PHI surface: two of the four measured instances are closed,
|
|
333
|
+
and the claim that tied them together is corrected on every surface that carried it.
|
|
334
|
+
|
|
335
|
+
**The three private-tag Tier-2 codes take no tag parameter at all.**
|
|
336
|
+
`DICOM_PRIVATE_TAG_NO_CREATOR`, `DICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR` and
|
|
337
|
+
`DICOM_PRIVATE_CREATOR_UNKNOWN` are built from `position` alone. Measured on a synthetic,
|
|
338
|
+
name-bearing payload: an `ST` carrying `"MR BRAIN SMITHSON "` whose Value Length under-declares by 12
|
|
339
|
+
desynchronizes the Implicit VR LE reader onto a fabricated header at an odd group, and the first two
|
|
340
|
+
of those codes rendered its tag as `4E495320` - `"IN S"` in wire order, four letters from inside the
|
|
341
|
+
name - on the same parse, from the same branch of `resolveImplicitVR`. `renderTag` validates a tag's
|
|
342
|
+
shape and therefore cannot refuse a fabricated one, so the bound is the **factory signature**, the
|
|
343
|
+
same remedy `DICOM_NONZERO_RESERVED_BYTES`, `DICOM_ITEM_CROSSES_SEQUENCE_END`,
|
|
344
|
+
`DICOM_DUPLICATE_TAG_IN_DATA_SET` and `DICOM_DUPLICATE_FILE_META_ELEMENT` already take. **What is
|
|
345
|
+
specific here is why it is a bound and not a product call:** all three fire only on an **odd** group,
|
|
346
|
+
and an odd group is the one class of tag no closed table this library holds can vouch for - PS3.6's
|
|
347
|
+
registry is even-group and a `Profile`'s private dictionary is keyed by a creator string this code
|
|
348
|
+
fires because it does not have. The third of the three is bound by that argument rather than by a
|
|
349
|
+
measurement, and is described that way. **The cost, stated:** on a well-formed file with an unclaimed
|
|
350
|
+
private block the tag is no longer in the message. It is still the element's key in the parsed Data
|
|
351
|
+
Set, and `position.byteOffset` locates the header.
|
|
352
|
+
|
|
353
|
+
**`report.embeddedAttributes[].hidden` carries only the tags the run acted on.** The embedded scanner
|
|
354
|
+
listed **every** tag in a run it found inside a kept carrier's Value Field, and a run needs only one
|
|
355
|
+
actionable attribute to be reported - so a fabricated header sitting beside a real one was listed
|
|
356
|
+
too. Measured: a `CS` carrier over-declaring across a fabricated `"SMIT"` header beside a genuine
|
|
357
|
+
`(0010,0020)` reported `hidden: ["4D535449", "00100020"]`. An entry is now one of the **652 literal
|
|
358
|
+
rows** of PS3.15 Table E.1-1 that this run's options left actionable, and **two drafts of that filter
|
|
359
|
+
were refuted before the sentence was true**. Filtering on the resolved Annex E action alone admits
|
|
360
|
+
every odd group, because the Basic Profile removes private attributes as a class. Adding "and an even
|
|
361
|
+
group" still admits every **repeating-group mask hit**: `annexE()` falls through to the family rows,
|
|
362
|
+
and `(50xx,xxxx)` Curve Data leaves the whole 16-bit element number free - 16 groups x 65,536
|
|
363
|
+
elements against 652 literal rows - so `"\fPAR"` composes `500C5241` and returns all four payload
|
|
364
|
+
bytes with one typed read. A mask match proves a rule exists; it does not make the membership finite.
|
|
365
|
+
The shipped test is "the tag has a literal row", which subsumes the odd-group case, since no literal
|
|
366
|
+
row is odd-group. **Two consequences that travel with the field:** it can now be **empty on a real
|
|
367
|
+
finding** (a run whose only actionable members are private attributes, Curve Data or Overlay elements
|
|
368
|
+
names none of them, and the carrier is still emptied and still counted), and it is **still uncapped**.
|
|
369
|
+
`DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED`'s `{n}` is unchanged and still counts the whole run, so
|
|
370
|
+
narrowing `hidden` did not silently re-scope a shipped message; its closing sentence was reworded,
|
|
371
|
+
because it pointed at a field that can now be empty.
|
|
372
|
+
|
|
373
|
+
**`report.removedPrivateTags` is deliberately unchanged.** It is a private-tag field, so no table can
|
|
374
|
+
vouch for its entries and a bound would empty it on every well-formed file - which is what it exists
|
|
375
|
+
to record. That is a product call, and the earlier reading that grouped all three as one call is
|
|
376
|
+
retracted: the test that decides it is whether a closed table can vouch for the tag, not whether the
|
|
377
|
+
tag is real on a good file.
|
|
378
|
+
|
|
379
|
+
**The "safe to log" claim is corrected, and a fifth instance was found correcting it.** Sweeping every
|
|
380
|
+
under-declare delta on both transfer syntaxes for every 4-byte window of the payload rendered as a
|
|
381
|
+
tag, every 4-byte window rendered as a `readUInt32LE` decimal and every 2-byte window rendered as a
|
|
382
|
+
VR, found a third leaking Tier-2 code on the base tree: **`DICOM_ODD_LENGTH_VALUE_PADDED` under
|
|
383
|
+
Explicit VR LE renders a fabricated tag AND a fabricated 32-bit declared length** - eight consecutive
|
|
384
|
+
payload bytes in one message, each reversible with one typed read. It is `PRE-EXISTING`, it is **not
|
|
385
|
+
closed here**, and it is why `ds.warnings[].message` is still not unconditionally safe: that code
|
|
386
|
+
fires on any tag, so the remedy is a membership `renderTag` plus a separate answer for the raw
|
|
387
|
+
length, which is a package-wide decision rather than a rider. It is pinned by an asserted row.
|
|
388
|
+
The claim was also attached to the wrong field: every fixture that produces the leak dies before a
|
|
389
|
+
`Dataset` exists, so the carrier is `onWarning` and the `{ strict: true }` `DicomParseError`, not a
|
|
390
|
+
surviving `ds.warnings` - and that no measured fixture put one on a surviving `ds.warnings` is stated
|
|
391
|
+
as a fact about those fixtures, never as a guarantee. Carriers of the old wording were found by
|
|
392
|
+
folding newlines rather than by a line-based search, since a sentence that wraps is invisible to one:
|
|
393
|
+
`README.md`, `limitations.md`, `troubleshooting.md`, `spec-notes-tolerance.md`, `cookbook.md` and the
|
|
394
|
+
JSDoc on `EmbeddedAttributeFinding`, `DeidentifyReport` and the three factories.
|
|
395
|
+
|
|
396
|
+
**Consumer-visible:** four warning message strings are reworded (no code, no `position` and no
|
|
397
|
+
`err.code` changes), and `embeddedAttributes[].hidden` may be shorter or empty on a file where it was
|
|
398
|
+
populated before. A consumer that string-matched those messages or read `hidden` as the whole run
|
|
399
|
+
should read the code and the warning's count instead.
|
|
400
|
+
|
|
401
|
+
- 3f41849: Docs, cookbook and examples: the roadmap's final phase, plus the gates that keep them honest.
|
|
402
|
+
|
|
403
|
+
**A cookbook that covers the jobs a metadata parser is actually handed.** Four new recipes, every one
|
|
404
|
+
executable in CI: extract metadata and index a folder of studies, build routing keys (hierarchy UIDs,
|
|
405
|
+
Accession Number, Patient ID paired with its issuer), read pixel-interpretation metadata safely, and
|
|
406
|
+
bridge to FHIR `ImagingStudy` and HL7 v2. The FHIR recipe works from the `ImagingStudy` "Mappings for
|
|
407
|
+
DICOM" tab and says which FHIR that is: the URL is the continuous build, not a balloted release, so
|
|
408
|
+
the recipe tells you to pin the mappings page for the FHIR version your integration targets.
|
|
409
|
+
|
|
410
|
+
**A clause citation written with its part beside it is checked against the SHA-pinned normative
|
|
411
|
+
documents, and the gate's coverage is stated rather than rounded up.** A new gate re-hashes
|
|
412
|
+
the vendored PS3.5, PS3.6 and PS3.15 2026c DocBook sources as a precondition, then runs two checks of
|
|
413
|
+
different strength. A clause of a vendored **prose** part (PS3.5, PS3.15) written with its label next
|
|
414
|
+
to its part (`PS3.N §X`, `PS3.N section X`, `PS3.N Annex X`) is resolved by collecting **every**
|
|
415
|
+
candidate section carrying that label and requiring exactly one, so zero and two are both refusals and
|
|
416
|
+
a first-match read cannot take the table of contents. **The gate's coverage is stated rather than
|
|
417
|
+
rounded up to "every citation": a label the text writes away from its part**, a second label in a list
|
|
418
|
+
or a bare `section X` whose part was named a sentence earlier, **is not seen**, and the cookbook says
|
|
419
|
+
so where a reader will meet it. Each clause the text leans
|
|
420
|
+
on for a normative statement is additionally required to carry that sentence **in its own body, not in
|
|
421
|
+
a subsection** (a body that swallowed subsections would certify `§7.5` for a `§7.5.2` fact and `§E.1`
|
|
422
|
+
for an `§E.1.1` one, which are the two confusions this package has already paid for; both are pinned
|
|
423
|
+
as controls). PS3.6 is cited for attribute identity rather than for prose, and is checked by its own
|
|
424
|
+
case: every `(gggg,eeee) Some Name` pair written anywhere in the docs must be the registry's own name
|
|
425
|
+
for that tag. **A numbered clause of a part this repository does not vendor may no longer be cited at
|
|
426
|
+
all**: two such citations were in the README and are replaced by prose that names the part and says
|
|
427
|
+
what is and is not claimed.
|
|
428
|
+
|
|
429
|
+
**`@example` on every public export is a gate rather than a convention.** Two exports were missing one
|
|
430
|
+
and now have it. The checker walks the public barrel through the compiler, so a namespace export and a
|
|
431
|
+
type count the same as a function does, and it carries a mutation control that proves it can go red.
|
|
432
|
+
|
|
433
|
+
**The PHI scanner reads doc fixtures.** The documentation ships DICOM objects as base64-encoded Part 10
|
|
434
|
+
buffers inline in markdown, and until now the scanner never opened one: to a text sweep a base64 run is
|
|
435
|
+
a single alphanumeric token with no `FAMILY^GIVEN` and no `YYYYMMDD` in it. `README.md` and
|
|
436
|
+
`docs-content/**` are now a second corpus, embedded objects are decoded and walked as DICOM, and both
|
|
437
|
+
the preamble-bearing and the preamble-less shape are recognized, the latter being what the cookbook
|
|
438
|
+
ships to demonstrate `DICOM_MISSING_PREAMBLE`. **The run floor is not the filter, and a first draft
|
|
439
|
+
that treated it as one shipped blind**: it required 120 base64 characters on the reasoning that a Part
|
|
440
|
+
10 object is big, and the cookbook's preamble-less fixture encodes to 88, so the one file the route's
|
|
441
|
+
own comments named as its reason was the one file it never opened while every test still passed. The
|
|
442
|
+
floor is now the shortest run that could encode a single Data Element header, the decode does the
|
|
443
|
+
filtering, and a regression case takes the **shortest real object out of the shipped cookbook** rather
|
|
444
|
+
than building its own. **It found real content the moment it ran**: every
|
|
445
|
+
sample object on the site carried a Study Date inside the 120-year window. Each is now `19000101`, and
|
|
446
|
+
the docs say why rather than leaving it as an unexplained oddity.
|
|
447
|
+
|
|
448
|
+
**A prominent "do not over-trust" page.** Known limitations moves out of the end of a long
|
|
449
|
+
troubleshooting page and into its own entry near the top of the navigation, linked from the README's
|
|
450
|
+
opening and from Getting started. It is an index rather than a second copy: scope non-goals, the open
|
|
451
|
+
PHI residuals (the retain-route leak whose extent is a matrix, the over-long `LO` in `(0012,0063)` that
|
|
452
|
+
this library is the likeliest writer of, `contextPath` being inert and not safe to log), the structural
|
|
453
|
+
facts no reader can resolve, and what is deliberately never defaulted.
|
|
454
|
+
|
|
455
|
+
**Two owed corrections.** The README described a Tier-2 warning message as "never composed from the
|
|
456
|
+
document", and `ParseOptions.strict`'s JSDoc called one "safe to log whole". Neither is true without a
|
|
457
|
+
qualifier: the registry's tag slot is filled by a shape check, which cannot refuse a tag a lying Value
|
|
458
|
+
Length composed out of somebody's value. Both now say safe on a well-formed file and not
|
|
459
|
+
unconditionally safe, which is what the measurement supports.
|
|
460
|
+
|
|
3
461
|
## 0.0.13
|
|
4
462
|
|
|
5
463
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
A developer-focused DICOM Part 10 parser and utility library for Node.js and TypeScript. **Metadata-first**: it reads the headers (patient, study, series, image, codes, UIDs) leniently and fast, exposes pixel data as raw bytes, and **never decodes pixels**. Sibling to [`@cosyte/hl7`](https://github.com/cosyte/hl7); same engineering bar.
|
|
18
18
|
|
|
19
|
+
> **Before you point this at real data, read [Known limitations](#known-limitations--non-goals).** Mis-reading a patient identifier, an image's signedness, or a rescale slope can cause real clinical harm, so the boundary is a deliverable here rather than a footnote: what is out of scope, which PHI residuals are measured-and-open, and which fields are not safe to log. A de-identified output from this package is **metadata-de-identified only**.
|
|
20
|
+
|
|
19
21
|
---
|
|
20
22
|
|
|
21
23
|
## Quickstart
|
|
@@ -157,7 +159,7 @@ A quirky object is tolerated rather than rejected, and absent fields come back `
|
|
|
157
159
|
|
|
158
160
|
### Build routing keys
|
|
159
161
|
|
|
160
|
-
Routing and reconciliation hang off a small set of identifiers. Surface them correctly: a Patient ID without its issuer is ambiguous across systems (PS3.3
|
|
162
|
+
Routing and reconciliation hang off a small set of identifiers. Surface them correctly: a Patient ID without its issuer is ambiguous across systems. The two attributes are `(0010,0020)` Patient ID and `(0010,0021)` Issuer of Patient ID in the PS3.6 2026c registry, which is vendored and SHA-pinned here; the module that requires them to be read together is in PS3.3, which is **not** vendored here, so no clause number is claimed for it.
|
|
161
163
|
|
|
162
164
|
```ts
|
|
163
165
|
// Hierarchy keys for filing into Study → Series → Instance:
|
|
@@ -187,7 +189,7 @@ img.pixelSpacing; // (0028,0030) patient-plane mm, distinct from imagerPixelSpac
|
|
|
187
189
|
|
|
188
190
|
> **Vendor note.** Philips writes private rescale tags `(2005,1409/140A/140B)` that shadow the standard `(0028,1052/1053)`; using the standard tags alone can yield non-quantitative values. This parser **preserves** the private tags so you can prefer them. Reach them with `ds.get("20051409")` (optionally under `profiles.philips`).
|
|
189
191
|
|
|
190
|
-
For Enhanced multi-frame objects, `image.frame(i)` resolves each frame's functional-group macros Per-Frame
|
|
192
|
+
For Enhanced multi-frame objects, `image.frame(i)` resolves each frame's functional-group macros Per-Frame first and Shared second. The functional-group macros are defined in PS3.3, which is **not** vendored here, so no clause number is claimed for them. It throws a `DicomValueError` (carrying only structural facts, never PHI) for an out-of-range frame or a required geometry macro missing from both groups.
|
|
191
193
|
|
|
192
194
|
```ts
|
|
193
195
|
if (img.isEnhancedMultiFrame) {
|
|
@@ -307,7 +309,7 @@ At an RSNA-era interoperability test, ~80% of real-world patient CDs failed stri
|
|
|
307
309
|
| 2 | Warning | Recoverable deviation | `DICOM_MISSING_PREAMBLE` |
|
|
308
310
|
| 3 | Fatal (always) | Unrecoverable structural error | `NOT_DICOM_PART_10` |
|
|
309
311
|
|
|
310
|
-
Tier-2 warnings are plain data on `ds.warnings`. Each carries a stable string `code`, a `message` looked up from a frozen registry
|
|
312
|
+
Tier-2 warnings are plain data on `ds.warnings`. Each carries a stable string `code`, a `message` looked up from a frozen registry, and a `position` with the byte offset where it occurred, so you can react programmatically. **What a message may contain is stated here as a mechanism rather than as a verdict**, because the verdict form of this paragraph was corrected twice and is deleted rather than tried a third time. The only substitutions into a registry template are structural. `{tag}` renders **only when PS3.6's element registry carries a literal row for that tag**, and `<withheld>` otherwise, so a tag that is private, a Group Length `(gggg,0000)`, a repeating-group member such as `(6000,3000)` Overlay Data, or four bytes a lying Value Length composed out of somebody's value is not echoed. `{vr}` renders only one of the 34 VRs PS3.5 2026c §6.2 defines. **A raw number a header carries is bound out of the factory signature rather than checked, where it is bound at all** - a declared Value Length has neither a shape nor a membership to test - so `DICOM_ODD_LENGTH_VALUE_PADDED` no longer prints the odd length and `DICOM_NONZERO_RESERVED_BYTES` no longer prints the two reserved bytes. **A raw number SHIFTED by a constant the reader can compute is that raw number**, so it is bound the same way: `DICOM_ITEM_CROSSES_SEQUENCE_END` no longer prints how many bytes remained inside the sequence, because that count is the sequence's own declared Value Length less the bytes of the sequence already consumed, and an addition puts it back. **The exceptions are named in one place that is not a record of a past change, and are deliberately not restated here** - the `WARNING_MESSAGES` docblock in [`src/parser/warnings.ts`](./src/parser/warnings.ts). No count of the copies is quoted, here or there: this package deletes a count it has corrected twice rather than incrementing it. `w.code` and `w.position` carry nothing from the document. **This is a statement about `w.message` and not about the rest of the output**: `DicomParseError.snippet` is still 16 raw source bytes, and `DeidentifyReport`'s value-bearing fields are named on the type. **The cost is real and is not minimised**: on a well-formed file a message about a private, overlay or group-length element no longer names its tag. The element is still in the Data Set under that tag and `position.byteOffset` locates the header.
|
|
311
313
|
|
|
312
314
|
```ts
|
|
313
315
|
import { parseDicom, WARNING_CODES } from "@cosyte/dicom";
|
|
@@ -336,7 +338,7 @@ The library throws five typed errors, all exported from the package barrel. Warn
|
|
|
336
338
|
|
|
337
339
|
### `DicomParseError`
|
|
338
340
|
|
|
339
|
-
Thrown by `parseDicom` on one of the 4 Tier-3 fatal codes. Carries the byte position, a registry-composed message, and a 16-byte hex `snippet` of the source. **The message is looked up in a frozen registry and its factories take no tag
|
|
341
|
+
Thrown by `parseDicom` on one of the 4 Tier-3 fatal codes. Carries the byte position, a registry-composed message, and a 16-byte hex `snippet` of the source. **The message is looked up in a frozen registry and its factories take no tag, no wire-length parameter and no count of the bytes left in the buffer**, so none of them can be interpolated: a fatal about a length field that lies fires precisely when bytes inside somebody's value are being read as a header, which is what used to make the tag and the wire length document content. The last of the three went in this release and **changes two message texts**, so a string match on them stops matching: a defined-length Sequence Item is parsed from a slice, so inside one the remaining-bytes count is that Item's own declared length less an offset the same message publishes. `err.code` is unchanged and `err.byteOffset` locates the element instead. The snippet is raw input and is cut in the frame that offset is counted in: treat it as PHI and redact it at your own boundary.
|
|
340
342
|
|
|
341
343
|
```ts
|
|
342
344
|
import { parseDicom, DicomParseError, FATAL_CODES } from "@cosyte/dicom";
|