dicom-curate 0.40.2 → 0.40.4

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.
@@ -86462,6 +86462,9 @@ var md5 = /* @__PURE__ */ createHasher(() => new _MD5());
86462
86462
  // src/hash.ts
86463
86463
  var import_js_crc = __toESM(require_crc(), 1);
86464
86464
  var DEFAULT_HASH_PART_SIZE = 5 * 1024 * 1024;
86465
+ function phiSafeToken(input) {
86466
+ return bytesToHex(sha256(new TextEncoder().encode(input))).slice(0, 16);
86467
+ }
86465
86468
  async function hashStream(stream, hashMethod, hashPartSize) {
86466
86469
  switch (hashMethod) {
86467
86470
  case "sha256":
@@ -86830,7 +86833,7 @@ async function curateOne({
86830
86833
  // fileInfo, keeping it in the private log. See #283.
86831
86834
  anomalies: [`Could not parse file as DICOM data`],
86832
86835
  errors: [`File is not a valid DICOM file or is corrupted`],
86833
- sourceInstanceUID: `invalid_${fileInfo.name.replace(/[^a-zA-Z0-9]/g, "_")}`,
86836
+ sourceInstanceUID: `invalid_${phiSafeToken(`${fileInfo.path}/${fileInfo.name}`)}`,
86834
86837
  fileInfo: {
86835
86838
  name: fileInfo.name,
86836
86839
  size: fileInfo.size,
@@ -86936,7 +86939,11 @@ async function curateOne({
86936
86939
  create: true
86937
86940
  });
86938
86941
  const writable = await fileHandle.createWritable();
86939
- await writable.write(modifiedBlob);
86942
+ for await (const chunk of readableStreamToAsyncIterable(
86943
+ modifiedBlob.stream()
86944
+ )) {
86945
+ await writable.write(chunk);
86946
+ }
86940
86947
  await writable.close();
86941
86948
  }
86942
86949
  } else if (typeof outputTarget?.directory === "string") {
@@ -80171,6 +80171,9 @@ var md5 = /* @__PURE__ */ createHasher(() => new _MD5());
80171
80171
  // src/hash.ts
80172
80172
  var import_js_crc = __toESM(require_crc(), 1);
80173
80173
  var DEFAULT_HASH_PART_SIZE = 5 * 1024 * 1024;
80174
+ function phiSafeToken(input) {
80175
+ return bytesToHex(sha256(new TextEncoder().encode(input))).slice(0, 16);
80176
+ }
80174
80177
  async function hashStream(stream, hashMethod, hashPartSize) {
80175
80178
  switch (hashMethod) {
80176
80179
  case "sha256":
@@ -80539,7 +80542,7 @@ async function curateOne({
80539
80542
  // fileInfo, keeping it in the private log. See #283.
80540
80543
  anomalies: [`Could not parse file as DICOM data`],
80541
80544
  errors: [`File is not a valid DICOM file or is corrupted`],
80542
- sourceInstanceUID: `invalid_${fileInfo.name.replace(/[^a-zA-Z0-9]/g, "_")}`,
80545
+ sourceInstanceUID: `invalid_${phiSafeToken(`${fileInfo.path}/${fileInfo.name}`)}`,
80543
80546
  fileInfo: {
80544
80547
  name: fileInfo.name,
80545
80548
  size: fileInfo.size,
@@ -80645,7 +80648,11 @@ async function curateOne({
80645
80648
  create: true
80646
80649
  });
80647
80650
  const writable = await fileHandle.createWritable();
80648
- await writable.write(modifiedBlob);
80651
+ for await (const chunk of readableStreamToAsyncIterable(
80652
+ modifiedBlob.stream()
80653
+ )) {
80654
+ await writable.write(chunk);
80655
+ }
80649
80656
  await writable.close();
80650
80657
  }
80651
80658
  } else if (typeof outputTarget?.directory === "string") {
package/dist/esm/hash.js CHANGED
@@ -800,6 +800,9 @@ var sha256 = /* @__PURE__ */ createHasher(
800
800
  // src/hash.ts
801
801
  var import_js_crc = __toESM(require_crc(), 1);
802
802
  var DEFAULT_HASH_PART_SIZE = 5 * 1024 * 1024;
803
+ function phiSafeToken(input) {
804
+ return bytesToHex(sha256(new TextEncoder().encode(input))).slice(0, 16);
805
+ }
803
806
  async function hash(buffer, hashMethod, hashPartSize) {
804
807
  switch (hashMethod) {
805
808
  case "sha256":
@@ -975,7 +978,8 @@ function crc64Hex(input) {
975
978
  }
976
979
  export {
977
980
  hash,
978
- hashStream
981
+ hashStream,
982
+ phiSafeToken
979
983
  };
980
984
  /*! Bundled license information:
981
985
 
package/dist/esm/index.js CHANGED
@@ -88025,6 +88025,9 @@ var md5 = /* @__PURE__ */ createHasher(() => new _MD5());
88025
88025
  // src/hash.ts
88026
88026
  var import_js_crc = __toESM(require_crc(), 1);
88027
88027
  var DEFAULT_HASH_PART_SIZE = 5 * 1024 * 1024;
88028
+ function phiSafeToken(input) {
88029
+ return bytesToHex(sha256(new TextEncoder().encode(input))).slice(0, 16);
88030
+ }
88028
88031
  async function hash(buffer, hashMethod, hashPartSize) {
88029
88032
  switch (hashMethod) {
88030
88033
  case "sha256":
@@ -88433,7 +88436,7 @@ async function curateOne({
88433
88436
  // fileInfo, keeping it in the private log. See #283.
88434
88437
  anomalies: [`Could not parse file as DICOM data`],
88435
88438
  errors: [`File is not a valid DICOM file or is corrupted`],
88436
- sourceInstanceUID: `invalid_${fileInfo.name.replace(/[^a-zA-Z0-9]/g, "_")}`,
88439
+ sourceInstanceUID: `invalid_${phiSafeToken(`${fileInfo.path}/${fileInfo.name}`)}`,
88437
88440
  fileInfo: {
88438
88441
  name: fileInfo.name,
88439
88442
  size: fileInfo.size,
@@ -88539,7 +88542,11 @@ async function curateOne({
88539
88542
  create: true
88540
88543
  });
88541
88544
  const writable = await fileHandle.createWritable();
88542
- await writable.write(modifiedBlob);
88545
+ for await (const chunk of readableStreamToAsyncIterable(
88546
+ modifiedBlob.stream()
88547
+ )) {
88548
+ await writable.write(chunk);
88549
+ }
88543
88550
  await writable.close();
88544
88551
  }
88545
88552
  } else if (typeof outputTarget?.directory === "string") {
@@ -94710,7 +94717,9 @@ async function dispatchMappingJobs() {
94710
94717
  const hasReadErrors = !!errors && errors.length > 0;
94711
94718
  if (hasReadErrors) {
94712
94719
  const scanErrorResult = {
94713
- sourceInstanceUID: `scan_${fileInfo.name.replace(/[^a-zA-Z0-9]/g, "_")}`,
94720
+ // phiSafeToken rather than the sanitised name: this result reaches
94721
+ // the server-bound log, so the UID must not encode the filename.
94722
+ sourceInstanceUID: `scan_${phiSafeToken(`${fileInfo.path}/${fileInfo.name}`)}`,
94714
94723
  // Intentionally no outputFilePath: an unread file has no
94715
94724
  // de-identified output path, and we must not place the raw input
94716
94725
  // path in the server-bound channel.
@@ -86462,6 +86462,9 @@ var md5 = /* @__PURE__ */ createHasher(() => new _MD5());
86462
86462
  // src/hash.ts
86463
86463
  var import_js_crc = __toESM(require_crc(), 1);
86464
86464
  var DEFAULT_HASH_PART_SIZE = 5 * 1024 * 1024;
86465
+ function phiSafeToken(input) {
86466
+ return bytesToHex(sha256(new TextEncoder().encode(input))).slice(0, 16);
86467
+ }
86465
86468
  async function hashStream(stream, hashMethod, hashPartSize) {
86466
86469
  switch (hashMethod) {
86467
86470
  case "sha256":
@@ -86830,7 +86833,7 @@ async function curateOne({
86830
86833
  // fileInfo, keeping it in the private log. See #283.
86831
86834
  anomalies: [`Could not parse file as DICOM data`],
86832
86835
  errors: [`File is not a valid DICOM file or is corrupted`],
86833
- sourceInstanceUID: `invalid_${fileInfo.name.replace(/[^a-zA-Z0-9]/g, "_")}`,
86836
+ sourceInstanceUID: `invalid_${phiSafeToken(`${fileInfo.path}/${fileInfo.name}`)}`,
86834
86837
  fileInfo: {
86835
86838
  name: fileInfo.name,
86836
86839
  size: fileInfo.size,
@@ -86936,7 +86939,11 @@ async function curateOne({
86936
86939
  create: true
86937
86940
  });
86938
86941
  const writable = await fileHandle.createWritable();
86939
- await writable.write(modifiedBlob);
86942
+ for await (const chunk of readableStreamToAsyncIterable(
86943
+ modifiedBlob.stream()
86944
+ )) {
86945
+ await writable.write(chunk);
86946
+ }
86940
86947
  await writable.close();
86941
86948
  }
86942
86949
  } else if (typeof outputTarget?.directory === "string") {
@@ -93110,7 +93117,9 @@ async function dispatchMappingJobs() {
93110
93117
  const hasReadErrors = !!errors && errors.length > 0;
93111
93118
  if (hasReadErrors) {
93112
93119
  const scanErrorResult = {
93113
- sourceInstanceUID: `scan_${fileInfo.name.replace(/[^a-zA-Z0-9]/g, "_")}`,
93120
+ // phiSafeToken rather than the sanitised name: this result reaches
93121
+ // the server-bound log, so the UID must not encode the filename.
93122
+ sourceInstanceUID: `scan_${phiSafeToken(`${fileInfo.path}/${fileInfo.name}`)}`,
93114
93123
  // Intentionally no outputFilePath: an unread file has no
93115
93124
  // de-identified output path, and we must not place the raw input
93116
93125
  // path in the server-bound channel.
@@ -37137,7 +37137,15 @@ var DEFAULT_EXCLUDED_FILETYPES = [
37137
37137
  ".ds_store"
37138
37138
  ];
37139
37139
  function safeReadErrorMessage(error2) {
37140
- const detail = error2 instanceof Error ? `${error2.name}: ${error2.message}` : String(error2);
37140
+ let detail = "unknown error";
37141
+ if (error2 && typeof error2 === "object") {
37142
+ const { code, name } = error2;
37143
+ if (typeof code === "string" && code) {
37144
+ detail = code;
37145
+ } else if (typeof name === "string" && name) {
37146
+ detail = name;
37147
+ }
37148
+ }
37141
37149
  return `Unable to read file (filesystem error): ${detail}`;
37142
37150
  }
37143
37151
  var keepScanning = true;
@@ -1,4 +1,12 @@
1
1
  import type { THashMethod } from './types';
2
+ /**
3
+ * Derive a short, stable, PHI-safe token from a string (e.g. a file path).
4
+ * Used to build synthetic sourceInstanceUIDs for files that failed to read or
5
+ * parse, where embedding the (sanitised) filename would leak PHI into the
6
+ * server-bound log. Same input always yields the same token, so results stay
7
+ * correlatable across passes and runs.
8
+ */
9
+ export declare function phiSafeToken(input: string): string;
2
10
  export declare function hash(buffer: ArrayBuffer, hashMethod: THashMethod, hashPartSize?: number): Promise<string>;
3
11
  /**
4
12
  * Computes a hash over a stream of data chunks without materializing the full
@@ -54699,6 +54699,16 @@
54699
54699
  var crcExports = requireCrc();
54700
54700
 
54701
54701
  const DEFAULT_HASH_PART_SIZE = 5 * 1024 * 1024; // 5 MB — matches @aws-sdk/lib-storage default
54702
+ /**
54703
+ * Derive a short, stable, PHI-safe token from a string (e.g. a file path).
54704
+ * Used to build synthetic sourceInstanceUIDs for files that failed to read or
54705
+ * parse, where embedding the (sanitised) filename would leak PHI into the
54706
+ * server-bound log. Same input always yields the same token, so results stay
54707
+ * correlatable across passes and runs.
54708
+ */
54709
+ function phiSafeToken(input) {
54710
+ return bytesToHex(sha256(new TextEncoder().encode(input))).slice(0, 16);
54711
+ }
54702
54712
  /**
54703
54713
  * Computes a hash over a stream of data chunks without materializing the full
54704
54714
  * data into memory at once.
@@ -55137,7 +55147,7 @@
55137
55147
  // fileInfo, keeping it in the private log. See #283.
55138
55148
  anomalies: [`Could not parse file as DICOM data`],
55139
55149
  errors: [`File is not a valid DICOM file or is corrupted`],
55140
- sourceInstanceUID: `invalid_${fileInfo.name.replace(/[^a-zA-Z0-9]/g, '_')}`,
55150
+ sourceInstanceUID: `invalid_${phiSafeToken(`${fileInfo.path}/${fileInfo.name}`)}`,
55141
55151
  fileInfo: {
55142
55152
  name: fileInfo.name,
55143
55153
  size: fileInfo.size,
@@ -55258,7 +55268,12 @@
55258
55268
  create: true,
55259
55269
  });
55260
55270
  const writable = await fileHandle.createWritable();
55261
- await writable.write(modifiedBlob);
55271
+ // Write via stream() rather than write(modifiedBlob): LazyCompositeBlob
55272
+ // keeps an empty native Blob body and exposes its bytes only through the
55273
+ // overridden stream(), which write(blob) bypasses (yielding 0-byte files).
55274
+ for await (const chunk of readableStreamToAsyncIterable(modifiedBlob.stream())) {
55275
+ await writable.write(chunk);
55276
+ }
55262
55277
  await writable.close();
55263
55278
  }
55264
55279
  }
@@ -68864,13 +68879,24 @@
68864
68879
  ];
68865
68880
  /**
68866
68881
  * Build a PHI-safe error message for a file that could not be read during
68867
- * scanning. Intentionally omits the filename/path so the string is safe to
68868
- * place in `errors` (which is shared between the private and server-bound
68869
- * logs). The raw path/name is carried separately in `fileInfo` so it appears
68870
- * only in the private (input) log.
68882
+ * scanning. Only the error code/name is included never `error.message`,
68883
+ * because node fs errors embed the full raw path in the message (e.g.
68884
+ * "ENOENT: no such file or directory, stat '/path/to/file.dcm'") and this
68885
+ * string goes into `errors`, which is shared between the private and
68886
+ * server-bound logs. The raw path/name is carried separately in `fileInfo`
68887
+ * so it appears only in the private (input) log.
68871
68888
  */
68872
68889
  function safeReadErrorMessage(error) {
68873
- const detail = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
68890
+ let detail = 'unknown error';
68891
+ if (error && typeof error === 'object') {
68892
+ const { code, name } = error;
68893
+ if (typeof code === 'string' && code) {
68894
+ detail = code;
68895
+ }
68896
+ else if (typeof name === 'string' && name) {
68897
+ detail = name;
68898
+ }
68899
+ }
68874
68900
  return `Unable to read file (filesystem error): ${detail}`;
68875
68901
  }
68876
68902
  let keepScanning = true;
@@ -126358,6 +126384,16 @@
126358
126384
  var crcExports = requireCrc();
126359
126385
 
126360
126386
  const DEFAULT_HASH_PART_SIZE = 5 * 1024 * 1024; // 5 MB — matches @aws-sdk/lib-storage default
126387
+ /**
126388
+ * Derive a short, stable, PHI-safe token from a string (e.g. a file path).
126389
+ * Used to build synthetic sourceInstanceUIDs for files that failed to read or
126390
+ * parse, where embedding the (sanitised) filename would leak PHI into the
126391
+ * server-bound log. Same input always yields the same token, so results stay
126392
+ * correlatable across passes and runs.
126393
+ */
126394
+ function phiSafeToken(input) {
126395
+ return bytesToHex(sha256(new TextEncoder().encode(input))).slice(0, 16);
126396
+ }
126361
126397
  async function hash(buffer, hashMethod, hashPartSize) {
126362
126398
  switch (hashMethod) {
126363
126399
  case 'sha256':
@@ -126857,7 +126893,7 @@
126857
126893
  // fileInfo, keeping it in the private log. See #283.
126858
126894
  anomalies: [`Could not parse file as DICOM data`],
126859
126895
  errors: [`File is not a valid DICOM file or is corrupted`],
126860
- sourceInstanceUID: `invalid_${fileInfo.name.replace(/[^a-zA-Z0-9]/g, '_')}`,
126896
+ sourceInstanceUID: `invalid_${phiSafeToken(`${fileInfo.path}/${fileInfo.name}`)}`,
126861
126897
  fileInfo: {
126862
126898
  name: fileInfo.name,
126863
126899
  size: fileInfo.size,
@@ -126978,7 +127014,12 @@
126978
127014
  create: true,
126979
127015
  });
126980
127016
  const writable = await fileHandle.createWritable();
126981
- await writable.write(modifiedBlob);
127017
+ // Write via stream() rather than write(modifiedBlob): LazyCompositeBlob
127018
+ // keeps an empty native Blob body and exposes its bytes only through the
127019
+ // overridden stream(), which write(blob) bypasses (yielding 0-byte files).
127020
+ for await (const chunk of readableStreamToAsyncIterable(modifiedBlob.stream())) {
127021
+ await writable.write(chunk);
127022
+ }
126982
127023
  await writable.close();
126983
127024
  }
126984
127025
  }
@@ -140883,7 +140924,9 @@
140883
140924
  const hasReadErrors = !!errors && errors.length > 0;
140884
140925
  if (hasReadErrors) {
140885
140926
  const scanErrorResult = {
140886
- sourceInstanceUID: `scan_${fileInfo.name.replace(/[^a-zA-Z0-9]/g, '_')}`,
140927
+ // phiSafeToken rather than the sanitised name: this result reaches
140928
+ // the server-bound log, so the UID must not encode the filename.
140929
+ sourceInstanceUID: `scan_${phiSafeToken(`${fileInfo.path}/${fileInfo.name}`)}`,
140887
140930
  // Intentionally no outputFilePath: an unread file has no
140888
140931
  // de-identified output path, and we must not place the raw input
140889
140932
  // path in the server-bound channel.