dicom-curate 0.40.1 → 0.40.3
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/esm/applyMappingsWorker.js +10 -5
- package/dist/esm/curateOne.js +10 -5
- package/dist/esm/hash.js +5 -1
- package/dist/esm/index.js +34 -11
- package/dist/esm/mappingWorkerPool.js +32 -9
- package/dist/esm/scanDirectoryWorker.js +68 -2
- package/dist/types/hash.d.ts +8 -0
- package/dist/types/mappingWorkerPool.d.ts +1 -0
- package/dist/types/scanDirectoryWorker.d.ts +9 -0
- package/dist/umd/dicom-curate.umd.js +162 -21
- package/dist/umd/dicom-curate.umd.js.map +1 -1
- package/dist/umd/dicom-curate.umd.min.js +2 -2
- package/dist/umd/dicom-curate.umd.min.js.map +1 -1
- package/package.json +1 -1
|
@@ -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":
|
|
@@ -86824,11 +86827,13 @@ async function curateOne({
|
|
|
86824
86827
|
error3
|
|
86825
86828
|
);
|
|
86826
86829
|
const mapResults = {
|
|
86827
|
-
|
|
86828
|
-
errors
|
|
86829
|
-
|
|
86830
|
-
|
|
86831
|
-
|
|
86830
|
+
// PHI-safe strings: the raw filename is intentionally omitted because
|
|
86831
|
+
// anomalies/errors are shared between the private (input) log and the
|
|
86832
|
+
// server-bound (output) log. The raw name/path is carried only in
|
|
86833
|
+
// fileInfo, keeping it in the private log. See #283.
|
|
86834
|
+
anomalies: [`Could not parse file as DICOM data`],
|
|
86835
|
+
errors: [`File is not a valid DICOM file or is corrupted`],
|
|
86836
|
+
sourceInstanceUID: `invalid_${phiSafeToken(`${fileInfo.path}/${fileInfo.name}`)}`,
|
|
86832
86837
|
fileInfo: {
|
|
86833
86838
|
name: fileInfo.name,
|
|
86834
86839
|
size: fileInfo.size,
|
package/dist/esm/curateOne.js
CHANGED
|
@@ -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":
|
|
@@ -80533,11 +80536,13 @@ async function curateOne({
|
|
|
80533
80536
|
error3
|
|
80534
80537
|
);
|
|
80535
80538
|
const mapResults = {
|
|
80536
|
-
|
|
80537
|
-
errors
|
|
80538
|
-
|
|
80539
|
-
|
|
80540
|
-
|
|
80539
|
+
// PHI-safe strings: the raw filename is intentionally omitted because
|
|
80540
|
+
// anomalies/errors are shared between the private (input) log and the
|
|
80541
|
+
// server-bound (output) log. The raw name/path is carried only in
|
|
80542
|
+
// fileInfo, keeping it in the private log. See #283.
|
|
80543
|
+
anomalies: [`Could not parse file as DICOM data`],
|
|
80544
|
+
errors: [`File is not a valid DICOM file or is corrupted`],
|
|
80545
|
+
sourceInstanceUID: `invalid_${phiSafeToken(`${fileInfo.path}/${fileInfo.name}`)}`,
|
|
80541
80546
|
fileInfo: {
|
|
80542
80547
|
name: fileInfo.name,
|
|
80543
80548
|
size: fileInfo.size,
|
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":
|
|
@@ -88427,11 +88430,13 @@ async function curateOne({
|
|
|
88427
88430
|
error3
|
|
88428
88431
|
);
|
|
88429
88432
|
const mapResults = {
|
|
88430
|
-
|
|
88431
|
-
errors
|
|
88432
|
-
|
|
88433
|
-
|
|
88434
|
-
|
|
88433
|
+
// PHI-safe strings: the raw filename is intentionally omitted because
|
|
88434
|
+
// anomalies/errors are shared between the private (input) log and the
|
|
88435
|
+
// server-bound (output) log. The raw name/path is carried only in
|
|
88436
|
+
// fileInfo, keeping it in the private log. See #283.
|
|
88437
|
+
anomalies: [`Could not parse file as DICOM data`],
|
|
88438
|
+
errors: [`File is not a valid DICOM file or is corrupted`],
|
|
88439
|
+
sourceInstanceUID: `invalid_${phiSafeToken(`${fileInfo.path}/${fileInfo.name}`)}`,
|
|
88435
88440
|
fileInfo: {
|
|
88436
88441
|
name: fileInfo.name,
|
|
88437
88442
|
size: fileInfo.size,
|
|
@@ -94704,8 +94709,26 @@ async function dispatchMappingJobs() {
|
|
|
94704
94709
|
console.log("job is finished");
|
|
94705
94710
|
if (!mapResultsList)
|
|
94706
94711
|
mapResultsList = [];
|
|
94707
|
-
|
|
94708
|
-
|
|
94712
|
+
scanAnomalies.forEach(({ fileInfo, anomalies, errors }) => {
|
|
94713
|
+
const hasReadErrors = !!errors && errors.length > 0;
|
|
94714
|
+
if (hasReadErrors) {
|
|
94715
|
+
const scanErrorResult = {
|
|
94716
|
+
// phiSafeToken rather than the sanitised name: this result reaches
|
|
94717
|
+
// the server-bound log, so the UID must not encode the filename.
|
|
94718
|
+
sourceInstanceUID: `scan_${phiSafeToken(`${fileInfo.path}/${fileInfo.name}`)}`,
|
|
94719
|
+
// Intentionally no outputFilePath: an unread file has no
|
|
94720
|
+
// de-identified output path, and we must not place the raw input
|
|
94721
|
+
// path in the server-bound channel.
|
|
94722
|
+
mappings: {},
|
|
94723
|
+
anomalies: anomalies ?? [],
|
|
94724
|
+
errors,
|
|
94725
|
+
quarantine: {},
|
|
94726
|
+
fileInfo
|
|
94727
|
+
};
|
|
94728
|
+
mapResultsList.push(scanErrorResult);
|
|
94729
|
+
return;
|
|
94730
|
+
}
|
|
94731
|
+
if (!mappingWorkerOptions.skipWrite && anomalies.length > 0) {
|
|
94709
94732
|
const scanAnomalyResult = {
|
|
94710
94733
|
sourceInstanceUID: `scan_${fileInfo.name.replace(/[^a-zA-Z0-9]/g, "_")}`,
|
|
94711
94734
|
outputFilePath: `${fileInfo.path}/${fileInfo.name}`,
|
|
@@ -94717,8 +94740,8 @@ async function dispatchMappingJobs() {
|
|
|
94717
94740
|
quarantine: {}
|
|
94718
94741
|
};
|
|
94719
94742
|
mapResultsList.push(scanAnomalyResult);
|
|
94720
|
-
}
|
|
94721
|
-
}
|
|
94743
|
+
}
|
|
94744
|
+
});
|
|
94722
94745
|
progressCallback({
|
|
94723
94746
|
response: "done",
|
|
94724
94747
|
mapResultsList,
|
|
@@ -94945,8 +94968,8 @@ async function initializeFileListWorker(rejectCallback) {
|
|
|
94945
94968
|
break;
|
|
94946
94969
|
}
|
|
94947
94970
|
case "scanAnomalies": {
|
|
94948
|
-
const { fileInfo: anomalyFileInfo, anomalies } = event.data;
|
|
94949
|
-
scanAnomalies.push({ fileInfo: anomalyFileInfo, anomalies });
|
|
94971
|
+
const { fileInfo: anomalyFileInfo, anomalies, errors } = event.data;
|
|
94972
|
+
scanAnomalies.push({ fileInfo: anomalyFileInfo, anomalies, errors });
|
|
94950
94973
|
break;
|
|
94951
94974
|
}
|
|
94952
94975
|
case "count": {
|
|
@@ -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":
|
|
@@ -86824,11 +86827,13 @@ async function curateOne({
|
|
|
86824
86827
|
error3
|
|
86825
86828
|
);
|
|
86826
86829
|
const mapResults = {
|
|
86827
|
-
|
|
86828
|
-
errors
|
|
86829
|
-
|
|
86830
|
-
|
|
86831
|
-
|
|
86830
|
+
// PHI-safe strings: the raw filename is intentionally omitted because
|
|
86831
|
+
// anomalies/errors are shared between the private (input) log and the
|
|
86832
|
+
// server-bound (output) log. The raw name/path is carried only in
|
|
86833
|
+
// fileInfo, keeping it in the private log. See #283.
|
|
86834
|
+
anomalies: [`Could not parse file as DICOM data`],
|
|
86835
|
+
errors: [`File is not a valid DICOM file or is corrupted`],
|
|
86836
|
+
sourceInstanceUID: `invalid_${phiSafeToken(`${fileInfo.path}/${fileInfo.name}`)}`,
|
|
86832
86837
|
fileInfo: {
|
|
86833
86838
|
name: fileInfo.name,
|
|
86834
86839
|
size: fileInfo.size,
|
|
@@ -93104,8 +93109,26 @@ async function dispatchMappingJobs() {
|
|
|
93104
93109
|
console.log("job is finished");
|
|
93105
93110
|
if (!mapResultsList)
|
|
93106
93111
|
mapResultsList = [];
|
|
93107
|
-
|
|
93108
|
-
|
|
93112
|
+
scanAnomalies.forEach(({ fileInfo, anomalies, errors }) => {
|
|
93113
|
+
const hasReadErrors = !!errors && errors.length > 0;
|
|
93114
|
+
if (hasReadErrors) {
|
|
93115
|
+
const scanErrorResult = {
|
|
93116
|
+
// phiSafeToken rather than the sanitised name: this result reaches
|
|
93117
|
+
// the server-bound log, so the UID must not encode the filename.
|
|
93118
|
+
sourceInstanceUID: `scan_${phiSafeToken(`${fileInfo.path}/${fileInfo.name}`)}`,
|
|
93119
|
+
// Intentionally no outputFilePath: an unread file has no
|
|
93120
|
+
// de-identified output path, and we must not place the raw input
|
|
93121
|
+
// path in the server-bound channel.
|
|
93122
|
+
mappings: {},
|
|
93123
|
+
anomalies: anomalies ?? [],
|
|
93124
|
+
errors,
|
|
93125
|
+
quarantine: {},
|
|
93126
|
+
fileInfo
|
|
93127
|
+
};
|
|
93128
|
+
mapResultsList.push(scanErrorResult);
|
|
93129
|
+
return;
|
|
93130
|
+
}
|
|
93131
|
+
if (!mappingWorkerOptions.skipWrite && anomalies.length > 0) {
|
|
93109
93132
|
const scanAnomalyResult = {
|
|
93110
93133
|
sourceInstanceUID: `scan_${fileInfo.name.replace(/[^a-zA-Z0-9]/g, "_")}`,
|
|
93111
93134
|
outputFilePath: `${fileInfo.path}/${fileInfo.name}`,
|
|
@@ -93117,8 +93140,8 @@ async function dispatchMappingJobs() {
|
|
|
93117
93140
|
quarantine: {}
|
|
93118
93141
|
};
|
|
93119
93142
|
mapResultsList.push(scanAnomalyResult);
|
|
93120
|
-
}
|
|
93121
|
-
}
|
|
93143
|
+
}
|
|
93144
|
+
});
|
|
93122
93145
|
progressCallback({
|
|
93123
93146
|
response: "done",
|
|
93124
93147
|
mapResultsList,
|
|
@@ -37136,6 +37136,18 @@ var DEFAULT_EXCLUDED_FILETYPES = [
|
|
|
37136
37136
|
"thumbs.db",
|
|
37137
37137
|
".ds_store"
|
|
37138
37138
|
];
|
|
37139
|
+
function safeReadErrorMessage(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
|
+
}
|
|
37149
|
+
return `Unable to read file (filesystem error): ${detail}`;
|
|
37150
|
+
}
|
|
37139
37151
|
var keepScanning = true;
|
|
37140
37152
|
var pauseResolve = null;
|
|
37141
37153
|
var pausePromise = null;
|
|
@@ -37445,9 +37457,36 @@ async function scanDirectory(dir) {
|
|
|
37445
37457
|
if (!keepScanning)
|
|
37446
37458
|
return;
|
|
37447
37459
|
if (entry.kind === "file") {
|
|
37448
|
-
const file = await entry.getFile();
|
|
37449
37460
|
const key = `${prefix}/${entry.name}`;
|
|
37450
37461
|
const prev = previousIndex ? previousIndex[key] : void 0;
|
|
37462
|
+
let file;
|
|
37463
|
+
try {
|
|
37464
|
+
file = await entry.getFile();
|
|
37465
|
+
} catch (readError) {
|
|
37466
|
+
if (cheapFilterNameOnly(entry.name, key)) {
|
|
37467
|
+
totalDiscovered--;
|
|
37468
|
+
globalThis.postMessage({
|
|
37469
|
+
response: "count",
|
|
37470
|
+
totalDiscovered
|
|
37471
|
+
});
|
|
37472
|
+
}
|
|
37473
|
+
globalThis.postMessage({
|
|
37474
|
+
response: "scanAnomalies",
|
|
37475
|
+
fileInfo: {
|
|
37476
|
+
path: prefix,
|
|
37477
|
+
name: entry.name,
|
|
37478
|
+
size: 0,
|
|
37479
|
+
kind: "handle",
|
|
37480
|
+
fileHandle: entry
|
|
37481
|
+
},
|
|
37482
|
+
anomalies: [],
|
|
37483
|
+
errors: [safeReadErrorMessage(readError)],
|
|
37484
|
+
previousFileInfo: prev
|
|
37485
|
+
});
|
|
37486
|
+
if (!await waitIfPaused())
|
|
37487
|
+
return;
|
|
37488
|
+
continue;
|
|
37489
|
+
}
|
|
37451
37490
|
const fileAnomalies = [];
|
|
37452
37491
|
if (await shouldProcessFile(file, fileAnomalies, key)) {
|
|
37453
37492
|
globalThis.postMessage({
|
|
@@ -37545,9 +37584,36 @@ async function scanDirectoryNode(dirPath) {
|
|
|
37545
37584
|
return;
|
|
37546
37585
|
if (entry.isFile()) {
|
|
37547
37586
|
const filePath = path.join(currentPath, entry.name);
|
|
37548
|
-
const stats = await fs.stat(filePath);
|
|
37549
37587
|
const key = `${prefix}/${entry.name}`;
|
|
37550
37588
|
const prev = previousIndex ? previousIndex[key] : void 0;
|
|
37589
|
+
let stats;
|
|
37590
|
+
try {
|
|
37591
|
+
stats = await fs.stat(filePath);
|
|
37592
|
+
} catch (readError) {
|
|
37593
|
+
if (cheapFilterNameOnly(entry.name, key)) {
|
|
37594
|
+
totalDiscovered--;
|
|
37595
|
+
globalThis.postMessage({
|
|
37596
|
+
response: "count",
|
|
37597
|
+
totalDiscovered
|
|
37598
|
+
});
|
|
37599
|
+
}
|
|
37600
|
+
globalThis.postMessage({
|
|
37601
|
+
response: "scanAnomalies",
|
|
37602
|
+
fileInfo: {
|
|
37603
|
+
path: prefix,
|
|
37604
|
+
name: entry.name,
|
|
37605
|
+
size: 0,
|
|
37606
|
+
kind: "path",
|
|
37607
|
+
fullPath: filePath
|
|
37608
|
+
},
|
|
37609
|
+
anomalies: [],
|
|
37610
|
+
errors: [safeReadErrorMessage(readError)],
|
|
37611
|
+
previousFileInfo: prev
|
|
37612
|
+
});
|
|
37613
|
+
if (!await waitIfPaused())
|
|
37614
|
+
return;
|
|
37615
|
+
continue;
|
|
37616
|
+
}
|
|
37551
37617
|
const fileAnomalies = [];
|
|
37552
37618
|
if (await shouldProcessFileNode(
|
|
37553
37619
|
filePath,
|
package/dist/types/hash.d.ts
CHANGED
|
@@ -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
|
|
@@ -27,6 +27,7 @@ export declare function setDirectoryScanFinished(value: boolean): void;
|
|
|
27
27
|
export declare let scanAnomalies: {
|
|
28
28
|
fileInfo: TFileInfo;
|
|
29
29
|
anomalies: string[];
|
|
30
|
+
errors?: string[];
|
|
30
31
|
}[];
|
|
31
32
|
export declare function setMappingWorkerOptions(opts: TMappingWorkerOptions): void;
|
|
32
33
|
export declare function setCustomUploader(uploader: TCustomUploader | undefined): void;
|
|
@@ -11,6 +11,15 @@ export type FileScanMsg = {
|
|
|
11
11
|
response: 'scanAnomalies';
|
|
12
12
|
fileInfo: TFileInfo;
|
|
13
13
|
anomalies: string[];
|
|
14
|
+
/**
|
|
15
|
+
* Hard errors discovered during scanning (e.g. a file that cannot be
|
|
16
|
+
* read at all via the FileSystem API / fs.stat). Unlike `anomalies`
|
|
17
|
+
* (benign findings such as non-DICOM or too-small files), these are
|
|
18
|
+
* surfaced as errors so they are visible regardless of pass. The string
|
|
19
|
+
* MUST NOT contain the raw filename/path — that is carried only in
|
|
20
|
+
* `fileInfo` so it stays in the private (input) log.
|
|
21
|
+
*/
|
|
22
|
+
errors?: string[];
|
|
14
23
|
previousFileInfo?: {
|
|
15
24
|
size?: number;
|
|
16
25
|
mtime?: string;
|
|
@@ -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.
|
|
@@ -55131,11 +55141,13 @@
|
|
|
55131
55141
|
catch (error) {
|
|
55132
55142
|
console.warn(`[dicom-curate] Could not parse ${fileInfo.name} as DICOM data:`, error);
|
|
55133
55143
|
const mapResults = {
|
|
55134
|
-
|
|
55135
|
-
errors
|
|
55136
|
-
|
|
55137
|
-
|
|
55138
|
-
|
|
55144
|
+
// PHI-safe strings: the raw filename is intentionally omitted because
|
|
55145
|
+
// anomalies/errors are shared between the private (input) log and the
|
|
55146
|
+
// server-bound (output) log. The raw name/path is carried only in
|
|
55147
|
+
// fileInfo, keeping it in the private log. See #283.
|
|
55148
|
+
anomalies: [`Could not parse file as DICOM data`],
|
|
55149
|
+
errors: [`File is not a valid DICOM file or is corrupted`],
|
|
55150
|
+
sourceInstanceUID: `invalid_${phiSafeToken(`${fileInfo.path}/${fileInfo.name}`)}`,
|
|
55139
55151
|
fileInfo: {
|
|
55140
55152
|
name: fileInfo.name,
|
|
55141
55153
|
size: fileInfo.size,
|
|
@@ -68860,6 +68872,28 @@
|
|
|
68860
68872
|
'thumbs.db',
|
|
68861
68873
|
'.ds_store',
|
|
68862
68874
|
];
|
|
68875
|
+
/**
|
|
68876
|
+
* Build a PHI-safe error message for a file that could not be read during
|
|
68877
|
+
* scanning. Only the error code/name is included — never `error.message`,
|
|
68878
|
+
* because node fs errors embed the full raw path in the message (e.g.
|
|
68879
|
+
* "ENOENT: no such file or directory, stat '/path/to/file.dcm'") and this
|
|
68880
|
+
* string goes into `errors`, which is shared between the private and
|
|
68881
|
+
* server-bound logs. The raw path/name is carried separately in `fileInfo`
|
|
68882
|
+
* so it appears only in the private (input) log.
|
|
68883
|
+
*/
|
|
68884
|
+
function safeReadErrorMessage(error) {
|
|
68885
|
+
let detail = 'unknown error';
|
|
68886
|
+
if (error && typeof error === 'object') {
|
|
68887
|
+
const { code, name } = error;
|
|
68888
|
+
if (typeof code === 'string' && code) {
|
|
68889
|
+
detail = code;
|
|
68890
|
+
}
|
|
68891
|
+
else if (typeof name === 'string' && name) {
|
|
68892
|
+
detail = name;
|
|
68893
|
+
}
|
|
68894
|
+
}
|
|
68895
|
+
return `Unable to read file (filesystem error): ${detail}`;
|
|
68896
|
+
}
|
|
68863
68897
|
let keepScanning = true;
|
|
68864
68898
|
// Backpressure gate: when the main thread signals 'stop', the feeder
|
|
68865
68899
|
// awaits this promise before emitting the next file. 'resume' resolves it.
|
|
@@ -69242,9 +69276,42 @@
|
|
|
69242
69276
|
if (!keepScanning)
|
|
69243
69277
|
return;
|
|
69244
69278
|
if (entry.kind === 'file') {
|
|
69245
|
-
const file = await entry.getFile();
|
|
69246
69279
|
const key = `${prefix}/${entry.name}`;
|
|
69247
69280
|
const prev = previousIndex ? previousIndex[key] : undefined;
|
|
69281
|
+
let file;
|
|
69282
|
+
try {
|
|
69283
|
+
file = await entry.getFile();
|
|
69284
|
+
}
|
|
69285
|
+
catch (readError) {
|
|
69286
|
+
// A single file we cannot read (corrupted, locked, permission
|
|
69287
|
+
// revoked via the Chromium FileSystem API) must NOT abort the whole
|
|
69288
|
+
// scan. Report it as a hard error and continue. The error string is
|
|
69289
|
+
// PHI-safe (no filename); the raw path/name lives only in fileInfo,
|
|
69290
|
+
// which keeps it in the private (input) log.
|
|
69291
|
+
if (cheapFilterNameOnly(entry.name, key)) {
|
|
69292
|
+
totalDiscovered--;
|
|
69293
|
+
globalThis.postMessage({
|
|
69294
|
+
response: 'count',
|
|
69295
|
+
totalDiscovered,
|
|
69296
|
+
});
|
|
69297
|
+
}
|
|
69298
|
+
globalThis.postMessage({
|
|
69299
|
+
response: 'scanAnomalies',
|
|
69300
|
+
fileInfo: {
|
|
69301
|
+
path: prefix,
|
|
69302
|
+
name: entry.name,
|
|
69303
|
+
size: 0,
|
|
69304
|
+
kind: 'handle',
|
|
69305
|
+
fileHandle: entry,
|
|
69306
|
+
},
|
|
69307
|
+
anomalies: [],
|
|
69308
|
+
errors: [safeReadErrorMessage(readError)],
|
|
69309
|
+
previousFileInfo: prev,
|
|
69310
|
+
});
|
|
69311
|
+
if (!(await waitIfPaused()))
|
|
69312
|
+
return;
|
|
69313
|
+
continue;
|
|
69314
|
+
}
|
|
69248
69315
|
const fileAnomalies = [];
|
|
69249
69316
|
if (await shouldProcessFile(file, fileAnomalies, key)) {
|
|
69250
69317
|
globalThis.postMessage({
|
|
@@ -69362,9 +69429,41 @@
|
|
|
69362
69429
|
return;
|
|
69363
69430
|
if (entry.isFile()) {
|
|
69364
69431
|
const filePath = path.join(currentPath, entry.name);
|
|
69365
|
-
const stats = await fs.stat(filePath);
|
|
69366
69432
|
const key = `${prefix}/${entry.name}`;
|
|
69367
69433
|
const prev = previousIndex ? previousIndex[key] : undefined;
|
|
69434
|
+
let stats;
|
|
69435
|
+
try {
|
|
69436
|
+
stats = await fs.stat(filePath);
|
|
69437
|
+
}
|
|
69438
|
+
catch (readError) {
|
|
69439
|
+
// A single file we cannot stat (vanished, permission denied) must
|
|
69440
|
+
// NOT abort the whole scan. Report it as a hard error and continue.
|
|
69441
|
+
// The error string is PHI-safe (no path); the raw path lives only
|
|
69442
|
+
// in fileInfo, keeping it in the private (input) log.
|
|
69443
|
+
if (cheapFilterNameOnly(entry.name, key)) {
|
|
69444
|
+
totalDiscovered--;
|
|
69445
|
+
globalThis.postMessage({
|
|
69446
|
+
response: 'count',
|
|
69447
|
+
totalDiscovered,
|
|
69448
|
+
});
|
|
69449
|
+
}
|
|
69450
|
+
globalThis.postMessage({
|
|
69451
|
+
response: 'scanAnomalies',
|
|
69452
|
+
fileInfo: {
|
|
69453
|
+
path: prefix,
|
|
69454
|
+
name: entry.name,
|
|
69455
|
+
size: 0,
|
|
69456
|
+
kind: 'path',
|
|
69457
|
+
fullPath: filePath,
|
|
69458
|
+
},
|
|
69459
|
+
anomalies: [],
|
|
69460
|
+
errors: [safeReadErrorMessage(readError)],
|
|
69461
|
+
previousFileInfo: prev,
|
|
69462
|
+
});
|
|
69463
|
+
if (!(await waitIfPaused()))
|
|
69464
|
+
return;
|
|
69465
|
+
continue;
|
|
69466
|
+
}
|
|
69368
69467
|
const fileAnomalies = [];
|
|
69369
69468
|
if (await shouldProcessFileNode(filePath, entry.name, stats.size, fileAnomalies, key)) {
|
|
69370
69469
|
globalThis.postMessage({
|
|
@@ -126280,6 +126379,16 @@
|
|
|
126280
126379
|
var crcExports = requireCrc();
|
|
126281
126380
|
|
|
126282
126381
|
const DEFAULT_HASH_PART_SIZE = 5 * 1024 * 1024; // 5 MB — matches @aws-sdk/lib-storage default
|
|
126382
|
+
/**
|
|
126383
|
+
* Derive a short, stable, PHI-safe token from a string (e.g. a file path).
|
|
126384
|
+
* Used to build synthetic sourceInstanceUIDs for files that failed to read or
|
|
126385
|
+
* parse, where embedding the (sanitised) filename would leak PHI into the
|
|
126386
|
+
* server-bound log. Same input always yields the same token, so results stay
|
|
126387
|
+
* correlatable across passes and runs.
|
|
126388
|
+
*/
|
|
126389
|
+
function phiSafeToken(input) {
|
|
126390
|
+
return bytesToHex(sha256(new TextEncoder().encode(input))).slice(0, 16);
|
|
126391
|
+
}
|
|
126283
126392
|
async function hash(buffer, hashMethod, hashPartSize) {
|
|
126284
126393
|
switch (hashMethod) {
|
|
126285
126394
|
case 'sha256':
|
|
@@ -126773,11 +126882,13 @@
|
|
|
126773
126882
|
catch (error) {
|
|
126774
126883
|
console.warn(`[dicom-curate] Could not parse ${fileInfo.name} as DICOM data:`, error);
|
|
126775
126884
|
const mapResults = {
|
|
126776
|
-
|
|
126777
|
-
errors
|
|
126778
|
-
|
|
126779
|
-
|
|
126780
|
-
|
|
126885
|
+
// PHI-safe strings: the raw filename is intentionally omitted because
|
|
126886
|
+
// anomalies/errors are shared between the private (input) log and the
|
|
126887
|
+
// server-bound (output) log. The raw name/path is carried only in
|
|
126888
|
+
// fileInfo, keeping it in the private log. See #283.
|
|
126889
|
+
anomalies: [`Could not parse file as DICOM data`],
|
|
126890
|
+
errors: [`File is not a valid DICOM file or is corrupted`],
|
|
126891
|
+
sourceInstanceUID: `invalid_${phiSafeToken(`${fileInfo.path}/${fileInfo.name}`)}`,
|
|
126781
126892
|
fileInfo: {
|
|
126782
126893
|
name: fileInfo.name,
|
|
126783
126894
|
size: fileInfo.size,
|
|
@@ -140785,10 +140896,41 @@
|
|
|
140785
140896
|
console.log('job is finished');
|
|
140786
140897
|
if (!mapResultsList)
|
|
140787
140898
|
mapResultsList = [];
|
|
140788
|
-
// Create individual mapResults entries for each scan
|
|
140789
|
-
//
|
|
140790
|
-
|
|
140791
|
-
|
|
140899
|
+
// Create individual mapResults entries for each scan finding.
|
|
140900
|
+
//
|
|
140901
|
+
// Two kinds of scan findings are surfaced differently:
|
|
140902
|
+
//
|
|
140903
|
+
// - Benign anomalies (non-DICOM, too-small, excluded filetypes): noise
|
|
140904
|
+
// during the first pass (form generation), so only emitted on the
|
|
140905
|
+
// write pass (!skipWrite), and they carry the input path in
|
|
140906
|
+
// `outputFilePath` as before.
|
|
140907
|
+
//
|
|
140908
|
+
// - Hard read errors (file could not be read at all): always surfaced,
|
|
140909
|
+
// in both passes, via `errors`. We deliberately OMIT `outputFilePath`
|
|
140910
|
+
// so the raw input path is not leaked into the server-bound log; the
|
|
140911
|
+
// raw path is retained in `fileInfo` for the private (input) log, and a
|
|
140912
|
+
// path-less trace is rendered in the server-bound log by the consumer.
|
|
140913
|
+
scanAnomalies.forEach(({ fileInfo, anomalies, errors }) => {
|
|
140914
|
+
const hasReadErrors = !!errors && errors.length > 0;
|
|
140915
|
+
if (hasReadErrors) {
|
|
140916
|
+
const scanErrorResult = {
|
|
140917
|
+
// phiSafeToken rather than the sanitised name: this result reaches
|
|
140918
|
+
// the server-bound log, so the UID must not encode the filename.
|
|
140919
|
+
sourceInstanceUID: `scan_${phiSafeToken(`${fileInfo.path}/${fileInfo.name}`)}`,
|
|
140920
|
+
// Intentionally no outputFilePath: an unread file has no
|
|
140921
|
+
// de-identified output path, and we must not place the raw input
|
|
140922
|
+
// path in the server-bound channel.
|
|
140923
|
+
mappings: {},
|
|
140924
|
+
anomalies: anomalies ?? [],
|
|
140925
|
+
errors,
|
|
140926
|
+
quarantine: {},
|
|
140927
|
+
fileInfo,
|
|
140928
|
+
};
|
|
140929
|
+
mapResultsList.push(scanErrorResult);
|
|
140930
|
+
return;
|
|
140931
|
+
}
|
|
140932
|
+
// Benign anomalies: only on the write pass.
|
|
140933
|
+
if (!mappingWorkerOptions.skipWrite && anomalies.length > 0) {
|
|
140792
140934
|
const scanAnomalyResult = {
|
|
140793
140935
|
sourceInstanceUID: `scan_${fileInfo.name.replace(/[^a-zA-Z0-9]/g, '_')}`,
|
|
140794
140936
|
outputFilePath: `${fileInfo.path}/${fileInfo.name}`, // Use the actual file path
|
|
@@ -140797,10 +140939,9 @@
|
|
|
140797
140939
|
errors: [],
|
|
140798
140940
|
quarantine: {},
|
|
140799
140941
|
};
|
|
140800
|
-
// Add each scan anomaly result to the final results
|
|
140801
140942
|
mapResultsList.push(scanAnomalyResult);
|
|
140802
|
-
}
|
|
140803
|
-
}
|
|
140943
|
+
}
|
|
140944
|
+
});
|
|
140804
140945
|
progressCallback({
|
|
140805
140946
|
response: 'done',
|
|
140806
140947
|
mapResultsList: mapResultsList,
|
|
@@ -141086,8 +141227,8 @@
|
|
|
141086
141227
|
}
|
|
141087
141228
|
case 'scanAnomalies': {
|
|
141088
141229
|
// Handle scan anomalies separately - they don't go to processing
|
|
141089
|
-
const { fileInfo: anomalyFileInfo, anomalies } = event.data;
|
|
141090
|
-
scanAnomalies.push({ fileInfo: anomalyFileInfo, anomalies });
|
|
141230
|
+
const { fileInfo: anomalyFileInfo, anomalies, errors } = event.data;
|
|
141231
|
+
scanAnomalies.push({ fileInfo: anomalyFileInfo, anomalies, errors });
|
|
141091
141232
|
break;
|
|
141092
141233
|
}
|
|
141093
141234
|
case 'count': {
|