@cosyte/dicom 0.0.6 → 0.0.7

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 CHANGED
@@ -6,6 +6,182 @@ All notable changes to `@cosyte/dicom` will be documented in this file. The form
6
6
 
7
7
  ### Security
8
8
 
9
+ - **An element whose on-wire VR is not one of the 34 PS3.5 §6.2 defines was kept verbatim by
10
+ `deidentify()`, carrying a source `(0010,0020)` Patient ID into de-identified output next to the
11
+ `(0012,0062) PatientIdentityRemoved = YES` this library writes** (`DICOM-CARRIER-LEAF-LEAKS`,
12
+ mechanism 2; pre-existing, live on the published `0.0.6`, identical on both trees). Re-derived on
13
+ `scripts/measure-sq-bound-grid.ts` at `35adc2d` before anything changed: **19 leaking cells → 11**.
14
+ Negative control first - the grid run against `d1031f5`'s `src/` restored **1,174** leaking cells
15
+ and reproduced `#54`'s published 2,448-cell cost - plus a second control confirming the harness
16
+ fails outright when pointed at another package.
17
+
18
+ **This is the _under_-declare, not the swallow the entry below closed.** An under-declared Value
19
+ Length desynchronizes the reader: it finishes the short value early, reads the leftover bytes of
20
+ the value that was actually encoded as the next Data Element header, and consumes the element that
21
+ genuinely followed as that fabricated element's "value". Measured: a 14-byte carrier
22
+ under-declaring by 6 yields tag `(4156,554C)` with the VR bytes `"E "`, holding the Patient ID in
23
+ full, **silently** and with a clean report. It reaches **string** carriers as readily as binary
24
+ ones, so it is not bounded by the binary-VR story that frames the residual below.
25
+
26
+ **The remedy reads a field the parser already recorded, not the bytes.** PS3.5 2026c §6.2: "All
27
+ new VRs defined in future versions of DICOM shall be of the same Data Element Structure as defined
28
+ in [§7.1.2] with reserved bytes after the VR and a 32-bit unsigned integer VL". An unrecognized VR
29
+ is long-form by that rule while this parser reads it short-form (Postel's Law on the read path), so
30
+ its bytes are not a Value Field this library decoded under any VR and PS3.15 2026c §E.1.1's
31
+ obligation cannot be discharged inside them. Read from the vendored SHA-pinned documents, pins
32
+ re-derived, each sentence unique in its document. Such an element is **emptied**,
33
+ `report.undefinedVrElements` names the **byte offset** and the byte length dropped, and the new
34
+ `DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE` is raised. **The finding names no tag, uniquely among
35
+ the report's findings**: the header _may_ have been fabricated out of the middle of some element's
36
+ value, in which case its four tag bytes are document content - on a synthetic `ST` carrier holding
37
+ `"MR BRAIN SMITHSON"` the tag renders as `48544F53`, four letters of the surname. An honestly
38
+ written unrecognized VR raises the same code with an ordinary tag, and the two are
39
+ indistinguishable here, so the tag is withheld either way.
40
+ `renderTag` shape-checks a tag and cannot refuse one, so the withholding happens at the call site.
41
+ An undefined-VR carrier whose bytes happened to tile was reported in `report.embeddedAttributes`
42
+ before and now reports here instead. The test is a set-membership check on
43
+ `el.vr`: O(1), **no scan**. The record is **capped at 64 across the run** and the emptying never is
44
+ - an undefined-VR element costs an attacker only an 8-byte header, so 1 MiB is 131,072 of them.
45
+
46
+ **No carve-out, and structurally so:** `keepOrEmpty` is the only path that keeps a source value
47
+ verbatim and the test sits at its top, so `RetainSafePrivate` does not exempt an element - pinned
48
+ by a test. **`UN` is untouched**, being one of the 34, and the rule cannot fire under Implicit VR
49
+ LE at all, where the VR comes from the dictionary; 0 Implicit VR LE cells moved.
50
+
51
+ **Cost, published rather than described:** 23 grid cells lose a marker from de-identified output,
52
+ **15 of which were not leaking**. On a file conformant to PS3.5 2026c the cost is zero.
53
+
54
+ **The root cause is a parse behaviour this slice does not touch, and it is disclosed rather than
55
+ implied fixed.** §6.2's note says informatively that an unrecognized VR may be handled "by applying
56
+ the rules stated in [§7.1.2]" - i.e. read long-form, value copied unchanged. This parser reads it
57
+ **short-form**. Emptying at the de-identify boundary is therefore compensation, not conformance.
58
+ `PRE-EXISTING`, its own slice. No claim is made here about what a §6.2-conformant future-VR file
59
+ does on either tree.
60
+
61
+ **Still leaking, measured, and now priced:** the 11 remaining cells are the over-declare swallow
62
+ into a **binary** carrier at `delta=18`. The one candidate remedy was built and measured - it
63
+ takes 11 to 0 and empties **all 5** conformant binary tiling controls, i.e. deletes a legal
64
+ `OB`/`UN` value because 8 of its bytes read as a zero-length `(0010,0020)`. That is a product
65
+ decision, not a bug fix, and it has its own item.
66
+
67
+ - **A sequence the parser could not open was passed through `deidentify()` as raw bytes, so a
68
+ `(0010,0020)` Patient ID inside it reached de-identified output next to the
69
+ `(0012,0062) PatientIdentityRemoved = YES` this library writes** (`DICOM-DEIDENT-RAWBYTES-PASSTHROUGH`,
70
+ pre-existing and live on the published `0.0.6`). It is the larger half of the 2,127 leaking grid
71
+ cells the previous entry decomposed: **1,155 of the 6,348 cells that parse**, all Implicit VR LE,
72
+ all carrying exactly `["DICOM_SQ_NOT_DESCENDED"]`. Re-measured before anything was changed, with a
73
+ negative control (reverting the previous entry's call site restored exactly 2,127) so the harness
74
+ is known live rather than assumed.
75
+
76
+ When an element resolves to `SQ` from PS3.6 under Implicit VR LE and its defined-length value is
77
+ not a valid `(FFFE,E000)` item stream, the parser refuses the descent and keeps the declared span
78
+ on `Element.rawBytes` with `Element.items` undefined. That is right for a parser. It was not safe
79
+ for `deidentify()`, which recurses only into a sequence whose items exist: for a carrier such as
80
+ `(0008,1115)` with no Table E.1-1 row, the action table resolved "keep" and the span was re-emitted
81
+ verbatim, with the report naming nothing.
82
+
83
+ **The remedy reads the parser's recorded refusal, not the bytes.** PS3.5 2026c §7.5.1 "Item
84
+ Encoding Rules" states "Each Item Value shall contain a DICOM Data Set composed of Data Elements",
85
+ so an `SQ` value is never legitimately opaque the way an `OB` value is; PS3.15 2026c §E.1.1
86
+ "De-identifier" obliges an implementation claiming the Basic Profile to "protect or retain all
87
+ instances of the Attributes listed in [Table E.1-1], whether contained in the top level Data Set or
88
+ embedded in an Item of a Sequence of Items". Unable to enumerate the items, the obligation falls on
89
+ the carrier, which is the escalation §E.1.1 itself makes for a SOP Instance UID inside a Sequence.
90
+ Both sentences read from the vendored SHA-pinned documents, pins re-derived, each unique in its
91
+ document. A **standard** `SQ` with no items is therefore **emptied**,
92
+ `report.unauditableSequences` names the tag and the byte length dropped, and the new
93
+ `DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE` is raised. That record is **capped at 64 across the run**
94
+ (element count is attacker-chosen too, and `#48` bound every other consumer-controlled
95
+ diagnostic); the **emptying is never capped**, so an array exactly 64 long means "at least 64". A
96
+ listed sequence kept by a Retain Option takes the same branch and its audit line now reads
97
+ `emptied` rather than `kept` - previously it produced an empty sequence anyway while claiming the
98
+ attribute was retained.
99
+
100
+ **It costs content, and the number is published rather than described:** 2,448 grid cells lose a
101
+ value from their de-identified output, of which **1,293 were not leaking anything** and pay purely
102
+ for the guarantee. The accompanying `DICOM_SQ_NOT_DESCENDED` says why the parse refused, which is
103
+ _usually_ a sender defect - but **not always**: a conformant file nested deeper than this
104
+ library's own `NESTING_DEPTH_LIMIT` of 64 (ours; PS3.5 sets no nesting bound) is refused the same
105
+ way and loses that sequence too. Because the trigger is `items === undefined` rather than a
106
+ content test there is no scan, so no cost follows an attacker-chosen value length; the cost tests
107
+ use a carrier where **every even offset** is a tiling candidate and assert that property rather
108
+ than describing it, as a **forward tripwire** - this path never traverses those bytes.
109
+
110
+ **New surface:** `UnauditableSequenceFinding`, `DeidentifyReport.unauditableSequences`, and
111
+ `DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE` (**27 Tier-2 codes, was 26**; snapshot updated
112
+ deliberately). `DICOM_SQ_NOT_DESCENDED`'s message changed, since it said nested content was
113
+ invisible to de-identification.
114
+
115
+ **Not covered, and one of these is measured here for the first time.** An undefined-length `UN`
116
+ whose CP-246 descent was refused keeps `vr === "UN"`, and the rule cannot be extended to it because
117
+ every ordinary `UN` element also has no items - measured on a hand-built file, the identifier still
118
+ reaches output with no report entry. A **private** `SQ` under `RetainSafePrivate` plus a `Profile`
119
+ is still kept verbatim, deliberately - `keepsPrivate` decides first, so it never reaches this rule,
120
+ and measured on a synthetic vendor block the identifier does still reach output. Both carve-outs
121
+ are pinned by tests. And the **binary-VR carrier** residual the previous entry
122
+ disclosed but could not measure now has a number: the grid gained an over-declaring **leaf**
123
+ carrier dimension and finds **19 leaking cells, identical on both trees** - 11 at `delta=18` (the
124
+ swallow into `OB`/`OW`/`US`/`UN`, silent, with `LO`/`ST` controls on the identical fixture at 0)
125
+ and 8 at `delta=-6` (an _under_-declare, where leftover bytes are read as a Data Element header and
126
+ the identifier lands in a manufactured element with an unknown on-wire VR; this one hits string
127
+ carriers too and was not previously disclosed anywhere).
128
+
129
+ **Measured, on the now 76,599-cell grid against `d1031f5`:** sequence-sweep leaks 1,155 → **0**;
130
+ **0 cells differing in any parse respect**, now a printed count rather than an inference from
131
+ `changed`, because no parser file is touched; 0 new fatals, 0 lost parse values, 0 reports that
132
+ lose an attribute, 0 new strict fatals.
133
+
134
+ - **An element that over-declared its own Value Length swallowed the next element into its value,
135
+ where the PS3.15 Annex E action table cannot see it, and `deidentify()` wrote the identifier into
136
+ its output with a clean report** (`DICOM-OVERDECLARE-SWALLOWS-INTO-VALUE`, pre-existing and live
137
+ on the published `0.0.6`). **No sequence is involved.** PS3.5 2026c defines Value Length as "The
138
+ length of the Value Field of the Data Element" - that element's own value. A sender that writes a
139
+ larger number produces a file that is not detectably broken: the reader consumes the declared
140
+ count, the bytes it over-consumes are the following element header and all, every subsequent
141
+ offset still lines up, and nothing on the wire says which length field lied. After the swallow
142
+ there is no `(0010,0020)` in the object for Table E.1-1 to match, only bytes inside some other
143
+ attribute's value, so de-identification passed over PHI it never recognised as an attribute.
144
+ Measured on the committed grid at `244a372`: **877 of the 6,348 cells that parse** wrote a source
145
+ value into de-identified output this way, on Explicit VR LE and Explicit VR BE, **871 of them with
146
+ no warning on either channel and no throw under `{ strict: true }`**.
147
+
148
+ **`parseDicom` is unchanged, deliberately.** The two readings - "the length is right and the value
149
+ is odd" and "the length lied and the next element was absorbed" - produce identical bytes, so a
150
+ parser cannot choose between them and this release does not pretend to. `deidentify()` answers a
151
+ strictly narrower question, and only about values it was about to **keep**: does this value's tail
152
+ decode, in the file's own transfer syntax, as a complete run of Data Elements ending exactly at the
153
+ end of the value, at least one of which this run would have acted on, and containing a byte the
154
+ carrier's VR cannot legally hold (PS3.5 §6.1.3 and Table 6.1-1 permit five C0 control characters in
155
+ DICOM text; **Table 6.2-1 decides which of the five each VR may hold**, in three tiers)? All three have to hold. If they do, the value is **emptied**
156
+ rather than kept, a new `DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED` warning appears on
157
+ `report.warnings`, and the new `report.embeddedAttributes` names the carrier, its VR and the tags
158
+ that were hiding inside it. PS3.15 §E.1 requires a de-identifier to "protect or retain all
159
+ instances of the Attributes listed in [Table E.1-1]"; §E.3.5 is the standard's own statement that
160
+ identifying information embedded **inside a string attribute's value** is in scope for removal.
161
+ Emptying is the fail-safe direction; keeping is not.
162
+
163
+ **What this does not cover, stated rather than implied.** Carriers are **string VRs only**, and the
164
+ residual is live rather than theoretical: the identical over-declare into an `OB`, `OW`, `UN` or
165
+ `US` carrier still writes the identifier into de-identified output, with **no warning and no report
166
+ entry**, exactly as before. Arbitrary bytes are what those VRs are for, so no content test can tell
167
+ a swallow from a legitimate value there, and scanning them would trade a measured guarantee for a
168
+ coin-flip that deletes real pixel and lookup-table data. The committed grid never puts a binary VR
169
+ in the over-declaring role, so this residual is **disclosed but unmeasured** and needs its own
170
+ item. (It was measured later in this same release cycle, by the entry above: **19 grid cells**,
171
+ once the grid gained an over-declaring leaf carrier.) A sequence the
172
+ parser declined to descend and kept as opaque bytes is a **different** defect with a different
173
+ remedy and is untouched here (1,155 grid cells, all Implicit VR LE, all carrying
174
+ `DICOM_SQ_NOT_DESCENDED`). And nothing here recovers the correct reading of a malformed file: the
175
+ carrier's parsed value still holds the absorbed bytes, and `ds.get("00100020")` still returns
176
+ nothing. This closes the leak, not the mis-read.
177
+
178
+ **Measured, on the same 76,293-cell grid, against `244a372`:** 2,127 leaking cells → 1,155; every
179
+ Explicit VR cell → **0**; and **0 cells whose parse differs in any respect** - reading, both
180
+ warning channels, `{ strict: true }`, and which values survive anywhere in the object are
181
+ byte-identical, because no parser file is touched. 0 new fatals, 0 lost values, 0 reports that lose
182
+ an attribute. `scripts/measure-sq-bound-grid.ts`, previously only on an unmerged branch, is
183
+ committed so every figure above is one command away.
184
+
9
185
  - **A defined-length sequence under Implicit VR Little Endian was never opened, so PHI nested inside
10
186
  one survived `deidentify()` into serialized output while the report said it had been removed**
11
187
  (`DICOM-IMPLICIT-SQ-NOT-DESCENDED`, pre-existing and live on the published `0.0.5`). The
@@ -249,6 +425,18 @@ All notable changes to `@cosyte/dicom` will be documented in this file. The form
249
425
 
250
426
  ### Added
251
427
 
428
+ - **`UndefinedVrFinding`, `DeidentifyReport.undefinedVrElements` and
429
+ `DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE`** (**28 Tier-2 codes, was 27**; the locked
430
+ `WARNING_CODES` snapshot was updated deliberately). `MAX_UNDEFINED_VR_FINDINGS` (64) caps the
431
+ record, never the emptying.
432
+
433
+ - **A conformant-tiling control family in `scripts/measure-sq-bound-grid.ts`**
434
+ (`LEGIT_TILING_CARRIERS`) plus the `conformant tiling control emptied` counters in `--diff`. It
435
+ exists to price the remaining binary-carrier leak, which `LEAKING` structurally cannot: a rule that
436
+ empties every binary value whose tail tiles reads as `11 → 0` there and says nothing about what it
437
+ destroyed. Restricted to stride-0 VRs, because `buildDicom` byte-swaps `OW`/`US` values under
438
+ Explicit VR BE - a first draft included them and read 9 emptied rows where the honest number was 6.
439
+
252
440
  - **`DICOM_SQ_NOT_DESCENDED`**, a Tier-2 warning code (25 total, was 24). Public surface: the locked
253
441
  `WARNING_CODES` snapshot was updated deliberately, and a profile may `escalate` or `suppress` it
254
442
  like any other code.
package/README.md CHANGED
@@ -207,6 +207,8 @@ const safe = serializeDicom(dataset); // safe to share: input dataset never muta
207
207
 
208
208
  report.attributes.length; // count of attributes acted on (each carries tag/keyword/action, no values)
209
209
  report.warnings; // e.g. DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED
210
+ report.unauditableSequences; // SQ elements emptied because their items could not be walked
211
+ report.undefinedVrElements; // elements emptied because their on-wire VR is not a VR
210
212
  ```
211
213
 
212
214
  UIDs are remapped to deterministic `2.25` replacements that stay consistent across files, so a de-identified study still hangs together. Opt into any of the nine metadata-affecting Annex E Options to keep specific classes of attribute:
@@ -316,7 +318,7 @@ for (const w of ds.warnings) {
316
318
  }
317
319
  ```
318
320
 
319
- The 25 Tier-2 codes (`DICOM_MISSING_PREAMBLE`, `DICOM_FILE_META_GROUP_LENGTH_MISMATCH`, `DICOM_UN_PARSED_AS_SQ`, `DICOM_ODD_LENGTH_VALUE_PADDED`, `DICOM_PRIVATE_CREATOR_UNKNOWN`, `DICOM_VR_MISMATCH`, `DICOM_DA_LEGACY_FORMAT`, … ) live in [`src/parser/warnings.ts`](./src/parser/warnings.ts). Narrow on `w.code === WARNING_CODES.…` for typo-free comparisons, or pass `{ onWarning }` to `parseDicom` to stream them.
321
+ The 26 Tier-2 codes (`DICOM_MISSING_PREAMBLE`, `DICOM_FILE_META_GROUP_LENGTH_MISMATCH`, `DICOM_UN_PARSED_AS_SQ`, `DICOM_ODD_LENGTH_VALUE_PADDED`, `DICOM_PRIVATE_CREATOR_UNKNOWN`, `DICOM_VR_MISMATCH`, `DICOM_DA_LEGACY_FORMAT`, … ) live in [`src/parser/warnings.ts`](./src/parser/warnings.ts). Narrow on `w.code === WARNING_CODES.…` for typo-free comparisons, or pass `{ onWarning }` to `parseDicom` to stream them.
320
322
 
321
323
  The 4 Tier-3 fatal codes (`NOT_DICOM_PART_10`, `INVALID_FILE_META`, `UNSUPPORTED_TRANSFER_SYNTAX`, `EMPTY_INPUT`) always throw a `DicomParseError`; they represent input the parser cannot meaningfully recover.
322
324
 
@@ -362,7 +364,9 @@ Thrown by `serializeDicom` for `MISSING_TRANSFER_SYNTAX` (the dataset names no t
362
364
 
363
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`.
364
366
  - **Burned-in PHI.** v1 **warns** it cannot remove burned-in annotation; a "de-identified" output is **metadata-de-identified only**.
365
- - **A sequence the parser could not open.** `deidentify()` recurses only into a sequence whose items the parser materialized, so anything nested inside one it could not read is neither cleaned nor listed in the report. Two shapes reach that state and **only one of them warns**: a defined-length Implicit VR LE value whose dictionary-resolved `SQ` is not an item stream raises `DICOM_SQ_NOT_DESCENDED`, while an undefined-length `UN` value the CP-246 descent could not read as a sequence is **silent** (it keeps `vr === "UN"` and raises nothing). So `ds.warnings` is worth reading but is not sufficient: the reliable test is `el.items === undefined` on an `SQ` or `UN` element you are trusting the report about.
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 element whose on-wire VR is not a VR: the value is dropped, not passed through.** Under an Explicit VR syntax the VR is two bytes the sender wrote and this parser trusts them. If those two bytes are not one of the 34 PS3.5 §6.2 defines, `deidentify()` **empties** the element: §6.2 requires every undefined VR to be long-form with a 32-bit VL, this parser reads it short-form, so those bytes are not a Value Field the library decoded and PS3.15 §E.1.1's obligation cannot be discharged inside them. `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: this header _may_ 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. The usual cause is an **under**-declared Value Length earlier in the file, which desynchronizes the reader so that leftover value bytes are read as a Data Element header - and the element that genuinely followed becomes the fabricated element's value, which is how a `(0010,0020)` Patient ID used to reach de-identified output. **A file conformant to PS3.5 2026c never trips this**, and an Implicit VR LE file cannot: there the VR comes from the dictionary. (The edition matters: §6.2 exists precisely to say how a _future_ VR will be encoded.) `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
+ - **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.
366
370
  - **Networking & web.** No DIMSE (C-STORE/FIND/MOVE, MWL, MPPS); no DICOMweb (QIDO/WADO/STOW). → `@cosyte/dicom-net`, `@cosyte/dicomweb`.
367
371
  - **Transcoding.** No transfer-syntax conversion. The serializer re-emits in the dataset's source syntax only.
368
372
  - **Terminology resolution.** Coded values are surfaced (designator + canonical source) but not validated against SNOMED/LOINC/etc.
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.6";
14
+ var VERSION = "0.0.7";
15
15
 
16
16
  // src/dictionary/index.ts
17
17
  var dictionary_exports = {};
@@ -11735,6 +11735,12 @@ var WARNING_CODES = {
11735
11735
  // === Reserved by later phases (declared, not emitted in Phase 2) ===
11736
11736
  DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED: "DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED",
11737
11737
  // reserved by Phase 7 - not emitted in Phase 2
11738
+ DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED: "DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED",
11739
+ // emitted by deidentify(), never by the parser
11740
+ DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE: "DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE",
11741
+ // emitted by deidentify(), never by the parser
11742
+ DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE: "DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE",
11743
+ // emitted by deidentify(), never by the parser
11738
11744
  DICOM_PRIVATE_CREATOR_UNKNOWN: "DICOM_PRIVATE_CREATOR_UNKNOWN"
11739
11745
  // reserved by Phase 6 - not emitted in Phase 2
11740
11746
  };
@@ -11748,13 +11754,30 @@ var WARNING_MESSAGES = Object.freeze({
11748
11754
  DICOM_PRIVATE_TAG_NO_CREATOR: "Private element ({tag}) has no Private Creator registered for its block; treating as VR=UN.",
11749
11755
  DICOM_GROUP_LENGTH_IN_DATASET: "Retired Group Length element ({tag}) encountered in dataset; preserved as-is.",
11750
11756
  DICOM_NONZERO_RESERVED_BYTES: "Element ({tag}) has non-zero reserved bytes between VR and length (first byte {n}, second byte {n2}); ignoring.",
11751
- 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 or de-identification.",
11757
+ 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.",
11752
11758
  DICOM_UN_PARSED_AS_SQ: "Element ({tag}) has VR=UN with undefined length; descended as Implicit VR LE sequence per CP-246.",
11753
11759
  DICOM_EMPTY_ITEM_IN_SEQUENCE: "Sequence ({tag}) contains an empty item (length=0); tolerated.",
11754
11760
  DICOM_PIXEL_DATA_LENGTH_MISMATCH: "(7FE0,0010) PixelData declared length {n} does not match computed {n2} bytes.",
11755
11761
  DICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR: "Private element ({tag}) under Implicit VR LE has no VR override; falling back to UN.",
11756
11762
  DICOM_PRIVATE_CREATOR_UNKNOWN: "Private element ({tag}) has a Private Creator the active profile's private dictionary does not name; falling back to UN. The creator string is not reproduced here - read the (gggg,00EE) element if you need it.",
11757
11763
  DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED: "Pixel Data is present and Burned In Annotation is not 'NO'; this metadata-only de-identifier cannot inspect or clean pixels. Recognizable text may remain burned into the image.",
11764
+ DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED: "Element ({tag}) {vr} was kept by the action table, but its value ends with {n} whole Data Element(s) - an over-declared Value Length swallowed what followed it. Emptied rather than kept, because the action table cannot see an attribute encoded inside a value. Read report.embeddedAttributes for the tags that were hidden there.",
11765
+ // Deliberately short. One of these is raised per un-auditable element, so a
11766
+ // long message is multiplied by an element count the input controls; the
11767
+ // reasoning belongs in the docs, not in a string repeated thousands of times.
11768
+ DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE: "Element ({tag}) is VR=SQ with no parsed items, so its {n} value bytes could not be audited (PS3.15 E.1.1); emptied. See report.unauditableSequences.",
11769
+ // Deliberately short for the same reason as the code above: one per element,
11770
+ // and the element count is chosen by the input.
11771
+ //
11772
+ // NEITHER THE TAG NOR THE VR IS ECHOED, and that is specific to this code
11773
+ // rather than caution. Every other factory's {tag} is composed from a real
11774
+ // Data Element header; the condition that raises THIS one is that the header
11775
+ // was fabricated from bytes inside some element's value, so its four tag bytes
11776
+ // and its two VR bytes are document content. `renderTag` shape-checks a tag
11777
+ // and cannot refuse one, so the withholding has to happen at the call site.
11778
+ // The byte offset locates the element instead, and is a position this parser
11779
+ // counted rather than anything the document said.
11780
+ DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE: "An element at byte offset {n2} carries an on-wire VR that is not one of the 34 PS3.5 6.2 defines, so its {n} value bytes are not a Value Field this library decoded; emptied (PS3.15 E.1.1). Its tag and VR are withheld: an earlier under-declared length can make them fragments of some element's value. See report.undefinedVrElements.",
11758
11781
  DICOM_BOM_IN_TEXT_VR: "Element ({tag}) {vr} value begins with a UTF-8 BOM; stripped on decode.",
11759
11782
  DICOM_TRAILING_NULL_IN_TEXT_VR: "Element ({tag}) {vr} value has a trailing NULL pad where SPACE is expected; trimmed.",
11760
11783
  DICOM_UI_TRAILING_SPACE: "Element ({tag}) UI value is SPACE-padded; UI requires NULL padding (PS3.5 \xA76.2). Trimmed.",
@@ -11832,6 +11855,25 @@ function privateCreatorUnknown(position, tag) {
11832
11855
  function burnedInAnnotationNotRemoved(position) {
11833
11856
  return build(WARNING_CODES.DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED, position);
11834
11857
  }
11858
+ function embeddedAttributeRemoved(position, tag, vr, count) {
11859
+ return build(WARNING_CODES.DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED, position, {
11860
+ tag,
11861
+ vr,
11862
+ n: count
11863
+ });
11864
+ }
11865
+ function sequenceNotAuditable(position, tag, byteLength) {
11866
+ return build(WARNING_CODES.DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE, position, {
11867
+ tag,
11868
+ n: byteLength
11869
+ });
11870
+ }
11871
+ function undefinedVrNotAuditable(position, byteLength) {
11872
+ return build(WARNING_CODES.DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE, position, {
11873
+ n: byteLength,
11874
+ n2: position.byteOffset
11875
+ });
11876
+ }
11835
11877
  function bomInTextVR(position, tag, vr) {
11836
11878
  return build(WARNING_CODES.DICOM_BOM_IN_TEXT_VR, position, { tag, vr });
11837
11879
  }
@@ -14656,6 +14698,132 @@ function uidValueMultiplicity(rawBytes) {
14656
14698
  return raw.split("\\").length;
14657
14699
  }
14658
14700
 
14701
+ // src/deident/embedded.ts
14702
+ var KNOWN_VRS2 = new Set(Object.keys(BE_VR_STRIDE));
14703
+ var UNDEFINED_LENGTH4 = 4294967295;
14704
+ var FFFE_GROUP = 65534;
14705
+ var SCANNABLE_CARRIER_VRS = /* @__PURE__ */ new Set([
14706
+ "AE",
14707
+ "AS",
14708
+ "CS",
14709
+ "DA",
14710
+ "DS",
14711
+ "DT",
14712
+ "IS",
14713
+ "LO",
14714
+ "LT",
14715
+ "PN",
14716
+ "SH",
14717
+ "ST",
14718
+ "TM",
14719
+ "UC",
14720
+ "UI",
14721
+ "UR",
14722
+ "UT"
14723
+ ]);
14724
+ var ALLOWED_CONTROL_BYTES = /* @__PURE__ */ new Set([
14725
+ 9,
14726
+ // TAB
14727
+ 10,
14728
+ // LF
14729
+ 12,
14730
+ // FF
14731
+ 13,
14732
+ // CR
14733
+ 27
14734
+ // ESC
14735
+ ]);
14736
+ var CONTROL_TOLERANT_VRS = /* @__PURE__ */ new Set(["LT", "ST", "UT"]);
14737
+ var ESC_ONLY_VRS = /* @__PURE__ */ new Set(["LO", "SH", "UC", "PN"]);
14738
+ var MAX_SCAN_BYTES = 1 << 20;
14739
+ var MIN_HEADER = 8;
14740
+ function readU16(buf, off, littleEndian) {
14741
+ return littleEndian ? buf.readUInt16LE(off) : buf.readUInt16BE(off);
14742
+ }
14743
+ function readU32(buf, off, littleEndian) {
14744
+ return littleEndian ? buf.readUInt32LE(off) : buf.readUInt32BE(off);
14745
+ }
14746
+ function decodeAt(value, off, encoding) {
14747
+ if (off + MIN_HEADER > value.length) return void 0;
14748
+ const littleEndian = encoding !== "explicitBE";
14749
+ const group = readU16(value, off, littleEndian);
14750
+ const element = readU16(value, off + 2, littleEndian);
14751
+ if (group === FFFE_GROUP) {
14752
+ return { next: off + 8, tag: void 0 };
14753
+ }
14754
+ if (group === 0 && element === 0) return void 0;
14755
+ const tag = joinTag(group, element);
14756
+ if (encoding === "implicit") {
14757
+ const length2 = readU32(value, off + 4, littleEndian);
14758
+ if (length2 === UNDEFINED_LENGTH4 || length2 % 2 !== 0) return void 0;
14759
+ const next2 = off + 8 + length2;
14760
+ return next2 > value.length ? void 0 : { next: next2, tag };
14761
+ }
14762
+ const vr = value.toString("latin1", off + 4, off + 6);
14763
+ if (!KNOWN_VRS2.has(vr)) return void 0;
14764
+ if (LONG_FORM_VRS.has(vr)) {
14765
+ if (off + 12 > value.length) return void 0;
14766
+ if (value[off + 6] !== 0 || value[off + 7] !== 0) return void 0;
14767
+ const length2 = readU32(value, off + 8, littleEndian);
14768
+ if (length2 === UNDEFINED_LENGTH4 || length2 % 2 !== 0) return void 0;
14769
+ const next2 = off + 12 + length2;
14770
+ return next2 > value.length ? void 0 : { next: next2, tag };
14771
+ }
14772
+ const length = readU16(value, off + 6, littleEndian);
14773
+ if (length % 2 !== 0) return void 0;
14774
+ const next = off + 8 + length;
14775
+ return next > value.length ? void 0 : { next, tag };
14776
+ }
14777
+ function isScannableCarrier(vr) {
14778
+ return SCANNABLE_CARRIER_VRS.has(vr);
14779
+ }
14780
+ function hasByteOutsideRepertoire(region, carrierVr) {
14781
+ const allFive = CONTROL_TOLERANT_VRS.has(carrierVr);
14782
+ const escOnly = ESC_ONLY_VRS.has(carrierVr);
14783
+ for (const byte of region) {
14784
+ if (byte >= 32 && byte !== 127) continue;
14785
+ if (allFive && ALLOWED_CONTROL_BYTES.has(byte)) continue;
14786
+ if (escOnly && byte === 27) continue;
14787
+ return true;
14788
+ }
14789
+ return false;
14790
+ }
14791
+ function findEmbeddedAttributes(value, carrierVr, encoding, isActionable) {
14792
+ if (!isScannableCarrier(carrierVr)) return void 0;
14793
+ if (value.length < MIN_HEADER) return void 0;
14794
+ const windowStart = value.length > MAX_SCAN_BYTES ? value.length - MAX_SCAN_BYTES : 0;
14795
+ const window = value.subarray(windowStart);
14796
+ const n = window.length;
14797
+ const half = (n >> 1) + 1;
14798
+ const tiles = new Uint8Array(half);
14799
+ const hit = new Uint8Array(half);
14800
+ for (let off = n - MIN_HEADER & -2; off >= 0; off -= 2) {
14801
+ const decoded = decodeAt(window, off, encoding);
14802
+ if (decoded === void 0) continue;
14803
+ const atEnd = decoded.next === n;
14804
+ const onward = decoded.next < n ? tiles[decoded.next >> 1] === 1 : false;
14805
+ if (!atEnd && !onward) continue;
14806
+ tiles[off >> 1] = 1;
14807
+ const actionable = decoded.tag !== void 0 && isActionable(decoded.tag);
14808
+ const onwardHit = decoded.next < n ? hit[decoded.next >> 1] === 1 : false;
14809
+ if (actionable || onwardHit) hit[off >> 1] = 1;
14810
+ }
14811
+ for (let off = 0; off + MIN_HEADER <= n; off += 2) {
14812
+ if (hit[off >> 1] !== 1) continue;
14813
+ if (!hasByteOutsideRepertoire(window.subarray(off), carrierVr)) return void 0;
14814
+ const tags = [];
14815
+ let pos = off;
14816
+ while (pos < n) {
14817
+ const decoded = decodeAt(window, pos, encoding);
14818
+ if (decoded === void 0) break;
14819
+ if (decoded.tag !== void 0) tags.push(decoded.tag);
14820
+ pos = decoded.next;
14821
+ }
14822
+ return tags;
14823
+ }
14824
+ return void 0;
14825
+ }
14826
+
14659
14827
  // src/deident/types.ts
14660
14828
  var DEIDENTIFY_OPTIONS = Object.freeze([
14661
14829
  "CleanGraphics",
@@ -14727,6 +14895,9 @@ var BODY_ENCODING2 = {
14727
14895
  "1.2.840.10008.1.2.2": "explicitBE",
14728
14896
  "1.2.840.10008.1.2.1.99": "explicitLE"
14729
14897
  };
14898
+ var MAX_UNAUDITABLE_SEQUENCE_FINDINGS = 64;
14899
+ var MAX_UNDEFINED_VR_FINDINGS = 64;
14900
+ var KNOWN_VRS3 = new Set(Object.keys(BE_VR_STRIDE));
14730
14901
  function validateRetain(retain) {
14731
14902
  const valid = new Set(DEIDENTIFY_OPTIONS);
14732
14903
  const active = /* @__PURE__ */ new Set();
@@ -14840,6 +15011,64 @@ function isPrivateCreatorElement(tag) {
14840
15011
  const { element } = splitTag(tag);
14841
15012
  return element >= 16 && element <= 255;
14842
15013
  }
15014
+ function actsOnTag(tag, ctx) {
15015
+ if (isPrivateTag(tag)) return true;
15016
+ const action = annexE(tag);
15017
+ if (action === void 0) return false;
15018
+ return resolveAction(effectiveCode(action, ctx.active)) !== "K";
15019
+ }
15020
+ function hasUndefinedVr(el) {
15021
+ return !KNOWN_VRS3.has(el.vr);
15022
+ }
15023
+ function keepOrEmpty(el, ctx, contextPath, out) {
15024
+ if (hasUndefinedVr(el)) {
15025
+ emptyUndefinedVrElement(el, ctx, contextPath, out);
15026
+ return false;
15027
+ }
15028
+ const hidden = findEmbeddedAttributes(el.rawBytes, el.vr, ctx.encoding, (t) => actsOnTag(t, ctx));
15029
+ if (hidden === void 0) {
15030
+ out.elements.set(el.tag, el);
15031
+ return true;
15032
+ }
15033
+ out.elements.set(el.tag, freshScalar(el, buffer.Buffer.alloc(0), 0));
15034
+ out.embeddedAttributes.push({
15035
+ tag: el.tag,
15036
+ vr: el.vr,
15037
+ hidden,
15038
+ ...contextPath.length > 0 ? { contextPath: [...contextPath] } : {}
15039
+ });
15040
+ out.warnings.push(
15041
+ embeddedAttributeRemoved({ byteOffset: el.byteOffset }, el.tag, el.vr, hidden.length)
15042
+ );
15043
+ return false;
15044
+ }
15045
+ function emptyUndefinedVrElement(el, ctx, contextPath, out) {
15046
+ out.elements.set(el.tag, freshScalar(el, buffer.Buffer.alloc(0), 0));
15047
+ if (ctx.budget.undefinedVrElements >= MAX_UNDEFINED_VR_FINDINGS) return;
15048
+ ctx.budget.undefinedVrElements += 1;
15049
+ out.undefinedVrElements.push({
15050
+ byteOffset: el.byteOffset,
15051
+ byteLength: el.rawBytes.length,
15052
+ ...contextPath.length > 0 ? { contextPath: [...contextPath] } : {}
15053
+ });
15054
+ out.warnings.push(undefinedVrNotAuditable({ byteOffset: el.byteOffset }, el.rawBytes.length));
15055
+ }
15056
+ function isUnauditableSequence(el) {
15057
+ return el.vr === "SQ" && el.items === void 0;
15058
+ }
15059
+ function emptyUnauditableSequence(el, ctx, contextPath, out) {
15060
+ out.elements.set(el.tag, rebuildSequence(el, [], ctx.encoding));
15061
+ if (ctx.budget.unauditableSequences >= MAX_UNAUDITABLE_SEQUENCE_FINDINGS) return;
15062
+ ctx.budget.unauditableSequences += 1;
15063
+ out.unauditableSequences.push({
15064
+ tag: el.tag,
15065
+ byteLength: el.rawBytes.length,
15066
+ ...contextPath.length > 0 ? { contextPath: [...contextPath] } : {}
15067
+ });
15068
+ out.warnings.push(
15069
+ sequenceNotAuditable({ byteOffset: el.byteOffset }, el.tag, el.rawBytes.length)
15070
+ );
15071
+ }
14843
15072
  function creatorsInScope(source) {
14844
15073
  const byBlock = /* @__PURE__ */ new Map();
14845
15074
  for (const el of source) {
@@ -14866,21 +15095,26 @@ function processElements(source, ctx, contextPath) {
14866
15095
  const out = {
14867
15096
  elements: /* @__PURE__ */ new Map(),
14868
15097
  attributes: [],
14869
- removedPrivateTags: []
15098
+ removedPrivateTags: [],
15099
+ embeddedAttributes: [],
15100
+ unauditableSequences: [],
15101
+ undefinedVrElements: [],
15102
+ warnings: []
14870
15103
  };
14871
15104
  const creators = creatorsInScope(source);
14872
15105
  for (const el of source) {
14873
15106
  if (isPrivateTag(el.tag)) {
14874
- if (keepsPrivate(el, ctx, creators)) out.elements.set(el.tag, el);
15107
+ if (keepsPrivate(el, ctx, creators)) keepOrEmpty(el, ctx, contextPath, out);
14875
15108
  else out.removedPrivateTags.push(el.tag);
14876
15109
  continue;
14877
15110
  }
14878
15111
  const action = annexE(el.tag);
14879
15112
  if (action === void 0) {
14880
- if (el.vr === "SQ" && el.items !== void 0) {
14881
- out.elements.set(el.tag, descendSequence(el, ctx, contextPath, out));
15113
+ if (el.vr === "SQ") {
15114
+ if (isUnauditableSequence(el)) emptyUnauditableSequence(el, ctx, contextPath, out);
15115
+ else out.elements.set(el.tag, descendSequence(el, ctx, contextPath, out));
14882
15116
  } else {
14883
- out.elements.set(el.tag, el);
15117
+ keepOrEmpty(el, ctx, contextPath, out);
14884
15118
  }
14885
15119
  continue;
14886
15120
  }
@@ -14892,8 +15126,7 @@ function processElements(source, ctx, contextPath) {
14892
15126
  let applied;
14893
15127
  switch (resolved) {
14894
15128
  case "K":
14895
- out.elements.set(el.tag, el);
14896
- applied = "kept";
15129
+ applied = keepOrEmpty(el, ctx, contextPath, out) ? "kept" : "emptied";
14897
15130
  break;
14898
15131
  case "X":
14899
15132
  applied = "removed";
@@ -14935,6 +15168,10 @@ function descendSequence(el, ctx, contextPath, out) {
14935
15168
  const inner = processElements(item.elements(), ctx, childPath);
14936
15169
  out.attributes.push(...inner.attributes);
14937
15170
  out.removedPrivateTags.push(...inner.removedPrivateTags);
15171
+ out.embeddedAttributes.push(...inner.embeddedAttributes);
15172
+ out.unauditableSequences.push(...inner.unauditableSequences);
15173
+ out.undefinedVrElements.push(...inner.undefinedVrElements);
15174
+ out.warnings.push(...inner.warnings);
14938
15175
  newItems.push(new Item({ index, warnings: [], elements: inner.elements }));
14939
15176
  });
14940
15177
  return rebuildSequence(el, newItems, ctx.encoding);
@@ -14951,13 +15188,23 @@ function applySequenceAction(el, resolved, action, ctx, contextPath, out) {
14951
15188
  applied = "emptied";
14952
15189
  break;
14953
15190
  case "C":
14954
- out.elements.set(el.tag, descendSequence(el, ctx, contextPath, out));
14955
- applied = "cleaned";
15191
+ if (isUnauditableSequence(el)) {
15192
+ emptyUnauditableSequence(el, ctx, contextPath, out);
15193
+ applied = "emptied";
15194
+ } else {
15195
+ out.elements.set(el.tag, descendSequence(el, ctx, contextPath, out));
15196
+ applied = "cleaned";
15197
+ }
14956
15198
  break;
14957
15199
  case "U":
14958
15200
  case "K":
14959
- out.elements.set(el.tag, descendSequence(el, ctx, contextPath, out));
14960
- applied = "kept";
15201
+ if (isUnauditableSequence(el)) {
15202
+ emptyUnauditableSequence(el, ctx, contextPath, out);
15203
+ applied = "emptied";
15204
+ } else {
15205
+ out.elements.set(el.tag, descendSequence(el, ctx, contextPath, out));
15206
+ applied = "kept";
15207
+ }
14961
15208
  break;
14962
15209
  }
14963
15210
  out.attributes.push(auditAttribute(el.tag, action, resolved, applied, contextPath));
@@ -15003,8 +15250,18 @@ function deidentify(ds, options = {}) {
15003
15250
  active,
15004
15251
  remap,
15005
15252
  profile: options.profile,
15006
- encoding};
15007
- const { elements, attributes, removedPrivateTags } = processElements(ds.elements(), ctx, []);
15253
+ encoding,
15254
+ budget: { unauditableSequences: 0, undefinedVrElements: 0 }
15255
+ };
15256
+ const processed = processElements(ds.elements(), ctx, []);
15257
+ const {
15258
+ elements,
15259
+ attributes,
15260
+ removedPrivateTags,
15261
+ embeddedAttributes,
15262
+ unauditableSequences,
15263
+ undefinedVrElements
15264
+ } = processed;
15008
15265
  elements.set(
15009
15266
  TAG_PATIENT_IDENTITY_REMOVED,
15010
15267
  insertedScalar(TAG_PATIENT_IDENTITY_REMOVED, "CS", buffer.Buffer.from("YES", "latin1"), littleEndian)
@@ -15014,7 +15271,7 @@ function deidentify(ds, options = {}) {
15014
15271
  TAG_DEIDENTIFICATION_METHOD,
15015
15272
  insertedScalar(TAG_DEIDENTIFICATION_METHOD, "LO", buffer.Buffer.from(method, "latin1"), littleEndian)
15016
15273
  );
15017
- const warnings = [];
15274
+ const warnings = [...processed.warnings];
15018
15275
  if (hasUncleanedBurnedIn(ds)) {
15019
15276
  const offset = ds.get(TAG_PIXEL_DATA)?.byteOffset ?? 0;
15020
15277
  warnings.push(burnedInAnnotationNotRemoved({ byteOffset: offset, fileMeta: false }));
@@ -15029,6 +15286,9 @@ function deidentify(ds, options = {}) {
15029
15286
  const report = {
15030
15287
  attributes,
15031
15288
  removedPrivateTags,
15289
+ embeddedAttributes,
15290
+ unauditableSequences,
15291
+ undefinedVrElements,
15032
15292
  uidMap: remap.cache,
15033
15293
  warnings,
15034
15294
  retained: [...active]