@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/dist/index.d.cts CHANGED
@@ -2526,13 +2526,21 @@ interface UndefinedVrFinding {
2526
2526
  /**
2527
2527
  * The audit trail returned alongside the de-identified dataset.
2528
2528
  *
2529
- * Every field except one is composed from static tables: tags, Part 6 keywords,
2530
- * Annex E action codes, structural `TAG[index]` sequence paths, and registry
2531
- * warning messages. **`uidMap` is the exception, and it is not value-free.** Its
2532
- * keys are the source UIDs read out of the file, kept so a caller can make UID
2533
- * replacement consistent across a study or an archive. A Study or SOP Instance
2534
- * UID is a unique identifying number, so treat `uidMap` as PHI: the rest of the
2535
- * report is safe to log, and that field is not.
2529
+ * Most fields are composed from static tables: Part 6 keywords, Annex E action
2530
+ * codes, structural `TAG[index]` sequence paths, and registry warning messages.
2531
+ * **Two are not, and both are named here rather than in a footnote.**
2532
+ *
2533
+ * 1. **`uidMap`** - its keys are the source UIDs read out of the file, kept so a
2534
+ * caller can make UID replacement consistent across a study or an archive. A
2535
+ * Study or SOP Instance UID is a unique identifying number, so treat it as
2536
+ * PHI.
2537
+ * 2. **`removedPrivateTags`** - see the field's own note. On a *well-formed*
2538
+ * file these are the sender's own private tag numbers and carry nothing; on a
2539
+ * malformed one a tag can be four bytes of a value, and it is measured, not
2540
+ * theoretical.
2541
+ *
2542
+ * So "the report is safe to log apart from `uidMap`" is **not** an accurate
2543
+ * description of this type, and was corrected rather than kept convenient.
2536
2544
  *
2537
2545
  * @example
2538
2546
  * ```ts
@@ -2545,7 +2553,24 @@ interface UndefinedVrFinding {
2545
2553
  interface DeidentifyReport {
2546
2554
  /** Per-attribute outcomes for every attribute Annex E acted on. */
2547
2555
  readonly attributes: readonly DeidentifiedAttribute[];
2548
- /** Private tags removed under the Basic Profile (kept ones are omitted). */
2556
+ /**
2557
+ * Private tags removed under the Basic Profile (kept ones are omitted).
2558
+ *
2559
+ * **This is the second field that is not value-free, and the qualification is
2560
+ * measured.** A tag here is composed from four bytes of the source, and on a
2561
+ * file whose Value Lengths disagree with its bytes those four bytes can be
2562
+ * document content rather than a tag the sender wrote: an `OB` carrier holding
2563
+ * `"SECRET-NOTE-"` followed by a well-formed odd-group header reports
2564
+ * `["41534342"]`, whose wire-order bytes read `"SABC"`. That reproduces
2565
+ * identically on every release that has shipped this field.
2566
+ *
2567
+ * It is reported anyway, because *which* private tags were removed is the
2568
+ * whole audit value of the field and withholding them would destroy it on
2569
+ * every well-formed file to bound a malformed one. Narrowing it - to blocks a
2570
+ * creator in the same Data Set actually reserved, say - is a product decision
2571
+ * about audit value versus a four-byte echo, not a defect fix, and it has not
2572
+ * been made. Treat this array as PHI when the source is untrusted.
2573
+ */
2549
2574
  readonly removedPrivateTags: readonly Tag[];
2550
2575
  /**
2551
2576
  * Values emptied because whole Data Elements were encoded inside them by an
@@ -2728,8 +2753,8 @@ declare class DeidentifyError extends Error {
2728
2753
  * nine *metadata-affecting* Annex E Options, driven by the generated Table E.1-1
2729
2754
  * action map ({@link annexE}). It is a **pure** function: the input {@link Dataset}
2730
2755
  * is never mutated; a fresh `Dataset` (with a rebuilt element map and File Meta)
2731
- * is returned alongside a {@link DeidentifyReport} that is value-free except for
2732
- * `uidMap`, whose keys are the file's own source UIDs.
2756
+ * is returned alongside a {@link DeidentifyReport} whose two non-value-free
2757
+ * fields are named on that type: `uidMap` and `removedPrivateTags`.
2733
2758
  *
2734
2759
  * **What it does**
2735
2760
  * - Resolves each attribute's action (basic profile, overridden by an active
@@ -2804,6 +2829,29 @@ declare class DeidentifyError extends Error {
2804
2829
  * `35adc2d`. No content test can decide it - arbitrary bytes are what those
2805
2830
  * VRs are for - and the one candidate remedy empties conformant binary values.
2806
2831
  * See `./embedded.ts`.
2832
+ * - `RetainSafePrivate` **retains nothing inside a Sequence Item whose Data Set
2833
+ * boundary the file contradicts** - an item stream that ran past its
2834
+ * sequence's own declared Value Length. Which elements are in the Item is then
2835
+ * not determined by the file, and PS3.5 §7.8.1 scopes a private block
2836
+ * reservation to exactly that boundary, so PS3.15 §E.3.10's "known ... to be
2837
+ * safe" cannot be established and its "all other Private Attributes shall be
2838
+ * removed **or processed in the element-specific manner recommended by
2839
+ * Deidentification Action (0008,0307), if present within Private Data Element
2840
+ * Characteristics Sequence (0008,0300)**" applies - a two-branch clause, of
2841
+ * which removal is the branch available here (`(0008,0307)` is not
2842
+ * implemented). Every private element the recursion **reaches** in such an
2843
+ * Item, at any depth below it, is removed and named in
2844
+ * `report.removedPrivateTags`. **There is one carve-out, and it is the same one
2845
+ * `#54` was refused for asserting away**: `keepsPrivate` decides *before* the
2846
+ * descent, so a **private `SQ`** the profile vouches for is kept **verbatim**,
2847
+ * its items are never walked, and this rule is never consulted inside it -
2848
+ * measured, and it still leaks a private value absorbed from the root
2849
+ * (`PRE-EXISTING`, identical on `164eb39`, its own item). That costs content
2850
+ * on a malformed file and nothing at all on a conformant one; without it a
2851
+ * private value the sender wrote **outside** the sequence was retained on the
2852
+ * Item's reservation and written into output stamped `PatientIdentityRemoved =
2853
+ * YES` with `removedPrivateTags: []` (`DICOM-PRIVATE-CREATOR-RESERVATION-LEAK`,
2854
+ * live through `0.0.9`). See {@link itemStreamOverrunsSequence}.
2807
2855
  *
2808
2856
  * @module
2809
2857
  */
package/dist/index.d.ts CHANGED
@@ -2526,13 +2526,21 @@ interface UndefinedVrFinding {
2526
2526
  /**
2527
2527
  * The audit trail returned alongside the de-identified dataset.
2528
2528
  *
2529
- * Every field except one is composed from static tables: tags, Part 6 keywords,
2530
- * Annex E action codes, structural `TAG[index]` sequence paths, and registry
2531
- * warning messages. **`uidMap` is the exception, and it is not value-free.** Its
2532
- * keys are the source UIDs read out of the file, kept so a caller can make UID
2533
- * replacement consistent across a study or an archive. A Study or SOP Instance
2534
- * UID is a unique identifying number, so treat `uidMap` as PHI: the rest of the
2535
- * report is safe to log, and that field is not.
2529
+ * Most fields are composed from static tables: Part 6 keywords, Annex E action
2530
+ * codes, structural `TAG[index]` sequence paths, and registry warning messages.
2531
+ * **Two are not, and both are named here rather than in a footnote.**
2532
+ *
2533
+ * 1. **`uidMap`** - its keys are the source UIDs read out of the file, kept so a
2534
+ * caller can make UID replacement consistent across a study or an archive. A
2535
+ * Study or SOP Instance UID is a unique identifying number, so treat it as
2536
+ * PHI.
2537
+ * 2. **`removedPrivateTags`** - see the field's own note. On a *well-formed*
2538
+ * file these are the sender's own private tag numbers and carry nothing; on a
2539
+ * malformed one a tag can be four bytes of a value, and it is measured, not
2540
+ * theoretical.
2541
+ *
2542
+ * So "the report is safe to log apart from `uidMap`" is **not** an accurate
2543
+ * description of this type, and was corrected rather than kept convenient.
2536
2544
  *
2537
2545
  * @example
2538
2546
  * ```ts
@@ -2545,7 +2553,24 @@ interface UndefinedVrFinding {
2545
2553
  interface DeidentifyReport {
2546
2554
  /** Per-attribute outcomes for every attribute Annex E acted on. */
2547
2555
  readonly attributes: readonly DeidentifiedAttribute[];
2548
- /** Private tags removed under the Basic Profile (kept ones are omitted). */
2556
+ /**
2557
+ * Private tags removed under the Basic Profile (kept ones are omitted).
2558
+ *
2559
+ * **This is the second field that is not value-free, and the qualification is
2560
+ * measured.** A tag here is composed from four bytes of the source, and on a
2561
+ * file whose Value Lengths disagree with its bytes those four bytes can be
2562
+ * document content rather than a tag the sender wrote: an `OB` carrier holding
2563
+ * `"SECRET-NOTE-"` followed by a well-formed odd-group header reports
2564
+ * `["41534342"]`, whose wire-order bytes read `"SABC"`. That reproduces
2565
+ * identically on every release that has shipped this field.
2566
+ *
2567
+ * It is reported anyway, because *which* private tags were removed is the
2568
+ * whole audit value of the field and withholding them would destroy it on
2569
+ * every well-formed file to bound a malformed one. Narrowing it - to blocks a
2570
+ * creator in the same Data Set actually reserved, say - is a product decision
2571
+ * about audit value versus a four-byte echo, not a defect fix, and it has not
2572
+ * been made. Treat this array as PHI when the source is untrusted.
2573
+ */
2549
2574
  readonly removedPrivateTags: readonly Tag[];
2550
2575
  /**
2551
2576
  * Values emptied because whole Data Elements were encoded inside them by an
@@ -2728,8 +2753,8 @@ declare class DeidentifyError extends Error {
2728
2753
  * nine *metadata-affecting* Annex E Options, driven by the generated Table E.1-1
2729
2754
  * action map ({@link annexE}). It is a **pure** function: the input {@link Dataset}
2730
2755
  * is never mutated; a fresh `Dataset` (with a rebuilt element map and File Meta)
2731
- * is returned alongside a {@link DeidentifyReport} that is value-free except for
2732
- * `uidMap`, whose keys are the file's own source UIDs.
2756
+ * is returned alongside a {@link DeidentifyReport} whose two non-value-free
2757
+ * fields are named on that type: `uidMap` and `removedPrivateTags`.
2733
2758
  *
2734
2759
  * **What it does**
2735
2760
  * - Resolves each attribute's action (basic profile, overridden by an active
@@ -2804,6 +2829,29 @@ declare class DeidentifyError extends Error {
2804
2829
  * `35adc2d`. No content test can decide it - arbitrary bytes are what those
2805
2830
  * VRs are for - and the one candidate remedy empties conformant binary values.
2806
2831
  * See `./embedded.ts`.
2832
+ * - `RetainSafePrivate` **retains nothing inside a Sequence Item whose Data Set
2833
+ * boundary the file contradicts** - an item stream that ran past its
2834
+ * sequence's own declared Value Length. Which elements are in the Item is then
2835
+ * not determined by the file, and PS3.5 §7.8.1 scopes a private block
2836
+ * reservation to exactly that boundary, so PS3.15 §E.3.10's "known ... to be
2837
+ * safe" cannot be established and its "all other Private Attributes shall be
2838
+ * removed **or processed in the element-specific manner recommended by
2839
+ * Deidentification Action (0008,0307), if present within Private Data Element
2840
+ * Characteristics Sequence (0008,0300)**" applies - a two-branch clause, of
2841
+ * which removal is the branch available here (`(0008,0307)` is not
2842
+ * implemented). Every private element the recursion **reaches** in such an
2843
+ * Item, at any depth below it, is removed and named in
2844
+ * `report.removedPrivateTags`. **There is one carve-out, and it is the same one
2845
+ * `#54` was refused for asserting away**: `keepsPrivate` decides *before* the
2846
+ * descent, so a **private `SQ`** the profile vouches for is kept **verbatim**,
2847
+ * its items are never walked, and this rule is never consulted inside it -
2848
+ * measured, and it still leaks a private value absorbed from the root
2849
+ * (`PRE-EXISTING`, identical on `164eb39`, its own item). That costs content
2850
+ * on a malformed file and nothing at all on a conformant one; without it a
2851
+ * private value the sender wrote **outside** the sequence was retained on the
2852
+ * Item's reservation and written into output stamped `PatientIdentityRemoved =
2853
+ * YES` with `removedPrivateTags: []` (`DICOM-PRIVATE-CREATOR-RESERVATION-LEAK`,
2854
+ * live through `0.0.9`). See {@link itemStreamOverrunsSequence}.
2807
2855
  *
2808
2856
  * @module
2809
2857
  */
package/dist/index.mjs CHANGED
@@ -9,7 +9,7 @@ var __export = (target, all) => {
9
9
  };
10
10
 
11
11
  // src/version.ts
12
- var VERSION = "0.0.8";
12
+ var VERSION = "0.0.10";
13
13
 
14
14
  // src/dictionary/index.ts
15
15
  var dictionary_exports = {};
@@ -11701,6 +11701,12 @@ var BE_VR_STRIDE = Object.freeze({
11701
11701
  UT: 0,
11702
11702
  SQ: 0
11703
11703
  });
11704
+ var KNOWN_VRS = Object.freeze(
11705
+ new Set(Object.keys(BE_VR_STRIDE))
11706
+ );
11707
+ function isRecognizedVr(vr) {
11708
+ return KNOWN_VRS.has(vr);
11709
+ }
11704
11710
 
11705
11711
  // src/parser/warnings.ts
11706
11712
  var WARNING_CODES = {
@@ -11751,7 +11757,11 @@ var WARNING_MESSAGES = Object.freeze({
11751
11757
  DICOM_VR_MISMATCH: "Element ({tag}) on-wire VR is {vr2}; dictionary lists {vr}. Trusting on-wire VR.",
11752
11758
  DICOM_PRIVATE_TAG_NO_CREATOR: "Private element ({tag}) has no Private Creator registered for its block; treating as VR=UN.",
11753
11759
  DICOM_GROUP_LENGTH_IN_DATASET: "Retired Group Length element ({tag}) encountered in dataset; preserved as-is.",
11754
- DICOM_NONZERO_RESERVED_BYTES: "Element ({tag}) has non-zero reserved bytes between VR and length (first byte {n}, second byte {n2}); ignoring.",
11760
+ // The tag is deliberately absent, and this is the only *parser* message where
11761
+ // that is true. See `nonzeroReservedBytes`. Kept terse for the same reason the
11762
+ // de-identify messages are: it is emitted once per element, and element count
11763
+ // is attacker-chosen.
11764
+ 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.",
11755
11765
  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.",
11756
11766
  DICOM_UN_PARSED_AS_SQ: "Element ({tag}) has VR=UN with undefined length; descended as Implicit VR LE sequence per CP-246.",
11757
11767
  DICOM_EMPTY_ITEM_IN_SEQUENCE: "Sequence ({tag}) contains an empty item (length=0); tolerated.",
@@ -11786,7 +11796,6 @@ var WARNING_MESSAGES = Object.freeze({
11786
11796
  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.",
11787
11797
  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."
11788
11798
  });
11789
- var KNOWN_VRS = new Set(Object.keys(BE_VR_STRIDE));
11790
11799
  var TAG_SHAPE = /^[0-9A-F]{8}$/u;
11791
11800
  function renderTag(tag) {
11792
11801
  return tag !== void 0 && TAG_SHAPE.test(tag) ? tag : WITHHELD;
@@ -11828,9 +11837,8 @@ function privateTagNoCreator(position, tag) {
11828
11837
  function groupLengthInDataset(position, tag) {
11829
11838
  return build(WARNING_CODES.DICOM_GROUP_LENGTH_IN_DATASET, position, { tag });
11830
11839
  }
11831
- function nonzeroReservedBytes(position, tag, first, second) {
11840
+ function nonzeroReservedBytes(position, first, second) {
11832
11841
  return build(WARNING_CODES.DICOM_NONZERO_RESERVED_BYTES, position, {
11833
- tag,
11834
11842
  n: first,
11835
11843
  n2: second
11836
11844
  });
@@ -12034,12 +12042,12 @@ function readExplicitElementHeader(cursor, _ctx, emit) {
12034
12042
  const vr = vrSlice.toString("ascii");
12035
12043
  let length;
12036
12044
  let headerLength;
12037
- if (LONG_FORM_VRS.has(vr)) {
12045
+ if (LONG_FORM_VRS.has(vr) || !isRecognizedVr(vr)) {
12038
12046
  const reserved0 = cursor.buffer[cursor.position];
12039
12047
  const reserved1 = cursor.buffer[cursor.position + 1];
12040
12048
  cursor.position += 2;
12041
12049
  if ((reserved0 ?? 0) !== 0 || (reserved1 ?? 0) !== 0) {
12042
- emit(nonzeroReservedBytes({ byteOffset: headerStart }, tag, reserved0 ?? 0, reserved1 ?? 0));
12050
+ emit(nonzeroReservedBytes({ byteOffset: headerStart }, reserved0 ?? 0, reserved1 ?? 0));
12043
12051
  }
12044
12052
  length = cursor.readUInt32();
12045
12053
  headerLength = 12;
@@ -12177,7 +12185,7 @@ function readExplicitLeElement(cursor) {
12177
12185
  const vr = cursor.slice(2).toString("ascii");
12178
12186
  let length;
12179
12187
  let headerLength;
12180
- if (LONG_FORM_VRS.has(vr)) {
12188
+ if (LONG_FORM_VRS.has(vr) || !isRecognizedVr(vr)) {
12181
12189
  cursor.slice(2);
12182
12190
  length = cursor.readUInt32();
12183
12191
  headerLength = 12;
@@ -13451,7 +13459,7 @@ function encodeDatasetElement(el, encoding) {
13451
13459
  }
13452
13460
  const littleEndian = encoding === "explicitLE";
13453
13461
  const vrBuf = Buffer.from(el.vr, "ascii");
13454
- if (LONG_FORM_VRS.has(el.vr)) {
13462
+ if (LONG_FORM_VRS.has(el.vr) || !isRecognizedVr(el.vr)) {
13455
13463
  return Buffer.concat([
13456
13464
  uint16(group, littleEndian),
13457
13465
  uint16(element, littleEndian),
@@ -13502,7 +13510,7 @@ function encodeMetaElement(tag, vr, rawValue) {
13502
13510
  const elementBuf = Buffer.alloc(2);
13503
13511
  elementBuf.writeUInt16LE(element, 0);
13504
13512
  const vrBuf = Buffer.from(vr, "ascii");
13505
- if (LONG_FORM_VRS.has(vr)) {
13513
+ if (LONG_FORM_VRS.has(vr) || !isRecognizedVr(vr)) {
13506
13514
  const lengthBuf2 = Buffer.alloc(4);
13507
13515
  lengthBuf2.writeUInt32LE(value.length, 0);
13508
13516
  return Buffer.concat([
@@ -14697,7 +14705,6 @@ function uidValueMultiplicity(rawBytes) {
14697
14705
  }
14698
14706
 
14699
14707
  // src/deident/embedded.ts
14700
- var KNOWN_VRS2 = new Set(Object.keys(BE_VR_STRIDE));
14701
14708
  var UNDEFINED_LENGTH4 = 4294967295;
14702
14709
  var FFFE_GROUP = 65534;
14703
14710
  var SCANNABLE_CARRIER_VRS = /* @__PURE__ */ new Set([
@@ -14758,7 +14765,7 @@ function decodeAt(value, off, encoding) {
14758
14765
  return next2 > value.length ? void 0 : { next: next2, tag };
14759
14766
  }
14760
14767
  const vr = value.toString("latin1", off + 4, off + 6);
14761
- if (!KNOWN_VRS2.has(vr)) return void 0;
14768
+ if (!KNOWN_VRS.has(vr)) return void 0;
14762
14769
  if (LONG_FORM_VRS.has(vr)) {
14763
14770
  if (off + 12 > value.length) return void 0;
14764
14771
  if (value[off + 6] !== 0 || value[off + 7] !== 0) return void 0;
@@ -14895,7 +14902,6 @@ var BODY_ENCODING2 = {
14895
14902
  };
14896
14903
  var MAX_UNAUDITABLE_SEQUENCE_FINDINGS = 64;
14897
14904
  var MAX_UNDEFINED_VR_FINDINGS = 64;
14898
- var KNOWN_VRS3 = new Set(Object.keys(BE_VR_STRIDE));
14899
14905
  function validateRetain(retain) {
14900
14906
  const valid = new Set(DEIDENTIFY_OPTIONS);
14901
14907
  const active = /* @__PURE__ */ new Set();
@@ -15016,7 +15022,7 @@ function actsOnTag(tag, ctx) {
15016
15022
  return resolveAction(effectiveCode(action, ctx.active)) !== "K";
15017
15023
  }
15018
15024
  function hasUndefinedVr(el) {
15019
- return !KNOWN_VRS3.has(el.vr);
15025
+ return !KNOWN_VRS.has(el.vr);
15020
15026
  }
15021
15027
  function keepOrEmpty(el, ctx, contextPath, out) {
15022
15028
  if (hasUndefinedVr(el)) {
@@ -15080,6 +15086,13 @@ function creatorFor(tag, creators) {
15080
15086
  const { group, element } = splitTag(tag);
15081
15087
  return creators.get(`${String(group)}:${String(element >> 8 & 255)}`);
15082
15088
  }
15089
+ var UNDEFINED_LENGTH5 = 4294967295;
15090
+ var EXPLICIT_SQ_HEADER_BYTES = 12;
15091
+ function itemStreamOverrunsSequence(el, encoding) {
15092
+ if (el.vr !== "SQ" || el.length === UNDEFINED_LENGTH5) return false;
15093
+ const headerBytes = encoding === "implicit" ? 0 : EXPLICIT_SQ_HEADER_BYTES;
15094
+ return el.rawBytes.length - headerBytes > el.length;
15095
+ }
15083
15096
  function keepsPrivate(el, ctx, creators) {
15084
15097
  if (!ctx.active.has("RetainSafePrivate") || ctx.profile === void 0) return false;
15085
15098
  if (isPrivateCreatorElement(el.tag)) {
@@ -15089,7 +15102,7 @@ function keepsPrivate(el, ctx, creators) {
15089
15102
  if (creator === void 0) return false;
15090
15103
  return resolvePrivateTag(ctx.profile, el.tag, creator) !== void 0;
15091
15104
  }
15092
- function processElements(source, ctx, contextPath) {
15105
+ function processElements(source, ctx, contextPath, reservationsUsable) {
15093
15106
  const out = {
15094
15107
  elements: /* @__PURE__ */ new Map(),
15095
15108
  attributes: [],
@@ -15102,7 +15115,8 @@ function processElements(source, ctx, contextPath) {
15102
15115
  const creators = creatorsInScope(source);
15103
15116
  for (const el of source) {
15104
15117
  if (isPrivateTag(el.tag)) {
15105
- if (keepsPrivate(el, ctx, creators)) keepOrEmpty(el, ctx, contextPath, out);
15118
+ if (reservationsUsable && keepsPrivate(el, ctx, creators))
15119
+ keepOrEmpty(el, ctx, contextPath, out);
15106
15120
  else out.removedPrivateTags.push(el.tag);
15107
15121
  continue;
15108
15122
  }
@@ -15110,7 +15124,8 @@ function processElements(source, ctx, contextPath) {
15110
15124
  if (action === void 0) {
15111
15125
  if (el.vr === "SQ") {
15112
15126
  if (isUnauditableSequence(el)) emptyUnauditableSequence(el, ctx, contextPath, out);
15113
- else out.elements.set(el.tag, descendSequence(el, ctx, contextPath, out));
15127
+ else
15128
+ out.elements.set(el.tag, descendSequence(el, ctx, contextPath, out, reservationsUsable));
15114
15129
  } else {
15115
15130
  keepOrEmpty(el, ctx, contextPath, out);
15116
15131
  }
@@ -15118,7 +15133,7 @@ function processElements(source, ctx, contextPath) {
15118
15133
  }
15119
15134
  const resolved = resolveAction(effectiveCode(action, ctx.active));
15120
15135
  if (el.vr === "SQ") {
15121
- applySequenceAction(el, resolved, action, ctx, contextPath, out);
15136
+ applySequenceAction(el, resolved, action, ctx, contextPath, out, reservationsUsable);
15122
15137
  continue;
15123
15138
  }
15124
15139
  let applied;
@@ -15159,11 +15174,12 @@ function processElements(source, ctx, contextPath) {
15159
15174
  }
15160
15175
  return out;
15161
15176
  }
15162
- function descendSequence(el, ctx, contextPath, out) {
15177
+ function descendSequence(el, ctx, contextPath, out, reservationsUsable) {
15178
+ const childReservationsUsable = reservationsUsable && !itemStreamOverrunsSequence(el, ctx.encoding);
15163
15179
  const newItems = [];
15164
15180
  (el.items ?? []).forEach((item, index) => {
15165
15181
  const childPath = [...contextPath, `${el.tag}[${String(index)}]`];
15166
- const inner = processElements(item.elements(), ctx, childPath);
15182
+ const inner = processElements(item.elements(), ctx, childPath, childReservationsUsable);
15167
15183
  out.attributes.push(...inner.attributes);
15168
15184
  out.removedPrivateTags.push(...inner.removedPrivateTags);
15169
15185
  out.embeddedAttributes.push(...inner.embeddedAttributes);
@@ -15174,7 +15190,7 @@ function descendSequence(el, ctx, contextPath, out) {
15174
15190
  });
15175
15191
  return rebuildSequence(el, newItems, ctx.encoding);
15176
15192
  }
15177
- function applySequenceAction(el, resolved, action, ctx, contextPath, out) {
15193
+ function applySequenceAction(el, resolved, action, ctx, contextPath, out, reservationsUsable) {
15178
15194
  let applied;
15179
15195
  switch (resolved) {
15180
15196
  case "X":
@@ -15190,7 +15206,7 @@ function applySequenceAction(el, resolved, action, ctx, contextPath, out) {
15190
15206
  emptyUnauditableSequence(el, ctx, contextPath, out);
15191
15207
  applied = "emptied";
15192
15208
  } else {
15193
- out.elements.set(el.tag, descendSequence(el, ctx, contextPath, out));
15209
+ out.elements.set(el.tag, descendSequence(el, ctx, contextPath, out, reservationsUsable));
15194
15210
  applied = "cleaned";
15195
15211
  }
15196
15212
  break;
@@ -15200,7 +15216,7 @@ function applySequenceAction(el, resolved, action, ctx, contextPath, out) {
15200
15216
  emptyUnauditableSequence(el, ctx, contextPath, out);
15201
15217
  applied = "emptied";
15202
15218
  } else {
15203
- out.elements.set(el.tag, descendSequence(el, ctx, contextPath, out));
15219
+ out.elements.set(el.tag, descendSequence(el, ctx, contextPath, out, reservationsUsable));
15204
15220
  applied = "kept";
15205
15221
  }
15206
15222
  break;
@@ -15251,7 +15267,7 @@ function deidentify(ds, options = {}) {
15251
15267
  encoding,
15252
15268
  budget: { unauditableSequences: 0, undefinedVrElements: 0 }
15253
15269
  };
15254
- const processed = processElements(ds.elements(), ctx, []);
15270
+ const processed = processElements(ds.elements(), ctx, [], true);
15255
15271
  const {
15256
15272
  elements,
15257
15273
  attributes,