@cosyte/dicom 0.1.3 → 0.1.5

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
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.5
4
+
5
+ ### Patch Changes
6
+
7
+ - c827734: `serializeDicom()` and `deidentify()` now write a value over 65,534 bytes in a VR with a 16-bit length field as `UN`, as PS3.5 section 6.2.2 requires, instead of throwing a `RangeError`.
8
+
9
+ Under the Explicit VR transfer syntaxes, VRs such as `CS`, `DS`, `LO`, `PN`, `UI` and `US` carry a 16-bit length field, and the element encoder put every length through it unchecked. A value the reader tolerated at the odd length FFFFH, padded to 65,536 bytes on write, made `serializeDicom()` throw a `RangeError` it does not document, and so did a multi-valued UID that `deidentify()` remapped past 65,534 bytes; inside a Sequence, `deidentify()` threw it itself while rebuilding the Item. Such a value is now written in the long form with VR `UN`, Little Endian under Explicit VR Big Endian too, and reads back byte for byte. A Private Creator, for which the same section forbids `UN`, is not rewritten.
10
+
11
+ ## 0.1.4
12
+
13
+ ### Patch Changes
14
+
15
+ - d21f159: Decoding a string value now takes time linear in its length, so a long run of spaces, digits, or pad bytes no longer stalls `Element.value` or `parseSpecificCharacterSet()` for seconds.
16
+
17
+ Three checks on file-controlled text used patterns that backtracked quadratically on such a run: the trim of each value of a multi-valued string VR, the DS number grammar, and the trim of each Specific Character Set term. Each now scans the value once from each end and returns the same result as before. De-identification also strips every asterisk from the conditional action branch it takes, not only the first; no row of the Annex E table puts one there today, so no output changes.
18
+
3
19
  ## 0.1.3
4
20
 
5
21
  ### Patch Changes
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.1.3";
14
+ var VERSION = "0.1.5";
15
15
 
16
16
  // src/dictionary/index.ts
17
17
  var dictionary_exports = {};
@@ -13223,7 +13223,18 @@ var MULTIBYTE_LABELS = /* @__PURE__ */ new Set([
13223
13223
  ]);
13224
13224
  function parseSpecificCharacterSet(bytes) {
13225
13225
  const raw = bytes.toString("latin1");
13226
- return raw.split("\\").map((t) => t.replace(/[ ]+$/u, "").replace(/^[ ]+/u, "")).map((t) => t === "" || TERM_TO_LABEL.has(t) ? t : WITHHELD);
13226
+ return raw.split("\\").map(trimTermPad).map((t) => t === "" || TERM_TO_LABEL.has(t) ? t : WITHHELD);
13227
+ }
13228
+ function trimTermPad(term) {
13229
+ let end = term.length;
13230
+ while (end > 0) {
13231
+ const code = term.charCodeAt(end - 1);
13232
+ if (code !== 0 && code !== 32) break;
13233
+ end--;
13234
+ }
13235
+ let start = 0;
13236
+ while (start < end && term.charCodeAt(start) === 32) start++;
13237
+ return term.slice(start, end);
13227
13238
  }
13228
13239
  function isKnownCharsetTerm(term) {
13229
13240
  return TERM_TO_LABEL.has(term);
@@ -13790,6 +13801,14 @@ function stripPad(bytes, tag, vr, position, warnings) {
13790
13801
  }
13791
13802
  return bytes.subarray(0, end);
13792
13803
  }
13804
+ function trimSpaces(value) {
13805
+ let start = 0;
13806
+ let end = value.length;
13807
+ while (start < end && value.charCodeAt(start) === SPACE2) start++;
13808
+ while (end > start && value.charCodeAt(end - 1) === SPACE2) end--;
13809
+ return value.slice(start, end);
13810
+ }
13811
+ var DECIMAL_STRING = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/u;
13793
13812
  function attachWarnings(value, warnings) {
13794
13813
  return warnings.length > 0 ? { ...value, warnings } : value;
13795
13814
  }
@@ -13841,7 +13860,7 @@ function decodeElementValue(element) {
13841
13860
  }
13842
13861
  if (vr === "LO" || vr === "SH" || vr === "UC") {
13843
13862
  const text = decodeCharsetString(trimmed, tag, vr, charset, position, warnings);
13844
- const values = text.split("\\").map((v) => v.replace(/^ +| +$/gu, ""));
13863
+ const values = text.split("\\").map(trimSpaces);
13845
13864
  return attachWarnings({ kind: "strings", values }, warnings);
13846
13865
  }
13847
13866
  if (vr === "LT" || vr === "ST" || vr === "UT") {
@@ -13854,7 +13873,7 @@ function decodeElementValue(element) {
13854
13873
  }
13855
13874
  if (vr === "AE" || vr === "CS" || vr === "AS" || vr === "UI") {
13856
13875
  const text = decodeAsciiString(trimmed, tag, vr, position, warnings);
13857
- const values = text.split("\\").map((v) => v.replace(/^ +| +$/gu, ""));
13876
+ const values = text.split("\\").map(trimSpaces);
13858
13877
  return attachWarnings({ kind: "strings", values }, warnings);
13859
13878
  }
13860
13879
  if (vr === "DS") {
@@ -13862,7 +13881,7 @@ function decodeElementValue(element) {
13862
13881
  const values = text.split("\\").map((v) => {
13863
13882
  const t = v.trim();
13864
13883
  if (t.length === 0) return null;
13865
- if (!/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/u.test(t)) return null;
13884
+ if (!DECIMAL_STRING.test(t)) return null;
13866
13885
  const n = Number(t);
13867
13886
  return Number.isFinite(n) ? n : null;
13868
13887
  });
@@ -14924,6 +14943,7 @@ function toDate(value, options) {
14924
14943
  return Number.isNaN(instant.getTime()) ? void 0 : instant;
14925
14944
  }
14926
14945
  var UNDEFINED_LENGTH4 = 4294967295;
14946
+ var MAX_SHORT_FORM_LENGTH = 65534;
14927
14947
  var NULL_PAD_VRS = /* @__PURE__ */ new Set(["UI", "OB", "OW", "OF", "OD", "OL", "OV", "UN"]);
14928
14948
  function padValue(value, vr) {
14929
14949
  if (value.length % 2 === 0) return buffer.Buffer.from(value);
@@ -14942,6 +14962,19 @@ function uint32(n, littleEndian) {
14942
14962
  else buf.writeUInt32BE(n, 0);
14943
14963
  return buf;
14944
14964
  }
14965
+ function littleEndianValue(value, vr, littleEndian) {
14966
+ const stride = BE_VR_STRIDE[vr];
14967
+ if (littleEndian || stride === 0) return value;
14968
+ const swapped = buffer.Buffer.from(value);
14969
+ const whole = swapped.subarray(0, swapped.length - swapped.length % stride);
14970
+ if (stride === 2) whole.swap16();
14971
+ else if (stride === 4) whole.swap32();
14972
+ else whole.swap64();
14973
+ return swapped;
14974
+ }
14975
+ function isPrivateCreator(group, element) {
14976
+ return group % 2 === 1 && element >= 16 && element <= 255;
14977
+ }
14945
14978
  function isFullSpanElement(el, encoding) {
14946
14979
  if (el.length === UNDEFINED_LENGTH4) return true;
14947
14980
  return el.vr === "SQ" && encoding !== "implicit";
@@ -14973,6 +15006,17 @@ function encodeDatasetElement(el, encoding, rawBytes = el.rawBytes) {
14973
15006
  value
14974
15007
  ]);
14975
15008
  }
15009
+ if (value.length > MAX_SHORT_FORM_LENGTH && !isPrivateCreator(group, element)) {
15010
+ return buffer.Buffer.concat([
15011
+ uint16(group, littleEndian),
15012
+ uint16(element, littleEndian),
15013
+ buffer.Buffer.from("UN", "ascii"),
15014
+ buffer.Buffer.from([0, 0]),
15015
+ // reserved (PS3.5 §7.1.2)
15016
+ uint32(value.length, littleEndian),
15017
+ littleEndianValue(value, el.vr, littleEndian)
15018
+ ]);
15019
+ }
14976
15020
  return buffer.Buffer.concat([
14977
15021
  uint16(group, littleEndian),
14978
15022
  uint16(element, littleEndian),
@@ -16695,7 +16739,7 @@ function effectiveCode(action, active) {
16695
16739
  return action.basicProfile;
16696
16740
  }
16697
16741
  function resolveAction(code) {
16698
- const first = code.split("/", 1)[0]?.replace("*", "") ?? "K";
16742
+ const first = code.split("/", 1)[0]?.replaceAll("*", "") ?? "K";
16699
16743
  return first;
16700
16744
  }
16701
16745
  var TEXT_DUMMIES = {