@cosyte/dicom 0.0.8 → 0.0.10
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 +235 -0
- package/README.md +4 -3
- package/dist/index.cjs +40 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +58 -10
- package/dist/index.d.ts +58 -10
- package/dist/index.mjs +40 -24
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,241 @@ All notable changes to `@cosyte/dicom` will be documented in this file. The form
|
|
|
6
6
|
|
|
7
7
|
### Fixed
|
|
8
8
|
|
|
9
|
+
- **🩺 A private value crossed the PS3.5 §7.8.1 reservation boundary and was retained under
|
|
10
|
+
`RetainSafePrivate`, in output stamped `PatientIdentityRemoved = YES`.** The **absorb** direction
|
|
11
|
+
is closed; the **eject** direction and the private-`SQ` carve-out are measured, pinned and **not**
|
|
12
|
+
closed (see below)
|
|
13
|
+
(`DICOM-PRIVATE-CREATOR-RESERVATION-LEAK`). **`PRE-EXISTING` and live on the published `0.0.9`**;
|
|
14
|
+
found by `#51`'s pass-6 `conformance-refuter` and measured **identical on `origin/main`**, so it
|
|
15
|
+
was never introduced by that branch.
|
|
16
|
+
|
|
17
|
+
**The shape.** An `(FFFE,E000)` Item that declares more bytes than its enclosing `SQ` element's
|
|
18
|
+
Value Length allows reads past the end of its sequence and absorbs the element that followed it.
|
|
19
|
+
With a Private Creator in the Item's genuine content and the private data element written at the
|
|
20
|
+
**root** - where no creator reserved its block, so the Basic Profile removes it - the absorbed
|
|
21
|
+
element lands beside that creator and is kept verbatim. Measured on `164eb39`, Explicit VR LE and
|
|
22
|
+
BE, `deidentify(ds, { retain: ["RetainSafePrivate"], profile: profiles.ge })`:
|
|
23
|
+
`report.removedPrivateTags` is `[]`, `SECRET-PRIVATE-PHI` is in the serialized output,
|
|
24
|
+
`(0012,0062)` is `YES`, and **the file parses with no warning on either channel**. A stamp that
|
|
25
|
+
outruns the redaction is the worst shape this class has, because a consumer trusting it has no
|
|
26
|
+
signal at all.
|
|
27
|
+
|
|
28
|
+
**The remedy is at the de-identify boundary and it removes; it does not downgrade the stamp.**
|
|
29
|
+
PS3.5 2026c §7.8.1, read from the vendored pin (`4dfd7b8c…`), one occurrence in the document:
|
|
30
|
+
"Items within a sequence are self contained Data Sets ..., any Item in the sequence that contains
|
|
31
|
+
Private Data Elements shall also have Private Creator Data Element reserving a block of Elements
|
|
32
|
+
for those Private Data Elements. The scope of the reservation is just within the Item. Items do
|
|
33
|
+
not inherit the Private Data Element reservations made by Private Creator Data Elements in the
|
|
34
|
+
Data Set in which the Item is nested." The reservation's scope **is** the Item's boundary, so a
|
|
35
|
+
file that contradicts itself about where the Item ends does not determine which reservation
|
|
36
|
+
covers an element. PS3.15 2026c §E.3.10 "Retain Safe Private Option": "Private Attributes that
|
|
37
|
+
are known by the de-identifier to be safe from identity leakage shall be retained, together with
|
|
38
|
+
the Private Creator IDs that are required to fully define the retained Private Attributes; all
|
|
39
|
+
other Private Attributes shall be removed **or processed in the element-specific manner recommended by Deidentification Action (0008,0307), if present within Private Data Element Characteristics Sequence (0008,0300)**". **The clause licenses two dispositions and the
|
|
40
|
+
first draft of this entry closed the quote at "removed"**, reading a permissive clause as an
|
|
41
|
+
absolute; this library does not implement `(0008,0307)`, so removal is the branch available to it.
|
|
42
|
+
Nothing there is _known_, so the standard's own
|
|
43
|
+
default applies. Every private element **the recursion reaches** in such an Item, and at every
|
|
44
|
+
depth below it, is removed and named in `report.removedPrivateTags` - **with one carve-out, stated
|
|
45
|
+
in the sentence rather than only in a later paragraph**: a private `SQ` the profile vouches for is
|
|
46
|
+
settled before the descent and is kept verbatim (see below).
|
|
47
|
+
**The unit is the SEQUENCE, not the individual Item**: `descendSequence` decides once and
|
|
48
|
+
applies it to every item, so a two-item sequence where only item 1 over-runs also loses a genuine
|
|
49
|
+
block in the honest item 0 (measured and pinned; the grid sweeps single-item fixtures, so that
|
|
50
|
+
extra cost is NOT inside the priced 20).
|
|
51
|
+
**The unit is the SEQUENCE, not the individual Item**: `descendSequence` decides once and
|
|
52
|
+
applies it to every item, so a two-item sequence where only item 1 over-runs also loses a genuine
|
|
53
|
+
block in the honest item 0 (measured and pinned; the grid sweeps single-item fixtures, so that
|
|
54
|
+
extra cost is NOT inside the priced 20).
|
|
55
|
+
|
|
56
|
+
**It is NOT a parser bound, and no reading changes.** An over-declaring Item and an
|
|
57
|
+
under-declaring Sequence are byte-identical - `#51` reached that conclusion three times and its
|
|
58
|
+
refuter could build no predicate separating them. `scripts/measure-sq-bound-grid.ts` over
|
|
59
|
+
**83,037 cells** against `164eb39`: **0 cells differ in any PARSE respect**, **0 cells whose
|
|
60
|
+
READING differs** (a new counter; the existing parse-respect count folds both warning channels
|
|
61
|
+
in), 0 new lenient fatals, 0 new strict fatals, 0 markers lost or gained from the parsed object,
|
|
62
|
+
0 wrong root `(0010,0020)`, 0 reports that lose an attribute, and **0 Implicit VR LE cells
|
|
63
|
+
changed** - a free control, because that path slices the item stream so no Item can over-run.
|
|
64
|
+
|
|
65
|
+
**The fail-safe-direction argument `#51` made in five artifacts is RETRACTED, not reworded.** The
|
|
66
|
+
direction is not a property of the two readings; it is a property of **where the sender put the
|
|
67
|
+
Private Creator relative to the disputed bytes**. Both **absorb** placements are pinned by tests
|
|
68
|
+
and both are refused now: the creator inside the Item with the data element absorbed in, and the
|
|
69
|
+
data element inside the Item with the creator absorbed in. The **eject** placement is pinned as a
|
|
70
|
+
residual and is not refused.
|
|
71
|
+
|
|
72
|
+
**The price, measured rather than asserted.** The grid gained a `priv|` family - the first
|
|
73
|
+
population in that harness that runs `RetainSafePrivate` at all, which is why three earlier
|
|
74
|
+
refuter passes read "0 PHI regressions" off it while this was live. **58 -> 0** cells keep a
|
|
75
|
+
private value inside an Item on a self-contradicting file, and **20 of those 58 were not leaking
|
|
76
|
+
anything**: the creator and the data element were both genuine Item content and the reservation
|
|
77
|
+
was real. They pay for the guarantee, because nothing on the wire distinguishes them. Retention on
|
|
78
|
+
a file whose length fields agree is **unchanged** - 6 -> 6 inside an Item, **9 -> 9** at the root -
|
|
79
|
+
and 0 -> 0 rows are kept with no creator in scope, on either tree.
|
|
80
|
+
|
|
81
|
+
**🩺 Two routes are NOT closed, found by the pass-1 `conformance-refuter` grade.** Both are
|
|
82
|
+
`PRE-EXISTING`, reproduce identically on `164eb39`, and both produce the same false attestation
|
|
83
|
+
(`removedPrivateTags: []`, the value in the output, `(0012,0062) = YES`).
|
|
84
|
+
**(a) The eject direction.** An Item that _under_-declares pushes its trailing elements **out**
|
|
85
|
+
into the enclosing Data Set, which is not narrowed, so a Private Creator that lands there reserves
|
|
86
|
+
a block for elements the sender never gave it. **It is NOT root-specific and an earlier draft of
|
|
87
|
+
this entry said it was** - pass 2 reproduced it one level down, where an inner sequence ejects a
|
|
88
|
+
creator into the still-usable enclosing Item. The item filed for this must be scoped to every
|
|
89
|
+
still-usable Data Set or it will be built to the wrong bar. Silent on every channel
|
|
90
|
+
and under `{ strict: true }`, Explicit VR LE and BE. **22 grid cells.** The widening was **built
|
|
91
|
+
and measured**: narrowing the flag whenever a Data Set _contains_ an over-running sequence costs
|
|
92
|
+
**24** root retentions and closes **2** of the 22, because the other 20 are Implicit VR LE where
|
|
93
|
+
the sequence records no over-run at all. A different mechanism, and its own item.
|
|
94
|
+
**(b) The private-`SQ` carve-out.** `keepsPrivate` decides **before** the descent, so a private
|
|
95
|
+
`SQ` the profile vouches for is kept **verbatim** and this rule is never consulted inside it. The
|
|
96
|
+
first draft of this entry said "every private element in such an Item ... is removed",
|
|
97
|
+
unconditionally, which is `#54`'s exact refusal repeated; the sentence is corrected rather than the
|
|
98
|
+
guard widened. Both routes are pinned as **residual tests that assert the leaking behaviour**, so
|
|
99
|
+
they go red when fixed.
|
|
100
|
+
|
|
101
|
+
**No new public surface**: no new warning code, no new report field, no snapshot change. The
|
|
102
|
+
action is already visible on `report.removedPrivateTags`, the channel this defect read `[]` on.
|
|
103
|
+
What is _not_ on any channel is the reason - a caller sees which private tags went, not that the
|
|
104
|
+
file contradicted itself. Disclosed rather than closed, because a new Tier-2 code would also have
|
|
105
|
+
to answer to `profiles.strict` escalation and to the per-element amplification bound `#48`
|
|
106
|
+
established.
|
|
107
|
+
|
|
108
|
+
- **An unrecognized Explicit VR was read short-form, contrary to PS3.5 2026c §6.2's normative
|
|
109
|
+
"shall"** (`DICOM-UNRECOGNIZED-VR-SHORT-FORM`). It is now read - and written - long-form. **This
|
|
110
|
+
is a behaviour change on the read path**, and the trade is measured below rather than asserted.
|
|
111
|
+
|
|
112
|
+
**The clause, read from the vendored `vendor/nema/part05` pin (`4dfd7b8c…`), one occurrence in the
|
|
113
|
+
document.** §6.2 "Value Representation (VR)": "All new VRs defined in future versions of DICOM
|
|
114
|
+
**shall** be of the same Data Element Structure as defined in [§7.1.2] with reserved bytes after
|
|
115
|
+
the VR and a 32-bit unsigned integer VL (i.e., following the format for VRs such as OB or UT), and
|
|
116
|
+
may or may not permit Undefined Length." §6.2's **note** about an implementation choosing to
|
|
117
|
+
ignore unrecognized VRs is informative, and is deliberately not what this rests on.
|
|
118
|
+
|
|
119
|
+
**What a conformant future-VR file used to do: no sentence, deliberately.** Four attempts have
|
|
120
|
+
now been made to summarize that in one sentence and all four were wrong, the fourth by the first
|
|
121
|
+
draft of this entry. It is **shape-specific**: the short-form read took the carrier's length from
|
|
122
|
+
the two bytes §7.1.2 reserves (`0x0000`), got a zero-length value, and resumed inside the 32-bit
|
|
123
|
+
VL field - so what happened next depended on the payload's own bytes. Sometimes a whole-object
|
|
124
|
+
`INVALID_FILE_META`; sometimes a **clean parse into a tree the sender did not write**, with a
|
|
125
|
+
zero-length carrier plus an element manufactured out of the payload and, under Explicit VR BE,
|
|
126
|
+
**no warning on either channel**. `scripts/measure-unrecognized-vr.ts` is new here and prints the
|
|
127
|
+
per-shape table for both trees, including that case (`long-payload-tiles`). **Add a shape rather
|
|
128
|
+
than writing a summary.**
|
|
129
|
+
|
|
130
|
+
**What changed, and what deliberately did not.** `readExplicitElementHeader` and the File Meta
|
|
131
|
+
element reader take the 12-byte header for any VR outside the 34 this edition defines;
|
|
132
|
+
`serializeDicom` writes one. Everything after the header read is untouched - the same value read,
|
|
133
|
+
the same "declared length exceeds the remaining buffer" refusal, and the same undefined-length
|
|
134
|
+
refusal that `OB` / `UT` / `UN` already take. **This is one more VR class routed into an existing
|
|
135
|
+
bound, not a new bound**, which is the distinction four refusals in this family were about. No new
|
|
136
|
+
warning code was minted: a §6.2-conformant file is not something to warn about, and a new Tier-2
|
|
137
|
+
code would throw under `{ strict: true }` on exactly such a file.
|
|
138
|
+
|
|
139
|
+
**The reader and the writer had to move in one commit.** The short form's length field is 16 bits.
|
|
140
|
+
With the reader fixed alone, a 70,000-byte unrecognized-VR value would have been re-emitted
|
|
141
|
+
declaring **4,464**, silently. Pinned by a round-trip test at that size.
|
|
142
|
+
|
|
143
|
+
**The trade, on `scripts/measure-sq-bound-grid.ts` against `66f0c95` (76,611 cells).**
|
|
144
|
+
**1,221 recovered** (fatal before, parse now) against **932 newly refused**; 0 PHI regressions;
|
|
145
|
+
0 cells where the root `(0010,0020)` changes value; 0 reports that lose an attribute on a cell
|
|
146
|
+
that still parses; **0 Implicit VR LE cells changed at all** (a free control - there is no on-wire
|
|
147
|
+
VR there); leaking cells unmoved at 11 (`DICOM-BINARY-CARRIER-OVERDECLARE`, untouched here).
|
|
148
|
+
**All 932** of the newly refused had an unrecognized VR in their `66f0c95` parse tree, so every
|
|
149
|
+
file this refuses is one the old reader only "read" by manufacturing a Data Element header out of
|
|
150
|
+
the middle of somebody's value; the fabricated VRs across that population include `"CT"` - a
|
|
151
|
+
Modality value read as a VR.
|
|
152
|
+
The grid also reads **0 cells that parse on both trees and read differently**. Read that as a
|
|
153
|
+
statement about the grid's fixtures, **not** as "nothing is silently re-read": the
|
|
154
|
+
`long-payload-tiles` shape is exactly such a cell and it is outside anything the grid sweeps.
|
|
155
|
+
**The mirror shape is the honest cost**: a sender that ignores §6.2 and writes an unrecognized VR
|
|
156
|
+
in the 8-byte short form produced a readable object before and is refused now. That is pinned by a
|
|
157
|
+
test rather than left to be discovered.
|
|
158
|
+
|
|
159
|
+
**`deidentify()` is unchanged, and one of its claims was not.** The rule that empties an element
|
|
160
|
+
whose VR is not one of the 34 still fires - reading a header is not the same as knowing what its
|
|
161
|
+
value means, and Table E.1-1 acts per attribute. **Disclosed, not fixed**: it is the same
|
|
162
|
+
over-redaction trade the un-auditable-sequence rule makes, and re-deciding it is a product call.
|
|
163
|
+
|
|
164
|
+
**`#55` published "on a file conformant to PS3.5 2026c the cost is zero". It was never true**, and
|
|
165
|
+
no replacement account of the old reader belongs here: three were written while this entry was
|
|
166
|
+
drafted and the gate refuted all three. The harness prints what each shape does on each tree, and
|
|
167
|
+
the shape that refutes the shortest version of the story is a row in it
|
|
168
|
+
(`long-payload-tiles-future-vr`). **Add a shape rather than a sentence.**
|
|
169
|
+
|
|
170
|
+
**`DICOM_NONZERO_RESERVED_BYTES` no longer names a tag, and that is a PHI fix this slice owed.**
|
|
171
|
+
Reading an unrecognized VR long-form means landing on the two bytes §7.1.2 reserves, so this code
|
|
172
|
+
is newly reachable on a **fabricated** header - one built out of the middle of somebody's value.
|
|
173
|
+
Its message interpolated the four bytes it would call a tag: measured on an `ST` carrier holding
|
|
174
|
+
`"MR BRAIN SMITHSON"` under-declared by 6, it streamed `Element (54495348) …` - the bytes
|
|
175
|
+
`"ITHS"` in wire order, four letters of the surname - where `66f0c95` emitted nothing naming that
|
|
176
|
+
element on the same file (its only warning there is `DICOM_ODD_LENGTH_VALUE_PADDED`, on a genuine
|
|
177
|
+
`(0010,0010)`). (Quote the payload **with** its tag: `"MR BRAIN SMITHSON "` produces `48544F53`
|
|
178
|
+
instead, and an earlier draft of this entry paired the two wrongly.) **The factory now takes no
|
|
179
|
+
tag parameter**, so the bound is the signature rather than a branch, and `position.byteOffset`
|
|
180
|
+
locates the element. This is exactly the remedy `#55` paid a blocker for in
|
|
181
|
+
`report.undefinedVrElements[].tag`, and the reason is the same: where the trigger is "these bytes
|
|
182
|
+
are not what they claim to be", `renderTag` checks shape and cannot refuse them. It also closes a
|
|
183
|
+
`PRE-EXISTING` instance on the same factory - a long-form VR fabricated the same way rendered its
|
|
184
|
+
tag on `66f0c95` too. Pinned with a name-bearing fixture and a non-vacuity assertion that the code
|
|
185
|
+
really fires.
|
|
186
|
+
|
|
187
|
+
**A second `DeidentifyReport` claim corrected rather than widened.** The report was described as
|
|
188
|
+
"value-free apart from `uidMap`". `removedPrivateTags` is a second exception: its entries are four
|
|
189
|
+
source bytes each, and on a malformed file those bytes can be document content. Measured
|
|
190
|
+
identically on **both** trees - an `OB` carrier holding `"SECRET-NOTE-"` followed by a well-formed
|
|
191
|
+
odd-group header reports `["41534342"]`, whose wire-order bytes read `"SABC"`. Reporting the tag
|
|
192
|
+
is the whole audit value of the field on a well-formed file, so it is documented as PHI rather
|
|
193
|
+
than withheld; narrowing it is a product decision that has not been made.
|
|
194
|
+
|
|
195
|
+
- **A test asserted the machine it ran on rather than the code, and the script suites paid a
|
|
196
|
+
process start they did not need** (`PARSER-TESTTIMEOUT-ASSERTS-AN-IDLE-BOX`). This is a trim plus
|
|
197
|
+
two deletions and nothing else. Test and test-configuration only: no source, parser, de-identify,
|
|
198
|
+
serializer or public API change, and no number in this package moves.
|
|
199
|
+
|
|
200
|
+
**The trim.** Three suites spawned this repository's own TypeScript scripts through `tsx`. Node
|
|
201
|
+
strips types natively, so that process start bought nothing and was paid on every invocation.
|
|
202
|
+
They now go through `test/helpers/run-script.ts`, which runs them under `node`, measured with
|
|
203
|
+
byte-identical stdout and the same exit code from both runners. The suites are measurably faster;
|
|
204
|
+
no ratio is quoted here, because independent re-takes on one box do not agree with each other and
|
|
205
|
+
the gap moves with the workload as well as the box.
|
|
206
|
+
|
|
207
|
+
**The one script that cannot make that move, pinned rather than asserted.**
|
|
208
|
+
`scripts/generate-annex-e.ts` imports `"../src/dictionary/repeating-groups.js"`, and Node's ESM
|
|
209
|
+
resolver will not rewrite that `.js` specifier onto the `.ts` file; under `node` it exits 1 with
|
|
210
|
+
`ERR_MODULE_NOT_FOUND` at module resolution, before it opens a byte of DocBook or touches the
|
|
211
|
+
tracked `SHA.txt` this suite otherwise rewrites. It stays on `tsx`, and a new test asserts that
|
|
212
|
+
failure **by name**, so the carve-out goes red the day Node can resolve the specifier rather than
|
|
213
|
+
quietly outliving its reason. The rule and its single exception are stated once, in
|
|
214
|
+
`run-script.ts`, rather than copied into each suite: the sibling generator imports nothing from
|
|
215
|
+
`src/` and was measured green under `node` with byte-identical output, so the carve-out is one
|
|
216
|
+
script and not "the generators".
|
|
217
|
+
|
|
218
|
+
**Deletion one: a stopwatch that was already a false red.** `embedded-attribute.test.ts` carried
|
|
219
|
+
`expect(performance.now() - started).toBeLessThan(2_000)` over a 512 KiB value. It reads a couple
|
|
220
|
+
of hundred milliseconds on a quiet box, but on an ordinary loaded one it was measured at or over
|
|
221
|
+
that literal with the algorithm untouched. It was also the weakest cost claim in this package: its
|
|
222
|
+
fixture is half a megabyte of `0x41`, which yields zero tiling candidates, so every offset
|
|
223
|
+
short-circuits before the repertoire scan where the quadratic it named lived and the number it
|
|
224
|
+
produced was never a measurement of that cost. Deleted, and the test renamed to what it does
|
|
225
|
+
assert. The cost is still bounded, by `vitest.config.ts`'s per-test
|
|
226
|
+
timeout, which is the mechanism built for it.
|
|
227
|
+
|
|
228
|
+
**Deletion two: `hookTimeout: 10_000`, a verbatim no-op.** Measured rather than read: a `beforeAll`
|
|
229
|
+
that sleeps past it reds at "Hook timed out in 10000ms" on this repository's own Vitest with no
|
|
230
|
+
configuration at all, so the line restated the default exactly and said nothing. The one hook that
|
|
231
|
+
needs more (`docs-content.test.ts`, which shells out to `pnpm build`) already passes its own budget
|
|
232
|
+
at the call site.
|
|
233
|
+
|
|
234
|
+
**`testTimeout: 10_000` is UNCHANGED.**
|
|
235
|
+
|
|
236
|
+
**Disclosed residual, not fixed here.** The two `test/property/` fast-check suites can exceed that
|
|
237
|
+
10,000 ms global on a contended box, which reds them with the parser untouched. A per-test budget
|
|
238
|
+
for them was built and then **withdrawn**: the measurements offered for it did not survive
|
|
239
|
+
re-measurement, and the correct figures do not support the claim they were used to make. Those
|
|
240
|
+
suites therefore remain on the 10,000 ms global - **exactly where `main` is today** - so this
|
|
241
|
+
change neither fixes nor worsens that. It is a live false red and it needs its own slice, measured
|
|
242
|
+
from scratch.
|
|
243
|
+
|
|
9
244
|
- **The `attw` publish gate exited 0 on a tarball carrying no type declarations, so a broken publish
|
|
10
245
|
read as a pass** (`ATTW-FALSE-GREEN-PORT`). `pnpm attw` now runs `scripts/attw.mjs`, ported from
|
|
11
246
|
`@cosyte/terminology` (#28, `bf153cb`), where the defect was diagnosed; `typecheck:exports` runs
|
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ That's the pitch: no config, no schema upload, no spec lookup. The parser accept
|
|
|
47
47
|
- **Lazy typed value decode**: `element.value` decodes raw bytes into a discriminated `DicomValue` across all 34 VRs (numbers, `bigint`s, person names, dates/times, sequences, raw `binary`), honoring `(0008,0005)` Specific Character Set through nested items.
|
|
48
48
|
- **Real-world tolerance, Postel's Law**: a lenient reader emits 25 stable warning codes for what it tolerated; only 4 truly-structural conditions are fatal. The serializer always writes spec-clean Part 10.
|
|
49
49
|
- **Source/vendor profile system**: `defineProfile()` + 5 built-ins (`ge`, `siemens`, `philips`, `strict`, `lenient`) that only ever _tighten or annotate_ a parse, resolving vendor private tags by the file's live Private Creator string, never a wrong decode.
|
|
50
|
-
- **Metadata-level de-identification**: `deidentify()` applies the PS3.15 Annex E Basic Profile + the nine metadata Options, returning a fresh dataset and an audit report that
|
|
50
|
+
- **Metadata-level de-identification**: `deidentify()` applies the PS3.15 Annex E Basic Profile + the nine metadata Options, returning a fresh dataset and an audit report built from static tables, with two fields that carry source bytes and are named as such on the type: `report.uidMap` (source UIDs) and `report.removedPrivateTags`.
|
|
51
51
|
- **Spec-clean serializer**: `serializeDicom(ds)` round-trips a dataset back to Part 10 bytes in its source transfer syntax (no transcode), with correct File Meta group length, even-length padding, byte-exact sequence passthrough, and lossless File Meta: non-modeled `(0002,xxxx)` elements are preserved and re-emitted in tag order.
|
|
52
52
|
- **Strict TypeScript, dual ESM + CJS, Node ≥ 22**: `noUncheckedIndexedAccess`, no `any`, JSDoc + `@example` on every public export feeding your editor's IntelliSense. Zero runtime dependencies today.
|
|
53
53
|
|
|
@@ -197,7 +197,7 @@ if (img.isEnhancedMultiFrame) {
|
|
|
197
197
|
|
|
198
198
|
### De-identify before sharing
|
|
199
199
|
|
|
200
|
-
`deidentify()` applies the PS3.15 Annex E Basic Application Level Confidentiality Profile (replacing, emptying, or removing every attribute the standard lists as identifying) and returns a fresh dataset plus a report
|
|
200
|
+
`deidentify()` applies the PS3.15 Annex E Basic Application Level Confidentiality Profile (replacing, emptying, or removing every attribute the standard lists as identifying) and returns a fresh dataset plus a report. Two of the report's fields are composed from source bytes rather than from static tables and are documented as such: `report.uidMap`, whose keys are the file's own UIDs, and `report.removedPrivateTags`, whose four-byte entries are the sender's own private tag numbers on a well-formed file and can be value bytes on a malformed one.
|
|
201
201
|
|
|
202
202
|
```ts
|
|
203
203
|
import { parseDicom, deidentify, serializeDicom } from "@cosyte/dicom";
|
|
@@ -365,7 +365,8 @@ Thrown by `serializeDicom` for `MISSING_TRANSFER_SYNTAX` (the dataset names no t
|
|
|
365
365
|
- **Pixel data.** No decode/decompression, no rendering, no measurements: Pixel Data is exposed as raw bytes. And v1 does not read a **compressed object at all**, not even structurally: a transfer syntax outside the four listed below is the fatal `UNSUPPORTED_TRANSFER_SYNTAX`, so JPEG / JPEG-LS / JPEG2000 / RLE / HTJ2K objects do not parse. → `@cosyte/dicom-pixel`.
|
|
366
366
|
- **Burned-in PHI.** v1 **warns** it cannot remove burned-in annotation; a "de-identified" output is **metadata-de-identified only**.
|
|
367
367
|
- **A sequence the parser could not open: content is dropped, not passed through.** `deidentify()` recurses only into a sequence whose items the parser materialized. A **standard** `SQ` element with no items is now **emptied**: its bytes are Data Sets by PS3.5 §7.5.1 and PS3.15 §E.1.1 obliges the de-identifier to reach the listed attributes inside them, so a run that cannot enumerate them must not ship them. `report.unauditableSequences` names the carrier and the byte length dropped (capped at 64 entries; the emptying itself is never capped), and `DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE` is raised. Expect **data loss** on such a file. The parse refusal is usually a sender-side encoding defect, but not always: a **conformant** file nested deeper than this library's own `NESTING_DEPTH_LIMIT` of 64 is refused the same way and loses that sequence too. **Two shapes are exempt and still leak.** A **private** `SQ` kept under `RetainSafePrivate` + a `Profile` is kept verbatim and unaudited, because the profile vouched for it. And an undefined-length **`UN`** the CP-246 descent could not read as a sequence keeps `vr === "UN"`, so the rule does not reach it and cannot be extended there: every ordinary `UN` element also has no items. For either, the reliable test remains `el.items === undefined` on the element itself.
|
|
368
|
-
- **An
|
|
368
|
+
- **An unrecognized Explicit VR is read (and written) long-form, and `deidentify()` still empties the element.** PS3.5 2026c §6.2: "All new VRs defined in future versions of DICOM **shall** be of the same Data Element Structure as defined in [§7.1.2] with reserved bytes after the VR and a 32-bit unsigned integer VL." So two on-wire VR bytes outside the 34 this release knows get a **12-byte header**, exactly as `OB` and `UT` do, on both the read and the write path. Before `0.0.9` the reader took the 8-byte form, which read the length out of the two bytes §6.2 reserves. What such a file did then was **shape-specific** - sometimes a whole-object refusal, sometimes a clean parse into a tree the sender did not write - which is why this package ships `scripts/measure-unrecognized-vr.ts` and no one-sentence account of it. The value is otherwise handled like any other: a declared length past the end of the buffer, or an undefined length, is refused exactly as it is for every other non-`SQ` VR. **The trade is stated rather than buried**: a sender that ignores §6.2 and writes an unrecognized VR short-form produced a readable object before and is refused now. `deidentify()` **still empties** such an element, because reading a header is not the same as knowing what its value means - Table E.1-1 acts per attribute, and nothing can say whether a VR from a later edition holds a name, a date or an opaque blob. `report.undefinedVrElements` names the **byte offset** and the byte length dropped (capped at 64 entries; the emptying itself is never capped) and `DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE` is raised. It names **no tag**, deliberately and uniquely among the report's findings: the header may still have been fabricated out of the middle of some element's value, in which case its tag bytes are document content - and nothing distinguishes that case from an honestly-written unrecognized VR, so the tag is withheld either way. An Implicit VR LE file cannot reach any of this: there the VR comes from the dictionary. `UN` is one of the 34 and is not affected. **There is no exemption** - unlike the sequence rule above, `RetainSafePrivate` does not keep such an element.
|
|
369
|
+
- **`RetainSafePrivate` retains nothing inside ANY Item of a sequence whose item stream over-runs its own declared length.** An Item that declares more bytes than its enclosing Sequence does absorbs whatever follows the sequence, so a private data element the sender wrote at the **root** - with no Private Creator reserving its block there - can land beside a creator that is genuine Item content and be kept verbatim. Through `0.0.9` it was, with `report.removedPrivateTags` reading `[]` and the object stamped `(0012,0062) Patient Identity Removed = YES`: a stamp that outran the redaction. PS3.5 2026c §7.8.1 scopes a private block reservation to the Item ("The scope of the reservation is just within the Item"), so a file that contradicts itself about where the Item ends does not determine which reservation covers an element; PS3.15 2026c §E.3.10 retains only what is **known** safe and requires that "all other Private Attributes shall be removed **or processed in the element-specific manner recommended by Deidentification Action (0008,0307), if present within Private Data Element Characteristics Sequence (0008,0300)**" (a two-branch clause; this library does not implement `(0008,0307)`, so removal is the branch available to it). So every private element the recursion **reaches** inside such a sequence, at every depth below it, is removed and named in `report.removedPrivateTags`. **The unit is the SEQUENCE, not the individual Item**: `descendSequence` decides once and applies it to every item, so a two-item sequence where only item 1 over-runs also loses a genuine block sitting in the honest item 0 (measured; the grid sweeps single-item fixtures, so that extra cost is NOT inside the priced 20). **Two routes are not closed and both still leak, measured and pinned:** an Item that _under_-declares **ejects** its trailing elements out into the enclosing Data Set, which is not narrowed - **at any depth, not only at the root** (the widening was built and measured at 24 retentions lost to close 2 of 22, so it is a separate slice); and a **private `SQ`** the profile vouches for is kept verbatim before the descent runs, which is the carve-out named two bullets above. **The direction is not fail-safe on its own and the opposite claim is retracted**: it depends on where the sender put the Private Creator. Both **absorb** placements are pinned by tests; the **eject** placement is pinned as a residual and still leaks. **The price is measured, not asserted**: on `scripts/measure-sq-bound-grid.ts`, 58 cells lose a private value and **20 of them were not leaking anything** (the creator and the data element were both genuine Item content). Retention on a file that does not contradict itself is unchanged, and **no reading changes** - no parser file is touched. This is not a parser bound: an over-declaring Item and an under-declaring Sequence are byte-identical, so nothing on the wire can separate them.
|
|
369
370
|
- **An over-declared Value Length into a binary carrier still leaks, and it is a known trade.** The mirror case - an _over_-declared length that swallows the following element into an `OB`/`OW`/`US`/`UN` value - is **not** detected, and a `(0010,0020)` inside it reaches de-identified output with no warning and no report entry. Arbitrary bytes are exactly what those VRs are for, so no content test can decide it; the only candidate remedy was measured and empties conformant binary values (a legal LUT or blob deleted because 8 of its bytes read as a zero-length `(0010,0020)`). String carriers **are** covered, because there the same bytes are provably outside the VR's repertoire. If you accept files from a sender you do not control, treat a binary attribute's length as untrusted.
|
|
370
371
|
- **Networking & web.** No DIMSE (C-STORE/FIND/MOVE, MWL, MPPS); no DICOMweb (QIDO/WADO/STOW). → `@cosyte/dicom-net`, `@cosyte/dicomweb`.
|
|
371
372
|
- **Transcoding.** No transfer-syntax conversion. The serializer re-emits in the dataset's source syntax only.
|
package/dist/index.cjs
CHANGED
|
@@ -11,7 +11,7 @@ var __export = (target, all) => {
|
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
// src/version.ts
|
|
14
|
-
var VERSION = "0.0.
|
|
14
|
+
var VERSION = "0.0.10";
|
|
15
15
|
|
|
16
16
|
// src/dictionary/index.ts
|
|
17
17
|
var dictionary_exports = {};
|
|
@@ -11703,6 +11703,12 @@ var BE_VR_STRIDE = Object.freeze({
|
|
|
11703
11703
|
UT: 0,
|
|
11704
11704
|
SQ: 0
|
|
11705
11705
|
});
|
|
11706
|
+
var KNOWN_VRS = Object.freeze(
|
|
11707
|
+
new Set(Object.keys(BE_VR_STRIDE))
|
|
11708
|
+
);
|
|
11709
|
+
function isRecognizedVr(vr) {
|
|
11710
|
+
return KNOWN_VRS.has(vr);
|
|
11711
|
+
}
|
|
11706
11712
|
|
|
11707
11713
|
// src/parser/warnings.ts
|
|
11708
11714
|
var WARNING_CODES = {
|
|
@@ -11753,7 +11759,11 @@ var WARNING_MESSAGES = Object.freeze({
|
|
|
11753
11759
|
DICOM_VR_MISMATCH: "Element ({tag}) on-wire VR is {vr2}; dictionary lists {vr}. Trusting on-wire VR.",
|
|
11754
11760
|
DICOM_PRIVATE_TAG_NO_CREATOR: "Private element ({tag}) has no Private Creator registered for its block; treating as VR=UN.",
|
|
11755
11761
|
DICOM_GROUP_LENGTH_IN_DATASET: "Retired Group Length element ({tag}) encountered in dataset; preserved as-is.",
|
|
11756
|
-
|
|
11762
|
+
// The tag is deliberately absent, and this is the only *parser* message where
|
|
11763
|
+
// that is true. See `nonzeroReservedBytes`. Kept terse for the same reason the
|
|
11764
|
+
// de-identify messages are: it is emitted once per element, and element count
|
|
11765
|
+
// is attacker-chosen.
|
|
11766
|
+
DICOM_NONZERO_RESERVED_BYTES: "Non-zero reserved bytes between VR and length (first byte {n}, second byte {n2}); ignoring. Tag withheld; the byte offset identifies the element.",
|
|
11757
11767
|
DICOM_SQ_NOT_DESCENDED: "Element ({tag}) resolved to VR=SQ from the dictionary but its defined-length value is not a valid item stream; kept as opaque bytes and NOT descended, so nothing nested inside it is visible to navigation, and deidentify() cannot audit it. See DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE.",
|
|
11758
11768
|
DICOM_UN_PARSED_AS_SQ: "Element ({tag}) has VR=UN with undefined length; descended as Implicit VR LE sequence per CP-246.",
|
|
11759
11769
|
DICOM_EMPTY_ITEM_IN_SEQUENCE: "Sequence ({tag}) contains an empty item (length=0); tolerated.",
|
|
@@ -11788,7 +11798,6 @@ var WARNING_MESSAGES = Object.freeze({
|
|
|
11788
11798
|
DICOM_UNSUPPORTED_CHARSET: "(0008,0005) Specific Character Set value {n} names a defined term this build does not support; decoding text as UTF-8 best-effort. The term is not reproduced here - read the element if you need it.",
|
|
11789
11799
|
DICOM_CHARSET_AMBIGUOUS_SEPARATOR: "(0008,0005) Specific Character Set value {n} is ambiguous under the active code extensions; decoding text as UTF-8 best-effort."
|
|
11790
11800
|
});
|
|
11791
|
-
var KNOWN_VRS = new Set(Object.keys(BE_VR_STRIDE));
|
|
11792
11801
|
var TAG_SHAPE = /^[0-9A-F]{8}$/u;
|
|
11793
11802
|
function renderTag(tag) {
|
|
11794
11803
|
return tag !== void 0 && TAG_SHAPE.test(tag) ? tag : WITHHELD;
|
|
@@ -11830,9 +11839,8 @@ function privateTagNoCreator(position, tag) {
|
|
|
11830
11839
|
function groupLengthInDataset(position, tag) {
|
|
11831
11840
|
return build(WARNING_CODES.DICOM_GROUP_LENGTH_IN_DATASET, position, { tag });
|
|
11832
11841
|
}
|
|
11833
|
-
function nonzeroReservedBytes(position,
|
|
11842
|
+
function nonzeroReservedBytes(position, first, second) {
|
|
11834
11843
|
return build(WARNING_CODES.DICOM_NONZERO_RESERVED_BYTES, position, {
|
|
11835
|
-
tag,
|
|
11836
11844
|
n: first,
|
|
11837
11845
|
n2: second
|
|
11838
11846
|
});
|
|
@@ -12036,12 +12044,12 @@ function readExplicitElementHeader(cursor, _ctx, emit) {
|
|
|
12036
12044
|
const vr = vrSlice.toString("ascii");
|
|
12037
12045
|
let length;
|
|
12038
12046
|
let headerLength;
|
|
12039
|
-
if (LONG_FORM_VRS.has(vr)) {
|
|
12047
|
+
if (LONG_FORM_VRS.has(vr) || !isRecognizedVr(vr)) {
|
|
12040
12048
|
const reserved0 = cursor.buffer[cursor.position];
|
|
12041
12049
|
const reserved1 = cursor.buffer[cursor.position + 1];
|
|
12042
12050
|
cursor.position += 2;
|
|
12043
12051
|
if ((reserved0 ?? 0) !== 0 || (reserved1 ?? 0) !== 0) {
|
|
12044
|
-
emit(nonzeroReservedBytes({ byteOffset: headerStart },
|
|
12052
|
+
emit(nonzeroReservedBytes({ byteOffset: headerStart }, reserved0 ?? 0, reserved1 ?? 0));
|
|
12045
12053
|
}
|
|
12046
12054
|
length = cursor.readUInt32();
|
|
12047
12055
|
headerLength = 12;
|
|
@@ -12179,7 +12187,7 @@ function readExplicitLeElement(cursor) {
|
|
|
12179
12187
|
const vr = cursor.slice(2).toString("ascii");
|
|
12180
12188
|
let length;
|
|
12181
12189
|
let headerLength;
|
|
12182
|
-
if (LONG_FORM_VRS.has(vr)) {
|
|
12190
|
+
if (LONG_FORM_VRS.has(vr) || !isRecognizedVr(vr)) {
|
|
12183
12191
|
cursor.slice(2);
|
|
12184
12192
|
length = cursor.readUInt32();
|
|
12185
12193
|
headerLength = 12;
|
|
@@ -13453,7 +13461,7 @@ function encodeDatasetElement(el, encoding) {
|
|
|
13453
13461
|
}
|
|
13454
13462
|
const littleEndian = encoding === "explicitLE";
|
|
13455
13463
|
const vrBuf = buffer.Buffer.from(el.vr, "ascii");
|
|
13456
|
-
if (LONG_FORM_VRS.has(el.vr)) {
|
|
13464
|
+
if (LONG_FORM_VRS.has(el.vr) || !isRecognizedVr(el.vr)) {
|
|
13457
13465
|
return buffer.Buffer.concat([
|
|
13458
13466
|
uint16(group, littleEndian),
|
|
13459
13467
|
uint16(element, littleEndian),
|
|
@@ -13504,7 +13512,7 @@ function encodeMetaElement(tag, vr, rawValue) {
|
|
|
13504
13512
|
const elementBuf = buffer.Buffer.alloc(2);
|
|
13505
13513
|
elementBuf.writeUInt16LE(element, 0);
|
|
13506
13514
|
const vrBuf = buffer.Buffer.from(vr, "ascii");
|
|
13507
|
-
if (LONG_FORM_VRS.has(vr)) {
|
|
13515
|
+
if (LONG_FORM_VRS.has(vr) || !isRecognizedVr(vr)) {
|
|
13508
13516
|
const lengthBuf2 = buffer.Buffer.alloc(4);
|
|
13509
13517
|
lengthBuf2.writeUInt32LE(value.length, 0);
|
|
13510
13518
|
return buffer.Buffer.concat([
|
|
@@ -14699,7 +14707,6 @@ function uidValueMultiplicity(rawBytes) {
|
|
|
14699
14707
|
}
|
|
14700
14708
|
|
|
14701
14709
|
// src/deident/embedded.ts
|
|
14702
|
-
var KNOWN_VRS2 = new Set(Object.keys(BE_VR_STRIDE));
|
|
14703
14710
|
var UNDEFINED_LENGTH4 = 4294967295;
|
|
14704
14711
|
var FFFE_GROUP = 65534;
|
|
14705
14712
|
var SCANNABLE_CARRIER_VRS = /* @__PURE__ */ new Set([
|
|
@@ -14760,7 +14767,7 @@ function decodeAt(value, off, encoding) {
|
|
|
14760
14767
|
return next2 > value.length ? void 0 : { next: next2, tag };
|
|
14761
14768
|
}
|
|
14762
14769
|
const vr = value.toString("latin1", off + 4, off + 6);
|
|
14763
|
-
if (!
|
|
14770
|
+
if (!KNOWN_VRS.has(vr)) return void 0;
|
|
14764
14771
|
if (LONG_FORM_VRS.has(vr)) {
|
|
14765
14772
|
if (off + 12 > value.length) return void 0;
|
|
14766
14773
|
if (value[off + 6] !== 0 || value[off + 7] !== 0) return void 0;
|
|
@@ -14897,7 +14904,6 @@ var BODY_ENCODING2 = {
|
|
|
14897
14904
|
};
|
|
14898
14905
|
var MAX_UNAUDITABLE_SEQUENCE_FINDINGS = 64;
|
|
14899
14906
|
var MAX_UNDEFINED_VR_FINDINGS = 64;
|
|
14900
|
-
var KNOWN_VRS3 = new Set(Object.keys(BE_VR_STRIDE));
|
|
14901
14907
|
function validateRetain(retain) {
|
|
14902
14908
|
const valid = new Set(DEIDENTIFY_OPTIONS);
|
|
14903
14909
|
const active = /* @__PURE__ */ new Set();
|
|
@@ -15018,7 +15024,7 @@ function actsOnTag(tag, ctx) {
|
|
|
15018
15024
|
return resolveAction(effectiveCode(action, ctx.active)) !== "K";
|
|
15019
15025
|
}
|
|
15020
15026
|
function hasUndefinedVr(el) {
|
|
15021
|
-
return !
|
|
15027
|
+
return !KNOWN_VRS.has(el.vr);
|
|
15022
15028
|
}
|
|
15023
15029
|
function keepOrEmpty(el, ctx, contextPath, out) {
|
|
15024
15030
|
if (hasUndefinedVr(el)) {
|
|
@@ -15082,6 +15088,13 @@ function creatorFor(tag, creators) {
|
|
|
15082
15088
|
const { group, element } = splitTag(tag);
|
|
15083
15089
|
return creators.get(`${String(group)}:${String(element >> 8 & 255)}`);
|
|
15084
15090
|
}
|
|
15091
|
+
var UNDEFINED_LENGTH5 = 4294967295;
|
|
15092
|
+
var EXPLICIT_SQ_HEADER_BYTES = 12;
|
|
15093
|
+
function itemStreamOverrunsSequence(el, encoding) {
|
|
15094
|
+
if (el.vr !== "SQ" || el.length === UNDEFINED_LENGTH5) return false;
|
|
15095
|
+
const headerBytes = encoding === "implicit" ? 0 : EXPLICIT_SQ_HEADER_BYTES;
|
|
15096
|
+
return el.rawBytes.length - headerBytes > el.length;
|
|
15097
|
+
}
|
|
15085
15098
|
function keepsPrivate(el, ctx, creators) {
|
|
15086
15099
|
if (!ctx.active.has("RetainSafePrivate") || ctx.profile === void 0) return false;
|
|
15087
15100
|
if (isPrivateCreatorElement(el.tag)) {
|
|
@@ -15091,7 +15104,7 @@ function keepsPrivate(el, ctx, creators) {
|
|
|
15091
15104
|
if (creator === void 0) return false;
|
|
15092
15105
|
return resolvePrivateTag(ctx.profile, el.tag, creator) !== void 0;
|
|
15093
15106
|
}
|
|
15094
|
-
function processElements(source, ctx, contextPath) {
|
|
15107
|
+
function processElements(source, ctx, contextPath, reservationsUsable) {
|
|
15095
15108
|
const out = {
|
|
15096
15109
|
elements: /* @__PURE__ */ new Map(),
|
|
15097
15110
|
attributes: [],
|
|
@@ -15104,7 +15117,8 @@ function processElements(source, ctx, contextPath) {
|
|
|
15104
15117
|
const creators = creatorsInScope(source);
|
|
15105
15118
|
for (const el of source) {
|
|
15106
15119
|
if (isPrivateTag(el.tag)) {
|
|
15107
|
-
if (keepsPrivate(el, ctx, creators))
|
|
15120
|
+
if (reservationsUsable && keepsPrivate(el, ctx, creators))
|
|
15121
|
+
keepOrEmpty(el, ctx, contextPath, out);
|
|
15108
15122
|
else out.removedPrivateTags.push(el.tag);
|
|
15109
15123
|
continue;
|
|
15110
15124
|
}
|
|
@@ -15112,7 +15126,8 @@ function processElements(source, ctx, contextPath) {
|
|
|
15112
15126
|
if (action === void 0) {
|
|
15113
15127
|
if (el.vr === "SQ") {
|
|
15114
15128
|
if (isUnauditableSequence(el)) emptyUnauditableSequence(el, ctx, contextPath, out);
|
|
15115
|
-
else
|
|
15129
|
+
else
|
|
15130
|
+
out.elements.set(el.tag, descendSequence(el, ctx, contextPath, out, reservationsUsable));
|
|
15116
15131
|
} else {
|
|
15117
15132
|
keepOrEmpty(el, ctx, contextPath, out);
|
|
15118
15133
|
}
|
|
@@ -15120,7 +15135,7 @@ function processElements(source, ctx, contextPath) {
|
|
|
15120
15135
|
}
|
|
15121
15136
|
const resolved = resolveAction(effectiveCode(action, ctx.active));
|
|
15122
15137
|
if (el.vr === "SQ") {
|
|
15123
|
-
applySequenceAction(el, resolved, action, ctx, contextPath, out);
|
|
15138
|
+
applySequenceAction(el, resolved, action, ctx, contextPath, out, reservationsUsable);
|
|
15124
15139
|
continue;
|
|
15125
15140
|
}
|
|
15126
15141
|
let applied;
|
|
@@ -15161,11 +15176,12 @@ function processElements(source, ctx, contextPath) {
|
|
|
15161
15176
|
}
|
|
15162
15177
|
return out;
|
|
15163
15178
|
}
|
|
15164
|
-
function descendSequence(el, ctx, contextPath, out) {
|
|
15179
|
+
function descendSequence(el, ctx, contextPath, out, reservationsUsable) {
|
|
15180
|
+
const childReservationsUsable = reservationsUsable && !itemStreamOverrunsSequence(el, ctx.encoding);
|
|
15165
15181
|
const newItems = [];
|
|
15166
15182
|
(el.items ?? []).forEach((item, index) => {
|
|
15167
15183
|
const childPath = [...contextPath, `${el.tag}[${String(index)}]`];
|
|
15168
|
-
const inner = processElements(item.elements(), ctx, childPath);
|
|
15184
|
+
const inner = processElements(item.elements(), ctx, childPath, childReservationsUsable);
|
|
15169
15185
|
out.attributes.push(...inner.attributes);
|
|
15170
15186
|
out.removedPrivateTags.push(...inner.removedPrivateTags);
|
|
15171
15187
|
out.embeddedAttributes.push(...inner.embeddedAttributes);
|
|
@@ -15176,7 +15192,7 @@ function descendSequence(el, ctx, contextPath, out) {
|
|
|
15176
15192
|
});
|
|
15177
15193
|
return rebuildSequence(el, newItems, ctx.encoding);
|
|
15178
15194
|
}
|
|
15179
|
-
function applySequenceAction(el, resolved, action, ctx, contextPath, out) {
|
|
15195
|
+
function applySequenceAction(el, resolved, action, ctx, contextPath, out, reservationsUsable) {
|
|
15180
15196
|
let applied;
|
|
15181
15197
|
switch (resolved) {
|
|
15182
15198
|
case "X":
|
|
@@ -15192,7 +15208,7 @@ function applySequenceAction(el, resolved, action, ctx, contextPath, out) {
|
|
|
15192
15208
|
emptyUnauditableSequence(el, ctx, contextPath, out);
|
|
15193
15209
|
applied = "emptied";
|
|
15194
15210
|
} else {
|
|
15195
|
-
out.elements.set(el.tag, descendSequence(el, ctx, contextPath, out));
|
|
15211
|
+
out.elements.set(el.tag, descendSequence(el, ctx, contextPath, out, reservationsUsable));
|
|
15196
15212
|
applied = "cleaned";
|
|
15197
15213
|
}
|
|
15198
15214
|
break;
|
|
@@ -15202,7 +15218,7 @@ function applySequenceAction(el, resolved, action, ctx, contextPath, out) {
|
|
|
15202
15218
|
emptyUnauditableSequence(el, ctx, contextPath, out);
|
|
15203
15219
|
applied = "emptied";
|
|
15204
15220
|
} else {
|
|
15205
|
-
out.elements.set(el.tag, descendSequence(el, ctx, contextPath, out));
|
|
15221
|
+
out.elements.set(el.tag, descendSequence(el, ctx, contextPath, out, reservationsUsable));
|
|
15206
15222
|
applied = "kept";
|
|
15207
15223
|
}
|
|
15208
15224
|
break;
|
|
@@ -15253,7 +15269,7 @@ function deidentify(ds, options = {}) {
|
|
|
15253
15269
|
encoding,
|
|
15254
15270
|
budget: { unauditableSequences: 0, undefinedVrElements: 0 }
|
|
15255
15271
|
};
|
|
15256
|
-
const processed = processElements(ds.elements(), ctx, []);
|
|
15272
|
+
const processed = processElements(ds.elements(), ctx, [], true);
|
|
15257
15273
|
const {
|
|
15258
15274
|
elements,
|
|
15259
15275
|
attributes,
|