@cosyte/dicom 0.0.8 โ†’ 0.0.11

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.cjs CHANGED
@@ -11,7 +11,7 @@ var __export = (target, all) => {
11
11
  };
12
12
 
13
13
  // src/version.ts
14
- var VERSION = "0.0.8";
14
+ var VERSION = "0.0.11";
15
15
 
16
16
  // src/dictionary/index.ts
17
17
  var dictionary_exports = {};
@@ -11703,15 +11703,24 @@ 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 = {
11709
11715
  // === Phase 2 actively emits (D-08 active list - alphabetical-within-prefix per CONTEXT specifics ยง) ===
11716
+ DICOM_DUPLICATE_FILE_META_ELEMENT: "DICOM_DUPLICATE_FILE_META_ELEMENT",
11717
+ DICOM_DUPLICATE_TAG_IN_DATA_SET: "DICOM_DUPLICATE_TAG_IN_DATA_SET",
11710
11718
  DICOM_EMPTY_ITEM_IN_SEQUENCE: "DICOM_EMPTY_ITEM_IN_SEQUENCE",
11711
11719
  DICOM_FILE_META_GROUP_LENGTH_MISMATCH: "DICOM_FILE_META_GROUP_LENGTH_MISMATCH",
11712
11720
  DICOM_FILE_META_GROUP_LENGTH_MISSING: "DICOM_FILE_META_GROUP_LENGTH_MISSING",
11713
11721
  DICOM_GROUP_LENGTH_IN_DATASET: "DICOM_GROUP_LENGTH_IN_DATASET",
11714
11722
  DICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR: "DICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR",
11723
+ DICOM_ITEM_CROSSES_SEQUENCE_END: "DICOM_ITEM_CROSSES_SEQUENCE_END",
11715
11724
  DICOM_MISSING_PREAMBLE: "DICOM_MISSING_PREAMBLE",
11716
11725
  DICOM_NONZERO_RESERVED_BYTES: "DICOM_NONZERO_RESERVED_BYTES",
11717
11726
  DICOM_ODD_LENGTH_VALUE_PADDED: "DICOM_ODD_LENGTH_VALUE_PADDED",
@@ -11737,6 +11746,10 @@ var WARNING_CODES = {
11737
11746
  // reserved by Phase 7 - not emitted in Phase 2
11738
11747
  DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED: "DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED",
11739
11748
  // emitted by deidentify(), never by the parser
11749
+ DICOM_DEIDENT_METHOD_NOT_ADDED: "DICOM_DEIDENT_METHOD_NOT_ADDED",
11750
+ // emitted by deidentify(), never by the parser
11751
+ DICOM_DEIDENT_METHOD_PRIOR_RETAINED: "DICOM_DEIDENT_METHOD_PRIOR_RETAINED",
11752
+ // emitted by deidentify(), never by the parser
11740
11753
  DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE: "DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE",
11741
11754
  // emitted by deidentify(), never by the parser
11742
11755
  DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE: "DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE",
@@ -11753,10 +11766,44 @@ var WARNING_MESSAGES = Object.freeze({
11753
11766
  DICOM_VR_MISMATCH: "Element ({tag}) on-wire VR is {vr2}; dictionary lists {vr}. Trusting on-wire VR.",
11754
11767
  DICOM_PRIVATE_TAG_NO_CREATOR: "Private element ({tag}) has no Private Creator registered for its block; treating as VR=UN.",
11755
11768
  DICOM_GROUP_LENGTH_IN_DATASET: "Retired Group Length element ({tag}) encountered in dataset; preserved as-is.",
11756
- DICOM_NONZERO_RESERVED_BYTES: "Element ({tag}) has non-zero reserved bytes between VR and length (first byte {n}, second byte {n2}); ignoring.",
11769
+ // The tag is deliberately absent, and this is the only *parser* message where
11770
+ // that is true. See `nonzeroReservedBytes`. Kept terse for the same reason the
11771
+ // de-identify messages are: it is emitted once per element, and element count
11772
+ // is attacker-chosen.
11773
+ 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
11774
  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
11775
  DICOM_UN_PARSED_AS_SQ: "Element ({tag}) has VR=UN with undefined length; descended as Implicit VR LE sequence per CP-246.",
11759
11776
  DICOM_EMPTY_ITEM_IN_SEQUENCE: "Sequence ({tag}) contains an empty item (length=0); tolerated.",
11777
+ // Neither the tag nor the replaced element's value is here, and the tag's
11778
+ // absence is specific to this code rather than caution. See
11779
+ // `duplicateTagInDataSet`. Short by measurement rather than by intention: one
11780
+ // is raised per collision and the collision count is chosen by the input, so a
11781
+ // 400-character draft was measured at 50 M characters over a 1 MiB file and
11782
+ // cut. The reasoning lives in the factory's JSDoc, not in the string.
11783
+ DICOM_DUPLICATE_TAG_IN_DATA_SET: "A Data Element carrying a tag this Data Set already holds replaced the earlier one at this byte offset; that element's value is not in the parsed object. PS3.5 7.1 and 7.5.1. Tag withheld.",
11784
+ // The tag is deliberately absent for the same reason as the code above, and
11785
+ // the surviving copy is named because the File Meta group resolves a repeat
11786
+ // the OPPOSITE way round to a Data Set: first copy wins here, last read wins
11787
+ // there. Short for the same measured reason - one is raised per repeat and the
11788
+ // repeat count is chosen by the input.
11789
+ // ๐Ÿ›‘ NO SPEC CITATION HERE, AND ITS ABSENCE IS DELIBERATE. A draft ended this
11790
+ // string "PS3.5 7.1.", which asserts that section's "at most once in a Data
11791
+ // Set" governs the File Meta group - the applicability this slice states it
11792
+ // cannot establish, because PS3.10 governs that group and is not vendored
11793
+ // here. Under `{ strict: true }` this string is also the thrown message, so an
11794
+ // unqualified citation there is the claim, not a footnote. Do not put it back.
11795
+ DICOM_DUPLICATE_FILE_META_ELEMENT: "A File Meta Data Element repeats a (0002,xxxx) tag the group already carries; the copy at this byte offset is not in the parsed File Meta, and the FIRST copy of that tag is the one projected. Tag withheld.",
11796
+ // No value and no length: the prior value is the file's own text. The tag is a
11797
+ // constant of this code rather than composed from input, as in the two
11798
+ // (0002,0000) messages above.
11799
+ DICOM_DEIDENT_METHOD_NOT_ADDED: "The De-identification Method this run recorded could not be added beside the value (0012,0063) already carried without exceeding the largest Value Length that VR can encode, so the earlier value was replaced (PS3.15 E.1.1). The replaced text is not in the output.",
11800
+ // No value and no length, for the same reason as the code above: the retained
11801
+ // text is the file's own. The tag is a constant of this code.
11802
+ DICOM_DEIDENT_METHOD_PRIOR_RETAINED: "A De-identification Method (0012,0063) value the source file already carried was kept beside the one this run recorded, as PS3.15 E.1.1 requires. That attribute is not in Table E.1-1, so no rule in this run inspected, audited or redacted those bytes: if the sender wrote identifying text there it is in the de-identified output, under (0012,0062) = YES. The text is withheld from this message.",
11803
+ // The Item's own declared length is deliberately absent. See
11804
+ // `itemCrossesSequenceEnd`; `{n2}` stays because the emit site's
11805
+ // `endLimit < buffer.length` conjunct bounds it by the buffer.
11806
+ DICOM_ITEM_CROSSES_SEQUENCE_END: "Item ({tag}) declares a length reaching past its enclosing sequence's declared end, so it reads the enclosing Data Set's bytes; {n2} bytes remained inside the sequence. The file's two length fields disagree (PS3.5 7.5.1 and 7.5.2 govern them); the item's is used. The declared length is withheld; the byte offset locates the item.",
11760
11807
  DICOM_PIXEL_DATA_LENGTH_MISMATCH: "(7FE0,0010) PixelData declared length {n} does not match computed {n2} bytes.",
11761
11808
  DICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR: "Private element ({tag}) under Implicit VR LE has no VR override; falling back to UN.",
11762
11809
  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.",
@@ -11788,7 +11835,6 @@ var WARNING_MESSAGES = Object.freeze({
11788
11835
  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
11836
  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
11837
  });
11791
- var KNOWN_VRS = new Set(Object.keys(BE_VR_STRIDE));
11792
11838
  var TAG_SHAPE = /^[0-9A-F]{8}$/u;
11793
11839
  function renderTag(tag) {
11794
11840
  return tag !== void 0 && TAG_SHAPE.test(tag) ? tag : WITHHELD;
@@ -11830,9 +11876,8 @@ function privateTagNoCreator(position, tag) {
11830
11876
  function groupLengthInDataset(position, tag) {
11831
11877
  return build(WARNING_CODES.DICOM_GROUP_LENGTH_IN_DATASET, position, { tag });
11832
11878
  }
11833
- function nonzeroReservedBytes(position, tag, first, second) {
11879
+ function nonzeroReservedBytes(position, first, second) {
11834
11880
  return build(WARNING_CODES.DICOM_NONZERO_RESERVED_BYTES, position, {
11835
- tag,
11836
11881
  n: first,
11837
11882
  n2: second
11838
11883
  });
@@ -11843,9 +11888,21 @@ function unParsedAsSQ(position, tag) {
11843
11888
  function sqNotDescended(position, tag) {
11844
11889
  return build(WARNING_CODES.DICOM_SQ_NOT_DESCENDED, position, { tag });
11845
11890
  }
11891
+ function itemCrossesSequenceEnd(position, tag, availableLength) {
11892
+ return build(WARNING_CODES.DICOM_ITEM_CROSSES_SEQUENCE_END, position, {
11893
+ tag,
11894
+ n2: availableLength
11895
+ });
11896
+ }
11846
11897
  function emptyItemInSequence(position, tag) {
11847
11898
  return build(WARNING_CODES.DICOM_EMPTY_ITEM_IN_SEQUENCE, position, { tag });
11848
11899
  }
11900
+ function duplicateTagInDataSet(position) {
11901
+ return build(WARNING_CODES.DICOM_DUPLICATE_TAG_IN_DATA_SET, position);
11902
+ }
11903
+ function duplicateFileMetaElement(position) {
11904
+ return build(WARNING_CODES.DICOM_DUPLICATE_FILE_META_ELEMENT, position);
11905
+ }
11849
11906
  function implicitVRForPrivateTagWithoutVR(position, tag) {
11850
11907
  return build(WARNING_CODES.DICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR, position, { tag });
11851
11908
  }
@@ -11855,6 +11912,12 @@ function privateCreatorUnknown(position, tag) {
11855
11912
  function burnedInAnnotationNotRemoved(position) {
11856
11913
  return build(WARNING_CODES.DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED, position);
11857
11914
  }
11915
+ function deidentMethodNotAdded(position) {
11916
+ return build(WARNING_CODES.DICOM_DEIDENT_METHOD_NOT_ADDED, position);
11917
+ }
11918
+ function deidentMethodPriorRetained(position) {
11919
+ return build(WARNING_CODES.DICOM_DEIDENT_METHOD_PRIOR_RETAINED, position);
11920
+ }
11858
11921
  function embeddedAttributeRemoved(position, tag, vr, count) {
11859
11922
  return build(WARNING_CODES.DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED, position, {
11860
11923
  tag,
@@ -12036,12 +12099,12 @@ function readExplicitElementHeader(cursor, _ctx, emit) {
12036
12099
  const vr = vrSlice.toString("ascii");
12037
12100
  let length;
12038
12101
  let headerLength;
12039
- if (LONG_FORM_VRS.has(vr)) {
12102
+ if (LONG_FORM_VRS.has(vr) || !isRecognizedVr(vr)) {
12040
12103
  const reserved0 = cursor.buffer[cursor.position];
12041
12104
  const reserved1 = cursor.buffer[cursor.position + 1];
12042
12105
  cursor.position += 2;
12043
12106
  if ((reserved0 ?? 0) !== 0 || (reserved1 ?? 0) !== 0) {
12044
- emit(nonzeroReservedBytes({ byteOffset: headerStart }, tag, reserved0 ?? 0, reserved1 ?? 0));
12107
+ emit(nonzeroReservedBytes({ byteOffset: headerStart }, reserved0 ?? 0, reserved1 ?? 0));
12045
12108
  }
12046
12109
  length = cursor.readUInt32();
12047
12110
  headerLength = 12;
@@ -12149,6 +12212,16 @@ function parseFileMeta(buffer$1, datasetStart, ctx, emit) {
12149
12212
  fmElements.push(next);
12150
12213
  }
12151
12214
  }
12215
+ const seenModeledTags = /* @__PURE__ */ new Set();
12216
+ if (declaredFmLength !== void 0) seenModeledTags.add("00020000");
12217
+ for (const e of fmElements) {
12218
+ if (!MODELED_FM_TAGS.has(e.tag)) continue;
12219
+ if (seenModeledTags.has(e.tag)) {
12220
+ emit(duplicateFileMetaElement({ byteOffset: e.byteOffset, fileMeta: true }));
12221
+ continue;
12222
+ }
12223
+ seenModeledTags.add(e.tag);
12224
+ }
12152
12225
  const tsElement = fmElements.find((e) => e.tag === "00020010");
12153
12226
  if (tsElement === void 0 || tsElement.vr !== "UI") {
12154
12227
  throw new DicomParseError(
@@ -12179,7 +12252,7 @@ function readExplicitLeElement(cursor) {
12179
12252
  const vr = cursor.slice(2).toString("ascii");
12180
12253
  let length;
12181
12254
  let headerLength;
12182
- if (LONG_FORM_VRS.has(vr)) {
12255
+ if (LONG_FORM_VRS.has(vr) || !isRecognizedVr(vr)) {
12183
12256
  cursor.slice(2);
12184
12257
  length = cursor.readUInt32();
12185
12258
  headerLength = 12;
@@ -12195,7 +12268,7 @@ function readExplicitLeElement(cursor) {
12195
12268
  if (bytesConsumed !== headerLength + length) {
12196
12269
  throw new RangeError("File Meta element accounting mismatch");
12197
12270
  }
12198
- return { tag, vr, value, bytesConsumed };
12271
+ return { tag, vr, value, bytesConsumed, byteOffset: headerStart };
12199
12272
  }
12200
12273
  function trimUI(buf) {
12201
12274
  let end = buf.length;
@@ -12635,6 +12708,14 @@ var Element = class {
12635
12708
  }
12636
12709
  };
12637
12710
 
12711
+ // src/parser/data-set-map.ts
12712
+ function defineElement(elements, element, emit) {
12713
+ if (elements.has(element.tag)) {
12714
+ emit(duplicateTagInDataSet({ byteOffset: element.byteOffset }));
12715
+ }
12716
+ elements.set(element.tag, element);
12717
+ }
12718
+
12638
12719
  // src/dataset/item.ts
12639
12720
  var Item = class extends Dataset {
12640
12721
  index;
@@ -12756,6 +12837,15 @@ function parseSequence(buffer, valueStart, ctx, emit, opts) {
12756
12837
  items.push(new Item({ warnings: [], elements: inner.elements, index: itemIndex }));
12757
12838
  cursor.position = inner.endOffset;
12758
12839
  } else {
12840
+ if (opts.explicitLength !== void 0 && endLimit < buffer.length && cursor.position + itemLength > endLimit) {
12841
+ emit(
12842
+ itemCrossesSequenceEnd(
12843
+ { byteOffset: itemHeaderStart },
12844
+ itemTag,
12845
+ Math.max(0, endLimit - cursor.position)
12846
+ )
12847
+ );
12848
+ }
12759
12849
  if (cursor.position + itemLength > buffer.length) {
12760
12850
  throw new DicomParseError(
12761
12851
  FATAL_CODES.INVALID_FILE_META,
@@ -12891,8 +12981,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
12891
12981
  cursor.position = seq.endOffset;
12892
12982
  const rawBytes = ctx.copyValues ? copyValueBytes(buffer.subarray(valueRawStart, cursor.position)) : buffer.subarray(valueRawStart, cursor.position);
12893
12983
  const privateCreator2 = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
12894
- elements.set(
12895
- tag,
12984
+ defineElement(
12985
+ elements,
12896
12986
  new Element({
12897
12987
  tag,
12898
12988
  vr,
@@ -12903,7 +12993,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
12903
12993
  littleEndian: true,
12904
12994
  items: seq.items,
12905
12995
  ...privateCreator2 !== void 0 ? { privateCreator: privateCreator2 } : {}
12906
- })
12996
+ }),
12997
+ emit
12907
12998
  );
12908
12999
  continue;
12909
13000
  }
@@ -12922,8 +13013,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
12922
13013
  cursor.position = cp246.endOffset;
12923
13014
  const rawBytes = ctx.copyValues ? copyValueBytes(buffer.subarray(valueRawStart, cursor.position)) : buffer.subarray(valueRawStart, cursor.position);
12924
13015
  const privateCreator2 = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
12925
- elements.set(
12926
- tag,
13016
+ defineElement(
13017
+ elements,
12927
13018
  new Element({
12928
13019
  tag,
12929
13020
  vr: "SQ",
@@ -12936,7 +13027,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
12936
13027
  littleEndian: true,
12937
13028
  items: cp246.items,
12938
13029
  ...privateCreator2 !== void 0 ? { privateCreator: privateCreator2 } : {}
12939
- })
13030
+ }),
13031
+ emit
12940
13032
  );
12941
13033
  continue;
12942
13034
  }
@@ -12972,8 +13064,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
12972
13064
  );
12973
13065
  if (seq.success) {
12974
13066
  const privateCreator2 = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
12975
- elements.set(
12976
- tag,
13067
+ defineElement(
13068
+ elements,
12977
13069
  new Element({
12978
13070
  tag,
12979
13071
  vr,
@@ -12984,7 +13076,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
12984
13076
  littleEndian: true,
12985
13077
  items: seq.items,
12986
13078
  ...privateCreator2 !== void 0 ? { privateCreator: privateCreator2 } : {}
12987
- })
13079
+ }),
13080
+ emit
12988
13081
  );
12989
13082
  continue;
12990
13083
  }
@@ -12997,8 +13090,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
12997
13090
  }
12998
13091
  applySpecificCharacterSet(tag, valueSlice, ctx, emit, position);
12999
13092
  const privateCreator = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
13000
- elements.set(
13001
- tag,
13093
+ defineElement(
13094
+ elements,
13002
13095
  new Element({
13003
13096
  tag,
13004
13097
  vr,
@@ -13010,7 +13103,8 @@ function parseImplicitLE(buffer, datasetStart, ctx, emit, opts = {}) {
13010
13103
  littleEndian: true,
13011
13104
  ...ctx.currentCharset !== void 0 ? { specificCharacterSet: ctx.currentCharset } : {},
13012
13105
  ...privateCreator !== void 0 ? { privateCreator } : {}
13013
- })
13106
+ }),
13107
+ emit
13014
13108
  );
13015
13109
  }
13016
13110
  return { elements, endOffset: cursor.position };
@@ -13117,8 +13211,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
13117
13211
  cursor.position = seqResult.endOffset;
13118
13212
  const rawBytes = ctx.copyValues ? copyValueBytes(buffer.subarray(headerStart, cursor.position)) : buffer.subarray(headerStart, cursor.position);
13119
13213
  const privateCreator2 = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
13120
- elements.set(
13121
- tag,
13214
+ defineElement(
13215
+ elements,
13122
13216
  new Element({
13123
13217
  tag,
13124
13218
  vr: "SQ",
@@ -13129,7 +13223,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
13129
13223
  littleEndian: mode.littleEndian,
13130
13224
  items: seqResult.items,
13131
13225
  ...privateCreator2 !== void 0 ? { privateCreator: privateCreator2 } : {}
13132
- })
13226
+ }),
13227
+ emit
13133
13228
  );
13134
13229
  continue;
13135
13230
  }
@@ -13144,8 +13239,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
13144
13239
  const result = parseSequence(buffer, valueStart2, ctx, emit, seqOpts);
13145
13240
  cursor.position = result.endOffset;
13146
13241
  const rawBytes = ctx.copyValues ? copyValueBytes(buffer.subarray(headerStart, cursor.position)) : buffer.subarray(headerStart, cursor.position);
13147
- elements.set(
13148
- tag,
13242
+ defineElement(
13243
+ elements,
13149
13244
  new Element({
13150
13245
  tag,
13151
13246
  vr: "OB",
@@ -13156,7 +13251,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
13156
13251
  byteOffset: headerStart,
13157
13252
  littleEndian: mode.littleEndian,
13158
13253
  items: result.items
13159
- })
13254
+ }),
13255
+ emit
13160
13256
  );
13161
13257
  continue;
13162
13258
  }
@@ -13175,8 +13271,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
13175
13271
  cursor.position = cp246.endOffset;
13176
13272
  const rawBytes = ctx.copyValues ? copyValueBytes(buffer.subarray(headerStart, cursor.position)) : buffer.subarray(headerStart, cursor.position);
13177
13273
  const privateCreator3 = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
13178
- elements.set(
13179
- tag,
13274
+ defineElement(
13275
+ elements,
13180
13276
  new Element({
13181
13277
  tag,
13182
13278
  vr: "SQ",
@@ -13189,15 +13285,16 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
13189
13285
  littleEndian: mode.littleEndian,
13190
13286
  items: cp246.items,
13191
13287
  ...privateCreator3 !== void 0 ? { privateCreator: privateCreator3 } : {}
13192
- })
13288
+ }),
13289
+ emit
13193
13290
  );
13194
13291
  continue;
13195
13292
  }
13196
13293
  const privateCreator2 = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
13197
13294
  const fallbackEnd = buffer.length;
13198
13295
  const fallbackBytes = ctx.copyValues ? copyValueBytes(buffer.subarray(headerStart, fallbackEnd)) : buffer.subarray(headerStart, fallbackEnd);
13199
- elements.set(
13200
- tag,
13296
+ defineElement(
13297
+ elements,
13201
13298
  new Element({
13202
13299
  tag,
13203
13300
  vr: "UN",
@@ -13207,7 +13304,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
13207
13304
  byteOffset: headerStart,
13208
13305
  littleEndian: mode.littleEndian,
13209
13306
  ...privateCreator2 !== void 0 ? { privateCreator: privateCreator2 } : {}
13210
- })
13307
+ }),
13308
+ emit
13211
13309
  );
13212
13310
  cursor.position = fallbackEnd;
13213
13311
  continue;
@@ -13242,8 +13340,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
13242
13340
  applySpecificCharacterSet(tag, valueSlice, ctx, emit, position);
13243
13341
  const privateCreator = safeModelCreator(resolvePrivateCreator(tag, ctx), ctx);
13244
13342
  const finalVr = vr;
13245
- elements.set(
13246
- tag,
13343
+ defineElement(
13344
+ elements,
13247
13345
  new Element({
13248
13346
  tag,
13249
13347
  vr: finalVr,
@@ -13255,7 +13353,8 @@ function _parseExplicit(buffer, datasetStart, ctx, emit, mode, opts = {}) {
13255
13353
  littleEndian: mode.littleEndian,
13256
13354
  ...ctx.currentCharset !== void 0 ? { specificCharacterSet: ctx.currentCharset } : {},
13257
13355
  ...privateCreator !== void 0 ? { privateCreator } : {}
13258
- })
13356
+ }),
13357
+ emit
13259
13358
  );
13260
13359
  }
13261
13360
  return { elements, endOffset: cursor.position };
@@ -13453,7 +13552,7 @@ function encodeDatasetElement(el, encoding) {
13453
13552
  }
13454
13553
  const littleEndian = encoding === "explicitLE";
13455
13554
  const vrBuf = buffer.Buffer.from(el.vr, "ascii");
13456
- if (LONG_FORM_VRS.has(el.vr)) {
13555
+ if (LONG_FORM_VRS.has(el.vr) || !isRecognizedVr(el.vr)) {
13457
13556
  return buffer.Buffer.concat([
13458
13557
  uint16(group, littleEndian),
13459
13558
  uint16(element, littleEndian),
@@ -13504,7 +13603,7 @@ function encodeMetaElement(tag, vr, rawValue) {
13504
13603
  const elementBuf = buffer.Buffer.alloc(2);
13505
13604
  elementBuf.writeUInt16LE(element, 0);
13506
13605
  const vrBuf = buffer.Buffer.from(vr, "ascii");
13507
- if (LONG_FORM_VRS.has(vr)) {
13606
+ if (LONG_FORM_VRS.has(vr) || !isRecognizedVr(vr)) {
13508
13607
  const lengthBuf2 = buffer.Buffer.alloc(4);
13509
13608
  lengthBuf2.writeUInt32LE(value.length, 0);
13510
13609
  return buffer.Buffer.concat([
@@ -14699,7 +14798,6 @@ function uidValueMultiplicity(rawBytes) {
14699
14798
  }
14700
14799
 
14701
14800
  // src/deident/embedded.ts
14702
- var KNOWN_VRS2 = new Set(Object.keys(BE_VR_STRIDE));
14703
14801
  var UNDEFINED_LENGTH4 = 4294967295;
14704
14802
  var FFFE_GROUP = 65534;
14705
14803
  var SCANNABLE_CARRIER_VRS = /* @__PURE__ */ new Set([
@@ -14760,7 +14858,7 @@ function decodeAt(value, off, encoding) {
14760
14858
  return next2 > value.length ? void 0 : { next: next2, tag };
14761
14859
  }
14762
14860
  const vr = value.toString("latin1", off + 4, off + 6);
14763
- if (!KNOWN_VRS2.has(vr)) return void 0;
14861
+ if (!KNOWN_VRS.has(vr)) return void 0;
14764
14862
  if (LONG_FORM_VRS.has(vr)) {
14765
14863
  if (off + 12 > value.length) return void 0;
14766
14864
  if (value[off + 6] !== 0 || value[off + 7] !== 0) return void 0;
@@ -14897,7 +14995,6 @@ var BODY_ENCODING2 = {
14897
14995
  };
14898
14996
  var MAX_UNAUDITABLE_SEQUENCE_FINDINGS = 64;
14899
14997
  var MAX_UNDEFINED_VR_FINDINGS = 64;
14900
- var KNOWN_VRS3 = new Set(Object.keys(BE_VR_STRIDE));
14901
14998
  function validateRetain(retain) {
14902
14999
  const valid = new Set(DEIDENTIFY_OPTIONS);
14903
15000
  const active = /* @__PURE__ */ new Set();
@@ -15018,7 +15115,7 @@ function actsOnTag(tag, ctx) {
15018
15115
  return resolveAction(effectiveCode(action, ctx.active)) !== "K";
15019
15116
  }
15020
15117
  function hasUndefinedVr(el) {
15021
- return !KNOWN_VRS3.has(el.vr);
15118
+ return !KNOWN_VRS.has(el.vr);
15022
15119
  }
15023
15120
  function keepOrEmpty(el, ctx, contextPath, out) {
15024
15121
  if (hasUndefinedVr(el)) {
@@ -15082,6 +15179,26 @@ function creatorFor(tag, creators) {
15082
15179
  const { group, element } = splitTag(tag);
15083
15180
  return creators.get(`${String(group)}:${String(element >> 8 & 255)}`);
15084
15181
  }
15182
+ var UNDEFINED_LENGTH5 = 4294967295;
15183
+ var EXPLICIT_SQ_HEADER_BYTES = 12;
15184
+ function itemStreamOverrunsSequence(el, encoding) {
15185
+ if (el.vr !== "SQ" || el.length === UNDEFINED_LENGTH5) return false;
15186
+ const headerBytes = encoding === "implicit" ? 0 : EXPLICIT_SQ_HEADER_BYTES;
15187
+ return el.rawBytes.length - headerBytes > el.length;
15188
+ }
15189
+ function settledBound(source, encoding) {
15190
+ let index = source.length;
15191
+ let byteOffset;
15192
+ for (const [at, el] of source.entries()) {
15193
+ if (!itemStreamOverrunsSequence(el, encoding) && !isUnauditableSequence(el)) continue;
15194
+ if (index === source.length) index = at + 1;
15195
+ if (byteOffset === void 0 || el.byteOffset < byteOffset) byteOffset = el.byteOffset;
15196
+ }
15197
+ return { index, byteOffset };
15198
+ }
15199
+ function isSettled(el, at, bound) {
15200
+ return at < bound.index && (bound.byteOffset === void 0 || el.byteOffset <= bound.byteOffset);
15201
+ }
15085
15202
  function keepsPrivate(el, ctx, creators) {
15086
15203
  if (!ctx.active.has("RetainSafePrivate") || ctx.profile === void 0) return false;
15087
15204
  if (isPrivateCreatorElement(el.tag)) {
@@ -15091,7 +15208,7 @@ function keepsPrivate(el, ctx, creators) {
15091
15208
  if (creator === void 0) return false;
15092
15209
  return resolvePrivateTag(ctx.profile, el.tag, creator) !== void 0;
15093
15210
  }
15094
- function processElements(source, ctx, contextPath) {
15211
+ function processElements(source, ctx, contextPath, reservationsUsable) {
15095
15212
  const out = {
15096
15213
  elements: /* @__PURE__ */ new Map(),
15097
15214
  attributes: [],
@@ -15101,10 +15218,12 @@ function processElements(source, ctx, contextPath) {
15101
15218
  undefinedVrElements: [],
15102
15219
  warnings: []
15103
15220
  };
15104
- const creators = creatorsInScope(source);
15105
- for (const el of source) {
15221
+ const bound = settledBound(source, ctx.encoding);
15222
+ const creators = creatorsInScope(source.filter((el, at) => isSettled(el, at, bound)));
15223
+ for (const [at, el] of source.entries()) {
15106
15224
  if (isPrivateTag(el.tag)) {
15107
- if (keepsPrivate(el, ctx, creators)) keepOrEmpty(el, ctx, contextPath, out);
15225
+ if (reservationsUsable && isSettled(el, at, bound) && keepsPrivate(el, ctx, creators))
15226
+ keepOrEmpty(el, ctx, contextPath, out);
15108
15227
  else out.removedPrivateTags.push(el.tag);
15109
15228
  continue;
15110
15229
  }
@@ -15112,7 +15231,8 @@ function processElements(source, ctx, contextPath) {
15112
15231
  if (action === void 0) {
15113
15232
  if (el.vr === "SQ") {
15114
15233
  if (isUnauditableSequence(el)) emptyUnauditableSequence(el, ctx, contextPath, out);
15115
- else out.elements.set(el.tag, descendSequence(el, ctx, contextPath, out));
15234
+ else
15235
+ out.elements.set(el.tag, descendSequence(el, ctx, contextPath, out, reservationsUsable));
15116
15236
  } else {
15117
15237
  keepOrEmpty(el, ctx, contextPath, out);
15118
15238
  }
@@ -15120,7 +15240,7 @@ function processElements(source, ctx, contextPath) {
15120
15240
  }
15121
15241
  const resolved = resolveAction(effectiveCode(action, ctx.active));
15122
15242
  if (el.vr === "SQ") {
15123
- applySequenceAction(el, resolved, action, ctx, contextPath, out);
15243
+ applySequenceAction(el, resolved, action, ctx, contextPath, out, reservationsUsable);
15124
15244
  continue;
15125
15245
  }
15126
15246
  let applied;
@@ -15161,11 +15281,12 @@ function processElements(source, ctx, contextPath) {
15161
15281
  }
15162
15282
  return out;
15163
15283
  }
15164
- function descendSequence(el, ctx, contextPath, out) {
15284
+ function descendSequence(el, ctx, contextPath, out, reservationsUsable) {
15285
+ const childReservationsUsable = reservationsUsable && !itemStreamOverrunsSequence(el, ctx.encoding);
15165
15286
  const newItems = [];
15166
15287
  (el.items ?? []).forEach((item, index) => {
15167
15288
  const childPath = [...contextPath, `${el.tag}[${String(index)}]`];
15168
- const inner = processElements(item.elements(), ctx, childPath);
15289
+ const inner = processElements(item.elements(), ctx, childPath, childReservationsUsable);
15169
15290
  out.attributes.push(...inner.attributes);
15170
15291
  out.removedPrivateTags.push(...inner.removedPrivateTags);
15171
15292
  out.embeddedAttributes.push(...inner.embeddedAttributes);
@@ -15176,7 +15297,7 @@ function descendSequence(el, ctx, contextPath, out) {
15176
15297
  });
15177
15298
  return rebuildSequence(el, newItems, ctx.encoding);
15178
15299
  }
15179
- function applySequenceAction(el, resolved, action, ctx, contextPath, out) {
15300
+ function applySequenceAction(el, resolved, action, ctx, contextPath, out, reservationsUsable) {
15180
15301
  let applied;
15181
15302
  switch (resolved) {
15182
15303
  case "X":
@@ -15192,7 +15313,7 @@ function applySequenceAction(el, resolved, action, ctx, contextPath, out) {
15192
15313
  emptyUnauditableSequence(el, ctx, contextPath, out);
15193
15314
  applied = "emptied";
15194
15315
  } else {
15195
- out.elements.set(el.tag, descendSequence(el, ctx, contextPath, out));
15316
+ out.elements.set(el.tag, descendSequence(el, ctx, contextPath, out, reservationsUsable));
15196
15317
  applied = "cleaned";
15197
15318
  }
15198
15319
  break;
@@ -15202,7 +15323,7 @@ function applySequenceAction(el, resolved, action, ctx, contextPath, out) {
15202
15323
  emptyUnauditableSequence(el, ctx, contextPath, out);
15203
15324
  applied = "emptied";
15204
15325
  } else {
15205
- out.elements.set(el.tag, descendSequence(el, ctx, contextPath, out));
15326
+ out.elements.set(el.tag, descendSequence(el, ctx, contextPath, out, reservationsUsable));
15206
15327
  applied = "kept";
15207
15328
  }
15208
15329
  break;
@@ -15234,6 +15355,47 @@ function defaultMethod(active) {
15234
15355
  const base = "Cosyte @cosyte/dicom: PS3.15 Basic Application Level Confidentiality Profile";
15235
15356
  return active.size === 0 ? base : `${base} + ${[...active].join(", ")}`;
15236
15357
  }
15358
+ var VALUE_DELIMITER = 92;
15359
+ var MAX_SHORT_FORM_VALUE_BYTES = 65534;
15360
+ function addDeidentificationMethod(existing, method) {
15361
+ const added = trimTrailingPad(buffer.Buffer.from(method, "latin1"));
15362
+ if (existing === void 0 || existing.vr !== "LO") {
15363
+ return { value: added, replacedPrior: false, retainedPrior: false };
15364
+ }
15365
+ const kept = buffer.Buffer.from(trimTrailingPad(existing.rawBytes));
15366
+ if (kept.length === 0) return { value: added, replacedPrior: false, retainedPrior: false };
15367
+ const keptValues = splitValues(kept).map(trimTrailingPad);
15368
+ const missing = added.length === 0 ? [] : splitValues(added).filter((v) => !keptValues.some((k) => k.equals(trimTrailingPad(v))));
15369
+ const parts = [kept];
15370
+ for (const value2 of missing) {
15371
+ parts.push(buffer.Buffer.from([VALUE_DELIMITER]), value2);
15372
+ }
15373
+ const value = trimTrailingPad(buffer.Buffer.concat(parts));
15374
+ if (value.length > MAX_SHORT_FORM_VALUE_BYTES) {
15375
+ return { value: added, replacedPrior: true, retainedPrior: false };
15376
+ }
15377
+ return { value, replacedPrior: false, retainedPrior: true };
15378
+ }
15379
+ function trimTrailingPad(value) {
15380
+ let end = value.length;
15381
+ while (end > 0) {
15382
+ const last = value[end - 1];
15383
+ if (last === 32 || last === 0) end--;
15384
+ else break;
15385
+ }
15386
+ return value.subarray(0, end);
15387
+ }
15388
+ function splitValues(value) {
15389
+ const out = [];
15390
+ let start = 0;
15391
+ for (let i = 0; i <= value.length; i++) {
15392
+ if (i === value.length || value[i] === VALUE_DELIMITER) {
15393
+ out.push(value.subarray(start, i));
15394
+ start = i + 1;
15395
+ }
15396
+ }
15397
+ return out;
15398
+ }
15237
15399
  function hasUncleanedBurnedIn(ds) {
15238
15400
  if (!ds.has(TAG_PIXEL_DATA)) return false;
15239
15401
  const flag = ds.get(TAG_BURNED_IN_ANNOTATION);
@@ -15253,7 +15415,7 @@ function deidentify(ds, options = {}) {
15253
15415
  encoding,
15254
15416
  budget: { unauditableSequences: 0, undefinedVrElements: 0 }
15255
15417
  };
15256
- const processed = processElements(ds.elements(), ctx, []);
15418
+ const processed = processElements(ds.elements(), ctx, [], true);
15257
15419
  const {
15258
15420
  elements,
15259
15421
  attributes,
@@ -15267,11 +15429,23 @@ function deidentify(ds, options = {}) {
15267
15429
  insertedScalar(TAG_PATIENT_IDENTITY_REMOVED, "CS", buffer.Buffer.from("YES", "latin1"), littleEndian)
15268
15430
  );
15269
15431
  const method = options.deidentificationMethod ?? defaultMethod(active);
15432
+ const priorMethod = elements.get(TAG_DEIDENTIFICATION_METHOD);
15433
+ const deidentMethod = addDeidentificationMethod(priorMethod, method);
15270
15434
  elements.set(
15271
15435
  TAG_DEIDENTIFICATION_METHOD,
15272
- insertedScalar(TAG_DEIDENTIFICATION_METHOD, "LO", buffer.Buffer.from(method, "latin1"), littleEndian)
15436
+ insertedScalar(TAG_DEIDENTIFICATION_METHOD, "LO", deidentMethod.value, littleEndian)
15273
15437
  );
15274
15438
  const warnings = [...processed.warnings];
15439
+ if (deidentMethod.replacedPrior) {
15440
+ warnings.push(
15441
+ deidentMethodNotAdded({ byteOffset: priorMethod?.byteOffset ?? 0, fileMeta: false })
15442
+ );
15443
+ }
15444
+ if (deidentMethod.retainedPrior) {
15445
+ warnings.push(
15446
+ deidentMethodPriorRetained({ byteOffset: priorMethod?.byteOffset ?? 0, fileMeta: false })
15447
+ );
15448
+ }
15275
15449
  if (hasUncleanedBurnedIn(ds)) {
15276
15450
  const offset = ds.get(TAG_PIXEL_DATA)?.byteOffset ?? 0;
15277
15451
  warnings.push(burnedInAnnotationNotRemoved({ byteOffset: offset, fileMeta: false }));