dicom-synth 1.11.0 → 1.12.0
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/collection/writer.js +44 -65
- package/dist/esm/describe/describe.js +9 -0
- package/dist/esm/index.js +66 -60
- package/dist/esm/schema/parametric.js +13 -1
- package/dist/esm/schema/validate.js +11 -0
- package/dist/esm/syntheticFixtures/streamWrite.js +20 -35
- package/dist/esm/syntheticFixtures/uid.js +28 -47
- package/dist/types/collection/writer.d.ts +2 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/schema/types.d.ts +2 -0
- package/dist/types/syntheticFixtures/streamWrite.d.ts +6 -2
- package/dist/types/syntheticFixtures/uid.d.ts +4 -2
- package/package.json +1 -1
|
@@ -269,68 +269,37 @@ import { closeSync, mkdirSync as mkdirSync2, openSync, writeSync } from "node:fs
|
|
|
269
269
|
import { resolve as resolve2 } from "node:path";
|
|
270
270
|
|
|
271
271
|
// src/syntheticFixtures/uid.ts
|
|
272
|
-
import { randomBytes } from "node:crypto";
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
return ()
|
|
277
|
-
s = Math.imul(1664525, s) + 1013904223 >>> 0;
|
|
278
|
-
return s;
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
function formatUid(a, b, role) {
|
|
282
|
-
return `2.25.${a}.${b}.${ROLE_CODE[role]}`;
|
|
272
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
273
|
+
function hashUid(salt, key) {
|
|
274
|
+
const digest = createHash("sha256").update(salt).update(" ").update(key).digest();
|
|
275
|
+
const n = BigInt(`0x${digest.subarray(0, 16).toString("hex")}`);
|
|
276
|
+
return `2.25.${n.toString()}`;
|
|
283
277
|
}
|
|
284
|
-
function randomUid(
|
|
285
|
-
const
|
|
286
|
-
return
|
|
278
|
+
function randomUid() {
|
|
279
|
+
const n = BigInt(`0x${randomBytes(16).toString("hex")}`);
|
|
280
|
+
return `2.25.${n.toString()}`;
|
|
287
281
|
}
|
|
288
|
-
function
|
|
289
|
-
return
|
|
282
|
+
function seedToSalt(seed) {
|
|
283
|
+
return seed === void 0 ? void 0 : `seed:${seed}`;
|
|
290
284
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
295
|
-
function makeGroupUidGenerator(seed) {
|
|
296
|
-
if (seed === void 0) {
|
|
297
|
-
return {
|
|
298
|
-
study: () => randomUid("study"),
|
|
299
|
-
series: () => randomUid("series")
|
|
300
|
-
};
|
|
285
|
+
function makeGroupUidGenerator(salt) {
|
|
286
|
+
if (salt === void 0) {
|
|
287
|
+
return { study: () => randomUid(), series: () => randomUid() };
|
|
301
288
|
}
|
|
302
289
|
return {
|
|
303
|
-
study: (studyIndex) =>
|
|
304
|
-
|
|
305
|
-
"study"
|
|
306
|
-
),
|
|
307
|
-
series: (studyIndex, seriesIndex) => seededUid(
|
|
308
|
-
seededStream(
|
|
309
|
-
seed,
|
|
310
|
-
GROUP_STREAM_OFFSET,
|
|
311
|
-
studyIndex + 1,
|
|
312
|
-
seriesIndex + 1
|
|
313
|
-
),
|
|
314
|
-
"series"
|
|
315
|
-
)
|
|
290
|
+
study: (studyIndex) => hashUid(salt, `study:${studyIndex}`),
|
|
291
|
+
series: (studyIndex, seriesIndex) => hashUid(salt, `series:${studyIndex}:${seriesIndex}`)
|
|
316
292
|
};
|
|
317
293
|
}
|
|
318
|
-
function makeUidGenerator(
|
|
319
|
-
if (
|
|
320
|
-
return () => ({
|
|
321
|
-
study: randomUid("study"),
|
|
322
|
-
series: randomUid("series"),
|
|
323
|
-
sop: randomUid("sop")
|
|
324
|
-
});
|
|
294
|
+
function makeUidGenerator(salt) {
|
|
295
|
+
if (salt === void 0) {
|
|
296
|
+
return () => ({ study: randomUid(), series: randomUid(), sop: randomUid() });
|
|
325
297
|
}
|
|
326
|
-
return (fileIndex) => {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
sop: seededUid(next, "sop")
|
|
332
|
-
};
|
|
333
|
-
};
|
|
298
|
+
return (fileIndex) => ({
|
|
299
|
+
study: hashUid(salt, `study:flat:${fileIndex}`),
|
|
300
|
+
series: hashUid(salt, `series:flat:${fileIndex}`),
|
|
301
|
+
sop: hashUid(salt, `sop:${fileIndex}`)
|
|
302
|
+
});
|
|
334
303
|
}
|
|
335
304
|
|
|
336
305
|
// src/syntheticFixtures/streamWrite.ts
|
|
@@ -443,7 +412,7 @@ function streamLargeImage(outPath, options) {
|
|
|
443
412
|
fragmentBytes = FRAGMENT_MAX_BYTES
|
|
444
413
|
} = options;
|
|
445
414
|
const modality = options.modality ?? "CT";
|
|
446
|
-
const uid = options.uid ?? makeUidGenerator(options.seed)(0);
|
|
415
|
+
const uid = options.uid ?? makeUidGenerator(options.uidSalt ?? seedToSalt(options.seed))(0);
|
|
447
416
|
const identity = { modality, uid, tags: options.tags };
|
|
448
417
|
mkdirSync2(resolve2(outPath, ".."), { recursive: true });
|
|
449
418
|
const zeroChunk = Buffer.alloc(Math.max(1, chunkBytes));
|
|
@@ -622,13 +591,16 @@ function* seriesFiles(series) {
|
|
|
622
591
|
}
|
|
623
592
|
}
|
|
624
593
|
}
|
|
625
|
-
function resolveUid(
|
|
626
|
-
return { ...makeUidGenerator(
|
|
594
|
+
function resolveUid(salt, index, override) {
|
|
595
|
+
return { ...makeUidGenerator(salt)(index), ...override };
|
|
596
|
+
}
|
|
597
|
+
function effectiveSalt(spec) {
|
|
598
|
+
return spec.uidSalt ?? seedToSalt(spec.seed);
|
|
627
599
|
}
|
|
628
600
|
async function generateFile(spec, options) {
|
|
629
601
|
const index = options?.index ?? 0;
|
|
630
602
|
const padWidth = options?.padWidth ?? 3;
|
|
631
|
-
const uid = resolveUid(options
|
|
603
|
+
const uid = resolveUid(effectiveSalt(options ?? {}), index, options?.uid);
|
|
632
604
|
const resolvedSpec = "tags" in spec && spec.tags ? {
|
|
633
605
|
...spec,
|
|
634
606
|
tags: resolveTagTemplates(spec.tags, { index, ...options?.context })
|
|
@@ -700,6 +672,9 @@ function hierarchicalName(studyOrdinal, seriesIndex, instanceNumber) {
|
|
|
700
672
|
function withResolvedSeed(spec) {
|
|
701
673
|
return spec.seed === void 0 ? { ...spec, seed: randomBytes2(4).readUInt32BE(0) } : spec;
|
|
702
674
|
}
|
|
675
|
+
function withResolvedSalt(spec) {
|
|
676
|
+
return spec.uidSalt === void 0 ? { ...spec, uidSalt: randomBytes2(16).toString("hex") } : spec;
|
|
677
|
+
}
|
|
703
678
|
function computeNames(type, index, padWidth, grouped, quirks) {
|
|
704
679
|
let names;
|
|
705
680
|
if (grouped) {
|
|
@@ -740,7 +715,7 @@ function* planCollection(spec) {
|
|
|
740
715
|
globalIndex++;
|
|
741
716
|
}
|
|
742
717
|
}
|
|
743
|
-
const groupUids = makeGroupUidGenerator(spec
|
|
718
|
+
const groupUids = makeGroupUidGenerator(effectiveSalt(spec));
|
|
744
719
|
let studyOrdinal = 0;
|
|
745
720
|
for (const study of studies) {
|
|
746
721
|
const copies = study.count ?? 1;
|
|
@@ -782,21 +757,23 @@ function* planCollection(spec) {
|
|
|
782
757
|
}
|
|
783
758
|
}
|
|
784
759
|
}
|
|
785
|
-
async function materialisePlan(plan,
|
|
760
|
+
async function materialisePlan(plan, salt) {
|
|
786
761
|
const file = await generateFile(plan.fileSpec, {
|
|
787
762
|
index: plan.index,
|
|
788
|
-
|
|
763
|
+
uidSalt: salt,
|
|
789
764
|
uid: plan.uidOverride,
|
|
790
765
|
context: plan.context
|
|
791
766
|
});
|
|
792
767
|
return { ...file, filename: plan.filename, relativePath: plan.relativePath };
|
|
793
768
|
}
|
|
794
769
|
async function* generateCollectionFromSpec(spec) {
|
|
770
|
+
const salt = effectiveSalt(spec);
|
|
795
771
|
for (const plan of planCollection(spec)) {
|
|
796
|
-
yield await materialisePlan(plan,
|
|
772
|
+
yield await materialisePlan(plan, salt);
|
|
797
773
|
}
|
|
798
774
|
}
|
|
799
775
|
async function* previewCollection(spec) {
|
|
776
|
+
const salt = effectiveSalt(spec);
|
|
800
777
|
for (const plan of planCollection(spec)) {
|
|
801
778
|
if (plan.fileSpec.type === "large-image") {
|
|
802
779
|
yield {
|
|
@@ -806,7 +783,7 @@ async function* previewCollection(spec) {
|
|
|
806
783
|
approxBytes: plan.fileSpec.targetBytes
|
|
807
784
|
};
|
|
808
785
|
} else {
|
|
809
|
-
const file = await materialisePlan(plan,
|
|
786
|
+
const file = await materialisePlan(plan, salt);
|
|
810
787
|
yield {
|
|
811
788
|
relativePath: plan.relativePath,
|
|
812
789
|
type: plan.fileSpec.type,
|
|
@@ -820,6 +797,7 @@ async function writeCollectionFromSpec(spec, outDir) {
|
|
|
820
797
|
const root = resolve3(outDir);
|
|
821
798
|
mkdirSync3(root, { recursive: true });
|
|
822
799
|
const manifest = [];
|
|
800
|
+
const salt = effectiveSalt(spec);
|
|
823
801
|
const createdDirs = /* @__PURE__ */ new Set([root]);
|
|
824
802
|
const ensureDir = (filePath) => {
|
|
825
803
|
const dir = dirname(filePath);
|
|
@@ -838,7 +816,7 @@ async function writeCollectionFromSpec(spec, outDir) {
|
|
|
838
816
|
`large-image targetBytes (${large.targetBytes}) exceeds the 50 GB limit`
|
|
839
817
|
);
|
|
840
818
|
}
|
|
841
|
-
const uid = resolveUid(
|
|
819
|
+
const uid = resolveUid(salt, plan.index, plan.uidOverride);
|
|
842
820
|
streamLargeImage(filePath, {
|
|
843
821
|
targetBytes: large.targetBytes,
|
|
844
822
|
modality: large.modality,
|
|
@@ -846,7 +824,7 @@ async function writeCollectionFromSpec(spec, outDir) {
|
|
|
846
824
|
tags: resolveTagTemplates(large.tags, plan.context)
|
|
847
825
|
});
|
|
848
826
|
} else {
|
|
849
|
-
const file = await materialisePlan(plan,
|
|
827
|
+
const file = await materialisePlan(plan, salt);
|
|
850
828
|
await writeFile(filePath, file.buffer);
|
|
851
829
|
}
|
|
852
830
|
manifest.push({
|
|
@@ -861,6 +839,7 @@ export {
|
|
|
861
839
|
generateCollectionFromSpec,
|
|
862
840
|
generateFile,
|
|
863
841
|
previewCollection,
|
|
842
|
+
withResolvedSalt,
|
|
864
843
|
withResolvedSeed,
|
|
865
844
|
writeCollectionFromSpec
|
|
866
845
|
};
|
|
@@ -105,6 +105,13 @@ function validateSeed(value, path) {
|
|
|
105
105
|
);
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
|
+
function validateUidSalt(value, path) {
|
|
109
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`${path}: must be a non-empty string; got ${JSON.stringify(value)}`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
108
115
|
function validateSizeKbLimit(kb, path) {
|
|
109
116
|
if (kb * 1024 > MAX_PIXEL_BYTES) {
|
|
110
117
|
throw new Error(`${path}: exceeds the 512 MB limit`);
|
|
@@ -406,6 +413,7 @@ function validateDatasetSpec(raw) {
|
|
|
406
413
|
(study, i) => validateStudy(study, `studies[${i}]`)
|
|
407
414
|
);
|
|
408
415
|
if ("seed" in r) validateSeed(r.seed, "DatasetSpec.seed");
|
|
416
|
+
if ("uidSalt" in r) validateUidSalt(r.uidSalt, "DatasetSpec.uidSalt");
|
|
409
417
|
if ("layout" in r) {
|
|
410
418
|
validateEnum(r.layout, VALID_LAYOUTS, "DatasetSpec.layout");
|
|
411
419
|
}
|
|
@@ -421,6 +429,7 @@ function validateDatasetSpec(raw) {
|
|
|
421
429
|
...entries.length > 0 ? { entries } : {},
|
|
422
430
|
...studies.length > 0 ? { studies } : {},
|
|
423
431
|
...r.seed !== void 0 ? { seed: r.seed } : {},
|
|
432
|
+
...r.uidSalt !== void 0 ? { uidSalt: r.uidSalt } : {},
|
|
424
433
|
...r.layout !== void 0 ? { layout: r.layout } : {},
|
|
425
434
|
...r.pathQuirks !== void 0 ? { pathQuirks: r.pathQuirks } : {}
|
|
426
435
|
};
|
package/dist/esm/index.js
CHANGED
|
@@ -154,6 +154,13 @@ function validateSeed(value, path) {
|
|
|
154
154
|
);
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
|
+
function validateUidSalt(value, path) {
|
|
158
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`${path}: must be a non-empty string; got ${JSON.stringify(value)}`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
157
164
|
function validateSizeKbLimit(kb, path) {
|
|
158
165
|
if (kb * 1024 > MAX_PIXEL_BYTES) {
|
|
159
166
|
throw new Error(`${path}: exceeds the 512 MB limit`);
|
|
@@ -455,6 +462,7 @@ function validateDatasetSpec(raw) {
|
|
|
455
462
|
(study, i) => validateStudy(study, `studies[${i}]`)
|
|
456
463
|
);
|
|
457
464
|
if ("seed" in r) validateSeed(r.seed, "DatasetSpec.seed");
|
|
465
|
+
if ("uidSalt" in r) validateUidSalt(r.uidSalt, "DatasetSpec.uidSalt");
|
|
458
466
|
if ("layout" in r) {
|
|
459
467
|
validateEnum(r.layout, VALID_LAYOUTS, "DatasetSpec.layout");
|
|
460
468
|
}
|
|
@@ -470,6 +478,7 @@ function validateDatasetSpec(raw) {
|
|
|
470
478
|
...entries.length > 0 ? { entries } : {},
|
|
471
479
|
...studies.length > 0 ? { studies } : {},
|
|
472
480
|
...r.seed !== void 0 ? { seed: r.seed } : {},
|
|
481
|
+
...r.uidSalt !== void 0 ? { uidSalt: r.uidSalt } : {},
|
|
473
482
|
...r.layout !== void 0 ? { layout: r.layout } : {},
|
|
474
483
|
...r.pathQuirks !== void 0 ? { pathQuirks: r.pathQuirks } : {}
|
|
475
484
|
};
|
|
@@ -585,6 +594,7 @@ function validateParametricSpec(raw) {
|
|
|
585
594
|
}
|
|
586
595
|
}
|
|
587
596
|
if ("seed" in r) validateSeed(r.seed, "ParametricSpec.seed");
|
|
597
|
+
if ("uidSalt" in r) validateUidSalt(r.uidSalt, "ParametricSpec.uidSalt");
|
|
588
598
|
if ("layout" in r) {
|
|
589
599
|
validateEnum(r.layout, VALID_LAYOUTS, "ParametricSpec.layout");
|
|
590
600
|
}
|
|
@@ -592,6 +602,7 @@ function validateParametricSpec(raw) {
|
|
|
592
602
|
studies,
|
|
593
603
|
...edgeCases.length > 0 ? { edgeCases } : {},
|
|
594
604
|
...r.seed !== void 0 ? { seed: r.seed } : {},
|
|
605
|
+
...r.uidSalt !== void 0 ? { uidSalt: r.uidSalt } : {},
|
|
595
606
|
...r.layout !== void 0 ? { layout: r.layout } : {}
|
|
596
607
|
};
|
|
597
608
|
}
|
|
@@ -779,8 +790,19 @@ import { closeSync, mkdirSync as mkdirSync2, openSync, writeSync } from "node:fs
|
|
|
779
790
|
import { resolve as resolve2 } from "node:path";
|
|
780
791
|
|
|
781
792
|
// src/syntheticFixtures/uid.ts
|
|
782
|
-
import { randomBytes } from "node:crypto";
|
|
783
|
-
|
|
793
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
794
|
+
function hashUid(salt, key) {
|
|
795
|
+
const digest = createHash("sha256").update(salt).update(" ").update(key).digest();
|
|
796
|
+
const n = BigInt(`0x${digest.subarray(0, 16).toString("hex")}`);
|
|
797
|
+
return `2.25.${n.toString()}`;
|
|
798
|
+
}
|
|
799
|
+
function randomUid() {
|
|
800
|
+
const n = BigInt(`0x${randomBytes(16).toString("hex")}`);
|
|
801
|
+
return `2.25.${n.toString()}`;
|
|
802
|
+
}
|
|
803
|
+
function seedToSalt(seed) {
|
|
804
|
+
return seed === void 0 ? void 0 : `seed:${seed}`;
|
|
805
|
+
}
|
|
784
806
|
function lcg(seed) {
|
|
785
807
|
let s = seed >>> 0;
|
|
786
808
|
return () => {
|
|
@@ -788,60 +810,28 @@ function lcg(seed) {
|
|
|
788
810
|
return s;
|
|
789
811
|
};
|
|
790
812
|
}
|
|
791
|
-
function formatUid(a, b, role) {
|
|
792
|
-
return `2.25.${a}.${b}.${ROLE_CODE[role]}`;
|
|
793
|
-
}
|
|
794
|
-
function randomUid(role) {
|
|
795
|
-
const buf = randomBytes(8);
|
|
796
|
-
return formatUid(buf.readUInt32BE(0), buf.readUInt32BE(4), role);
|
|
797
|
-
}
|
|
798
|
-
function seededUid(next, role) {
|
|
799
|
-
return formatUid(next(), next(), role);
|
|
800
|
-
}
|
|
801
|
-
var GROUP_STREAM_OFFSET = 2654435769;
|
|
802
813
|
var PARAMETRIC_STREAM_OFFSET = 2246822507;
|
|
803
814
|
function seededStream(seed, offset, a, b) {
|
|
804
815
|
return lcg(seed * 999983 + offset + a * 1000003 + b * 7919 >>> 0);
|
|
805
816
|
}
|
|
806
|
-
function makeGroupUidGenerator(
|
|
807
|
-
if (
|
|
808
|
-
return {
|
|
809
|
-
study: () => randomUid("study"),
|
|
810
|
-
series: () => randomUid("series")
|
|
811
|
-
};
|
|
817
|
+
function makeGroupUidGenerator(salt) {
|
|
818
|
+
if (salt === void 0) {
|
|
819
|
+
return { study: () => randomUid(), series: () => randomUid() };
|
|
812
820
|
}
|
|
813
821
|
return {
|
|
814
|
-
study: (studyIndex) =>
|
|
815
|
-
|
|
816
|
-
"study"
|
|
817
|
-
),
|
|
818
|
-
series: (studyIndex, seriesIndex) => seededUid(
|
|
819
|
-
seededStream(
|
|
820
|
-
seed,
|
|
821
|
-
GROUP_STREAM_OFFSET,
|
|
822
|
-
studyIndex + 1,
|
|
823
|
-
seriesIndex + 1
|
|
824
|
-
),
|
|
825
|
-
"series"
|
|
826
|
-
)
|
|
822
|
+
study: (studyIndex) => hashUid(salt, `study:${studyIndex}`),
|
|
823
|
+
series: (studyIndex, seriesIndex) => hashUid(salt, `series:${studyIndex}:${seriesIndex}`)
|
|
827
824
|
};
|
|
828
825
|
}
|
|
829
|
-
function makeUidGenerator(
|
|
830
|
-
if (
|
|
831
|
-
return () => ({
|
|
832
|
-
study: randomUid("study"),
|
|
833
|
-
series: randomUid("series"),
|
|
834
|
-
sop: randomUid("sop")
|
|
835
|
-
});
|
|
826
|
+
function makeUidGenerator(salt) {
|
|
827
|
+
if (salt === void 0) {
|
|
828
|
+
return () => ({ study: randomUid(), series: randomUid(), sop: randomUid() });
|
|
836
829
|
}
|
|
837
|
-
return (fileIndex) => {
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
sop: seededUid(next, "sop")
|
|
843
|
-
};
|
|
844
|
-
};
|
|
830
|
+
return (fileIndex) => ({
|
|
831
|
+
study: hashUid(salt, `study:flat:${fileIndex}`),
|
|
832
|
+
series: hashUid(salt, `series:flat:${fileIndex}`),
|
|
833
|
+
sop: hashUid(salt, `sop:${fileIndex}`)
|
|
834
|
+
});
|
|
845
835
|
}
|
|
846
836
|
|
|
847
837
|
// src/syntheticFixtures/streamWrite.ts
|
|
@@ -954,7 +944,7 @@ function streamLargeImage(outPath, options) {
|
|
|
954
944
|
fragmentBytes = FRAGMENT_MAX_BYTES
|
|
955
945
|
} = options;
|
|
956
946
|
const modality = options.modality ?? "CT";
|
|
957
|
-
const uid = options.uid ?? makeUidGenerator(options.seed)(0);
|
|
947
|
+
const uid = options.uid ?? makeUidGenerator(options.uidSalt ?? seedToSalt(options.seed))(0);
|
|
958
948
|
const identity = { modality, uid, tags: options.tags };
|
|
959
949
|
mkdirSync2(resolve2(outPath, ".."), { recursive: true });
|
|
960
950
|
const zeroChunk = Buffer.alloc(Math.max(1, chunkBytes));
|
|
@@ -1006,6 +996,7 @@ function writeLargeImageFile(spec, outPath, options = {}) {
|
|
|
1006
996
|
return streamLargeImage(outPath, {
|
|
1007
997
|
targetBytes,
|
|
1008
998
|
modality: spec.modality,
|
|
999
|
+
uidSalt: spec.uidSalt,
|
|
1009
1000
|
seed: spec.seed,
|
|
1010
1001
|
chunkBytes: options.chunkBytes
|
|
1011
1002
|
});
|
|
@@ -1153,13 +1144,16 @@ function* seriesFiles(series) {
|
|
|
1153
1144
|
}
|
|
1154
1145
|
}
|
|
1155
1146
|
}
|
|
1156
|
-
function resolveUid(
|
|
1157
|
-
return { ...makeUidGenerator(
|
|
1147
|
+
function resolveUid(salt, index, override) {
|
|
1148
|
+
return { ...makeUidGenerator(salt)(index), ...override };
|
|
1149
|
+
}
|
|
1150
|
+
function effectiveSalt(spec) {
|
|
1151
|
+
return spec.uidSalt ?? seedToSalt(spec.seed);
|
|
1158
1152
|
}
|
|
1159
1153
|
async function generateFile(spec, options) {
|
|
1160
1154
|
const index = options?.index ?? 0;
|
|
1161
1155
|
const padWidth = options?.padWidth ?? 3;
|
|
1162
|
-
const uid = resolveUid(options
|
|
1156
|
+
const uid = resolveUid(effectiveSalt(options ?? {}), index, options?.uid);
|
|
1163
1157
|
const resolvedSpec = "tags" in spec && spec.tags ? {
|
|
1164
1158
|
...spec,
|
|
1165
1159
|
tags: resolveTagTemplates(spec.tags, { index, ...options?.context })
|
|
@@ -1231,6 +1225,9 @@ function hierarchicalName(studyOrdinal, seriesIndex, instanceNumber) {
|
|
|
1231
1225
|
function withResolvedSeed(spec) {
|
|
1232
1226
|
return spec.seed === void 0 ? { ...spec, seed: randomBytes2(4).readUInt32BE(0) } : spec;
|
|
1233
1227
|
}
|
|
1228
|
+
function withResolvedSalt(spec) {
|
|
1229
|
+
return spec.uidSalt === void 0 ? { ...spec, uidSalt: randomBytes2(16).toString("hex") } : spec;
|
|
1230
|
+
}
|
|
1234
1231
|
function computeNames(type, index, padWidth, grouped, quirks) {
|
|
1235
1232
|
let names;
|
|
1236
1233
|
if (grouped) {
|
|
@@ -1271,7 +1268,7 @@ function* planCollection(spec) {
|
|
|
1271
1268
|
globalIndex++;
|
|
1272
1269
|
}
|
|
1273
1270
|
}
|
|
1274
|
-
const groupUids = makeGroupUidGenerator(spec
|
|
1271
|
+
const groupUids = makeGroupUidGenerator(effectiveSalt(spec));
|
|
1275
1272
|
let studyOrdinal = 0;
|
|
1276
1273
|
for (const study of studies) {
|
|
1277
1274
|
const copies = study.count ?? 1;
|
|
@@ -1313,21 +1310,23 @@ function* planCollection(spec) {
|
|
|
1313
1310
|
}
|
|
1314
1311
|
}
|
|
1315
1312
|
}
|
|
1316
|
-
async function materialisePlan(plan,
|
|
1313
|
+
async function materialisePlan(plan, salt) {
|
|
1317
1314
|
const file = await generateFile(plan.fileSpec, {
|
|
1318
1315
|
index: plan.index,
|
|
1319
|
-
|
|
1316
|
+
uidSalt: salt,
|
|
1320
1317
|
uid: plan.uidOverride,
|
|
1321
1318
|
context: plan.context
|
|
1322
1319
|
});
|
|
1323
1320
|
return { ...file, filename: plan.filename, relativePath: plan.relativePath };
|
|
1324
1321
|
}
|
|
1325
1322
|
async function* generateCollectionFromSpec(spec) {
|
|
1323
|
+
const salt = effectiveSalt(spec);
|
|
1326
1324
|
for (const plan of planCollection(spec)) {
|
|
1327
|
-
yield await materialisePlan(plan,
|
|
1325
|
+
yield await materialisePlan(plan, salt);
|
|
1328
1326
|
}
|
|
1329
1327
|
}
|
|
1330
1328
|
async function* previewCollection(spec) {
|
|
1329
|
+
const salt = effectiveSalt(spec);
|
|
1331
1330
|
for (const plan of planCollection(spec)) {
|
|
1332
1331
|
if (plan.fileSpec.type === "large-image") {
|
|
1333
1332
|
yield {
|
|
@@ -1337,7 +1336,7 @@ async function* previewCollection(spec) {
|
|
|
1337
1336
|
approxBytes: plan.fileSpec.targetBytes
|
|
1338
1337
|
};
|
|
1339
1338
|
} else {
|
|
1340
|
-
const file = await materialisePlan(plan,
|
|
1339
|
+
const file = await materialisePlan(plan, salt);
|
|
1341
1340
|
yield {
|
|
1342
1341
|
relativePath: plan.relativePath,
|
|
1343
1342
|
type: plan.fileSpec.type,
|
|
@@ -1351,6 +1350,7 @@ async function writeCollectionFromSpec(spec, outDir) {
|
|
|
1351
1350
|
const root = resolve3(outDir);
|
|
1352
1351
|
mkdirSync3(root, { recursive: true });
|
|
1353
1352
|
const manifest = [];
|
|
1353
|
+
const salt = effectiveSalt(spec);
|
|
1354
1354
|
const createdDirs = /* @__PURE__ */ new Set([root]);
|
|
1355
1355
|
const ensureDir = (filePath) => {
|
|
1356
1356
|
const dir = dirname(filePath);
|
|
@@ -1369,7 +1369,7 @@ async function writeCollectionFromSpec(spec, outDir) {
|
|
|
1369
1369
|
`large-image targetBytes (${large.targetBytes}) exceeds the 50 GB limit`
|
|
1370
1370
|
);
|
|
1371
1371
|
}
|
|
1372
|
-
const uid = resolveUid(
|
|
1372
|
+
const uid = resolveUid(salt, plan.index, plan.uidOverride);
|
|
1373
1373
|
streamLargeImage(filePath, {
|
|
1374
1374
|
targetBytes: large.targetBytes,
|
|
1375
1375
|
modality: large.modality,
|
|
@@ -1377,7 +1377,7 @@ async function writeCollectionFromSpec(spec, outDir) {
|
|
|
1377
1377
|
tags: resolveTagTemplates(large.tags, plan.context)
|
|
1378
1378
|
});
|
|
1379
1379
|
} else {
|
|
1380
|
-
const file = await materialisePlan(plan,
|
|
1380
|
+
const file = await materialisePlan(plan, salt);
|
|
1381
1381
|
await writeFile(filePath, file.buffer);
|
|
1382
1382
|
}
|
|
1383
1383
|
manifest.push({
|
|
@@ -1759,7 +1759,7 @@ function loadCaseById(casesJsonPath, id) {
|
|
|
1759
1759
|
}
|
|
1760
1760
|
|
|
1761
1761
|
// src/public-fixtures/fetch.ts
|
|
1762
|
-
import { createHash } from "node:crypto";
|
|
1762
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1763
1763
|
import { existsSync, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1764
1764
|
import { homedir } from "node:os";
|
|
1765
1765
|
import { join as join3 } from "node:path";
|
|
@@ -1768,7 +1768,7 @@ function caseCachePath(sha256, root = DEFAULT_CACHE_ROOT) {
|
|
|
1768
1768
|
return join3(root, sha256, "file.dcm");
|
|
1769
1769
|
}
|
|
1770
1770
|
function verifySha256(buffer, expected) {
|
|
1771
|
-
const h =
|
|
1771
|
+
const h = createHash2("sha256").update(buffer).digest("hex");
|
|
1772
1772
|
if (h !== expected) {
|
|
1773
1773
|
throw new Error(
|
|
1774
1774
|
`SHA256 mismatch: expected ${expected}, got ${h}. Upstream may have changed; bump metadata after review.`
|
|
@@ -1900,6 +1900,9 @@ function resolveParametricSpec(spec) {
|
|
|
1900
1900
|
...entries.length > 0 ? { entries } : {},
|
|
1901
1901
|
studies,
|
|
1902
1902
|
seed,
|
|
1903
|
+
// Pass an explicit salt through; when absent, UIDs derive from `seed` so the
|
|
1904
|
+
// resolved spec stays deterministic ("same seed → identical resolved spec").
|
|
1905
|
+
...validated.uidSalt !== void 0 ? { uidSalt: validated.uidSalt } : {},
|
|
1903
1906
|
...validated.layout !== void 0 ? { layout: validated.layout } : {}
|
|
1904
1907
|
};
|
|
1905
1908
|
}
|
|
@@ -1913,16 +1916,19 @@ export {
|
|
|
1913
1916
|
fetchPublicCaseToCache,
|
|
1914
1917
|
generateCollectionFromSpec,
|
|
1915
1918
|
generateFile,
|
|
1919
|
+
hashUid,
|
|
1916
1920
|
loadCaseById,
|
|
1917
1921
|
loadCasesFromJson,
|
|
1918
1922
|
loadDefaultCases,
|
|
1919
1923
|
previewCollection,
|
|
1920
1924
|
resolveParametricSpec,
|
|
1921
1925
|
resolveTagTemplates,
|
|
1926
|
+
seedToSalt,
|
|
1922
1927
|
streamLargeImage,
|
|
1923
1928
|
validateDatasetSpec,
|
|
1924
1929
|
validateParametricSpec,
|
|
1925
1930
|
verifySha256,
|
|
1931
|
+
withResolvedSalt,
|
|
1926
1932
|
withResolvedSeed,
|
|
1927
1933
|
writeCollectionFromSpec,
|
|
1928
1934
|
writeLargeImageFile
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
3
3
|
|
|
4
4
|
// src/syntheticFixtures/uid.ts
|
|
5
|
-
import { randomBytes } from "node:crypto";
|
|
5
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
6
6
|
function lcg(seed) {
|
|
7
7
|
let s = seed >>> 0;
|
|
8
8
|
return () => {
|
|
@@ -90,6 +90,13 @@ function validateSeed(value, path) {
|
|
|
90
90
|
);
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
|
+
function validateUidSalt(value, path) {
|
|
94
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`${path}: must be a non-empty string; got ${JSON.stringify(value)}`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
93
100
|
function validateSizeKbLimit(kb, path) {
|
|
94
101
|
if (kb * 1024 > MAX_PIXEL_BYTES) {
|
|
95
102
|
throw new Error(`${path}: exceeds the 512 MB limit`);
|
|
@@ -361,6 +368,7 @@ function validateParametricSpec(raw) {
|
|
|
361
368
|
}
|
|
362
369
|
}
|
|
363
370
|
if ("seed" in r) validateSeed(r.seed, "ParametricSpec.seed");
|
|
371
|
+
if ("uidSalt" in r) validateUidSalt(r.uidSalt, "ParametricSpec.uidSalt");
|
|
364
372
|
if ("layout" in r) {
|
|
365
373
|
validateEnum(r.layout, VALID_LAYOUTS, "ParametricSpec.layout");
|
|
366
374
|
}
|
|
@@ -368,6 +376,7 @@ function validateParametricSpec(raw) {
|
|
|
368
376
|
studies,
|
|
369
377
|
...edgeCases.length > 0 ? { edgeCases } : {},
|
|
370
378
|
...r.seed !== void 0 ? { seed: r.seed } : {},
|
|
379
|
+
...r.uidSalt !== void 0 ? { uidSalt: r.uidSalt } : {},
|
|
371
380
|
...r.layout !== void 0 ? { layout: r.layout } : {}
|
|
372
381
|
};
|
|
373
382
|
}
|
|
@@ -471,6 +480,9 @@ function resolveParametricSpec(spec) {
|
|
|
471
480
|
...entries.length > 0 ? { entries } : {},
|
|
472
481
|
studies,
|
|
473
482
|
seed,
|
|
483
|
+
// Pass an explicit salt through; when absent, UIDs derive from `seed` so the
|
|
484
|
+
// resolved spec stays deterministic ("same seed → identical resolved spec").
|
|
485
|
+
...validated.uidSalt !== void 0 ? { uidSalt: validated.uidSalt } : {},
|
|
474
486
|
...validated.layout !== void 0 ? { layout: validated.layout } : {}
|
|
475
487
|
};
|
|
476
488
|
}
|
|
@@ -79,6 +79,13 @@ function validateSeed(value, path) {
|
|
|
79
79
|
);
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
|
+
function validateUidSalt(value, path) {
|
|
83
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`${path}: must be a non-empty string; got ${JSON.stringify(value)}`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
82
89
|
function validateSizeKbLimit(kb, path) {
|
|
83
90
|
if (kb * 1024 > MAX_PIXEL_BYTES) {
|
|
84
91
|
throw new Error(`${path}: exceeds the 512 MB limit`);
|
|
@@ -380,6 +387,7 @@ function validateDatasetSpec(raw) {
|
|
|
380
387
|
(study, i) => validateStudy(study, `studies[${i}]`)
|
|
381
388
|
);
|
|
382
389
|
if ("seed" in r) validateSeed(r.seed, "DatasetSpec.seed");
|
|
390
|
+
if ("uidSalt" in r) validateUidSalt(r.uidSalt, "DatasetSpec.uidSalt");
|
|
383
391
|
if ("layout" in r) {
|
|
384
392
|
validateEnum(r.layout, VALID_LAYOUTS, "DatasetSpec.layout");
|
|
385
393
|
}
|
|
@@ -395,6 +403,7 @@ function validateDatasetSpec(raw) {
|
|
|
395
403
|
...entries.length > 0 ? { entries } : {},
|
|
396
404
|
...studies.length > 0 ? { studies } : {},
|
|
397
405
|
...r.seed !== void 0 ? { seed: r.seed } : {},
|
|
406
|
+
...r.uidSalt !== void 0 ? { uidSalt: r.uidSalt } : {},
|
|
398
407
|
...r.layout !== void 0 ? { layout: r.layout } : {},
|
|
399
408
|
...r.pathQuirks !== void 0 ? { pathQuirks: r.pathQuirks } : {}
|
|
400
409
|
};
|
|
@@ -510,6 +519,7 @@ function validateParametricSpec(raw) {
|
|
|
510
519
|
}
|
|
511
520
|
}
|
|
512
521
|
if ("seed" in r) validateSeed(r.seed, "ParametricSpec.seed");
|
|
522
|
+
if ("uidSalt" in r) validateUidSalt(r.uidSalt, "ParametricSpec.uidSalt");
|
|
513
523
|
if ("layout" in r) {
|
|
514
524
|
validateEnum(r.layout, VALID_LAYOUTS, "ParametricSpec.layout");
|
|
515
525
|
}
|
|
@@ -517,6 +527,7 @@ function validateParametricSpec(raw) {
|
|
|
517
527
|
studies,
|
|
518
528
|
...edgeCases.length > 0 ? { edgeCases } : {},
|
|
519
529
|
...r.seed !== void 0 ? { seed: r.seed } : {},
|
|
530
|
+
...r.uidSalt !== void 0 ? { uidSalt: r.uidSalt } : {},
|
|
520
531
|
...r.layout !== void 0 ? { layout: r.layout } : {}
|
|
521
532
|
};
|
|
522
533
|
}
|
|
@@ -144,44 +144,28 @@ function serializeDict(meta, dataset) {
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
// src/syntheticFixtures/uid.ts
|
|
147
|
-
import { randomBytes } from "node:crypto";
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
return ()
|
|
152
|
-
s = Math.imul(1664525, s) + 1013904223 >>> 0;
|
|
153
|
-
return s;
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
function formatUid(a, b, role) {
|
|
157
|
-
return `2.25.${a}.${b}.${ROLE_CODE[role]}`;
|
|
147
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
148
|
+
function hashUid(salt, key) {
|
|
149
|
+
const digest = createHash("sha256").update(salt).update(" ").update(key).digest();
|
|
150
|
+
const n = BigInt(`0x${digest.subarray(0, 16).toString("hex")}`);
|
|
151
|
+
return `2.25.${n.toString()}`;
|
|
158
152
|
}
|
|
159
|
-
function randomUid(
|
|
160
|
-
const
|
|
161
|
-
return
|
|
153
|
+
function randomUid() {
|
|
154
|
+
const n = BigInt(`0x${randomBytes(16).toString("hex")}`);
|
|
155
|
+
return `2.25.${n.toString()}`;
|
|
162
156
|
}
|
|
163
|
-
function
|
|
164
|
-
return
|
|
157
|
+
function seedToSalt(seed) {
|
|
158
|
+
return seed === void 0 ? void 0 : `seed:${seed}`;
|
|
165
159
|
}
|
|
166
|
-
function
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
function makeUidGenerator(seed) {
|
|
170
|
-
if (seed === void 0) {
|
|
171
|
-
return () => ({
|
|
172
|
-
study: randomUid("study"),
|
|
173
|
-
series: randomUid("series"),
|
|
174
|
-
sop: randomUid("sop")
|
|
175
|
-
});
|
|
160
|
+
function makeUidGenerator(salt) {
|
|
161
|
+
if (salt === void 0) {
|
|
162
|
+
return () => ({ study: randomUid(), series: randomUid(), sop: randomUid() });
|
|
176
163
|
}
|
|
177
|
-
return (fileIndex) => {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
sop: seededUid(next, "sop")
|
|
183
|
-
};
|
|
184
|
-
};
|
|
164
|
+
return (fileIndex) => ({
|
|
165
|
+
study: hashUid(salt, `study:flat:${fileIndex}`),
|
|
166
|
+
series: hashUid(salt, `series:flat:${fileIndex}`),
|
|
167
|
+
sop: hashUid(salt, `sop:${fileIndex}`)
|
|
168
|
+
});
|
|
185
169
|
}
|
|
186
170
|
|
|
187
171
|
// src/syntheticFixtures/streamWrite.ts
|
|
@@ -294,7 +278,7 @@ function streamLargeImage(outPath, options) {
|
|
|
294
278
|
fragmentBytes = FRAGMENT_MAX_BYTES
|
|
295
279
|
} = options;
|
|
296
280
|
const modality = options.modality ?? "CT";
|
|
297
|
-
const uid = options.uid ?? makeUidGenerator(options.seed)(0);
|
|
281
|
+
const uid = options.uid ?? makeUidGenerator(options.uidSalt ?? seedToSalt(options.seed))(0);
|
|
298
282
|
const identity = { modality, uid, tags: options.tags };
|
|
299
283
|
mkdirSync2(resolve2(outPath, ".."), { recursive: true });
|
|
300
284
|
const zeroChunk = Buffer.alloc(Math.max(1, chunkBytes));
|
|
@@ -346,6 +330,7 @@ function writeLargeImageFile(spec, outPath, options = {}) {
|
|
|
346
330
|
return streamLargeImage(outPath, {
|
|
347
331
|
targetBytes,
|
|
348
332
|
modality: spec.modality,
|
|
333
|
+
uidSalt: spec.uidSalt,
|
|
349
334
|
seed: spec.seed,
|
|
350
335
|
chunkBytes: options.chunkBytes
|
|
351
336
|
});
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
// src/syntheticFixtures/uid.ts
|
|
2
|
-
import { randomBytes } from "node:crypto";
|
|
3
|
-
|
|
2
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
+
function hashUid(salt, key) {
|
|
4
|
+
const digest = createHash("sha256").update(salt).update(" ").update(key).digest();
|
|
5
|
+
const n = BigInt(`0x${digest.subarray(0, 16).toString("hex")}`);
|
|
6
|
+
return `2.25.${n.toString()}`;
|
|
7
|
+
}
|
|
8
|
+
function randomUid() {
|
|
9
|
+
const n = BigInt(`0x${randomBytes(16).toString("hex")}`);
|
|
10
|
+
return `2.25.${n.toString()}`;
|
|
11
|
+
}
|
|
12
|
+
function seedToSalt(seed) {
|
|
13
|
+
return seed === void 0 ? void 0 : `seed:${seed}`;
|
|
14
|
+
}
|
|
4
15
|
function lcg(seed) {
|
|
5
16
|
let s = seed >>> 0;
|
|
6
17
|
return () => {
|
|
@@ -8,64 +19,34 @@ function lcg(seed) {
|
|
|
8
19
|
return s;
|
|
9
20
|
};
|
|
10
21
|
}
|
|
11
|
-
function formatUid(a, b, role) {
|
|
12
|
-
return `2.25.${a}.${b}.${ROLE_CODE[role]}`;
|
|
13
|
-
}
|
|
14
|
-
function randomUid(role) {
|
|
15
|
-
const buf = randomBytes(8);
|
|
16
|
-
return formatUid(buf.readUInt32BE(0), buf.readUInt32BE(4), role);
|
|
17
|
-
}
|
|
18
|
-
function seededUid(next, role) {
|
|
19
|
-
return formatUid(next(), next(), role);
|
|
20
|
-
}
|
|
21
|
-
var GROUP_STREAM_OFFSET = 2654435769;
|
|
22
22
|
var PARAMETRIC_STREAM_OFFSET = 2246822507;
|
|
23
23
|
function seededStream(seed, offset, a, b) {
|
|
24
24
|
return lcg(seed * 999983 + offset + a * 1000003 + b * 7919 >>> 0);
|
|
25
25
|
}
|
|
26
|
-
function makeGroupUidGenerator(
|
|
27
|
-
if (
|
|
28
|
-
return {
|
|
29
|
-
study: () => randomUid("study"),
|
|
30
|
-
series: () => randomUid("series")
|
|
31
|
-
};
|
|
26
|
+
function makeGroupUidGenerator(salt) {
|
|
27
|
+
if (salt === void 0) {
|
|
28
|
+
return { study: () => randomUid(), series: () => randomUid() };
|
|
32
29
|
}
|
|
33
30
|
return {
|
|
34
|
-
study: (studyIndex) =>
|
|
35
|
-
|
|
36
|
-
"study"
|
|
37
|
-
),
|
|
38
|
-
series: (studyIndex, seriesIndex) => seededUid(
|
|
39
|
-
seededStream(
|
|
40
|
-
seed,
|
|
41
|
-
GROUP_STREAM_OFFSET,
|
|
42
|
-
studyIndex + 1,
|
|
43
|
-
seriesIndex + 1
|
|
44
|
-
),
|
|
45
|
-
"series"
|
|
46
|
-
)
|
|
31
|
+
study: (studyIndex) => hashUid(salt, `study:${studyIndex}`),
|
|
32
|
+
series: (studyIndex, seriesIndex) => hashUid(salt, `series:${studyIndex}:${seriesIndex}`)
|
|
47
33
|
};
|
|
48
34
|
}
|
|
49
|
-
function makeUidGenerator(
|
|
50
|
-
if (
|
|
51
|
-
return () => ({
|
|
52
|
-
study: randomUid("study"),
|
|
53
|
-
series: randomUid("series"),
|
|
54
|
-
sop: randomUid("sop")
|
|
55
|
-
});
|
|
35
|
+
function makeUidGenerator(salt) {
|
|
36
|
+
if (salt === void 0) {
|
|
37
|
+
return () => ({ study: randomUid(), series: randomUid(), sop: randomUid() });
|
|
56
38
|
}
|
|
57
|
-
return (fileIndex) => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
sop: seededUid(next, "sop")
|
|
63
|
-
};
|
|
64
|
-
};
|
|
39
|
+
return (fileIndex) => ({
|
|
40
|
+
study: hashUid(salt, `study:flat:${fileIndex}`),
|
|
41
|
+
series: hashUid(salt, `series:flat:${fileIndex}`),
|
|
42
|
+
sop: hashUid(salt, `sop:${fileIndex}`)
|
|
43
|
+
});
|
|
65
44
|
}
|
|
66
45
|
export {
|
|
67
46
|
PARAMETRIC_STREAM_OFFSET,
|
|
47
|
+
hashUid,
|
|
68
48
|
makeGroupUidGenerator,
|
|
69
49
|
makeUidGenerator,
|
|
50
|
+
seedToSalt,
|
|
70
51
|
seededStream
|
|
71
52
|
};
|
|
@@ -16,11 +16,13 @@ export type CollectionManifest = Array<{
|
|
|
16
16
|
export declare function generateFile(spec: FileSpec, options?: {
|
|
17
17
|
index?: number;
|
|
18
18
|
seed?: number;
|
|
19
|
+
uidSalt?: string;
|
|
19
20
|
padWidth?: number;
|
|
20
21
|
uid?: Partial<UidSet>;
|
|
21
22
|
context?: Partial<TagContext>;
|
|
22
23
|
}): Promise<GeneratedFile>;
|
|
23
24
|
export declare function withResolvedSeed(spec: DatasetSpec): DatasetSpec;
|
|
25
|
+
export declare function withResolvedSalt(spec: DatasetSpec): DatasetSpec;
|
|
24
26
|
export declare function generateCollectionFromSpec(spec: DatasetSpec): AsyncGenerator<GeneratedFile>;
|
|
25
27
|
export type CollectionPreviewItem = {
|
|
26
28
|
relativePath: string;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { type CollectionManifest, type CollectionPreviewItem, type GeneratedFile, generateCollectionFromSpec, generateFile, previewCollection, withResolvedSeed, writeCollectionFromSpec, } from './collection/writer.js';
|
|
1
|
+
export { type CollectionManifest, type CollectionPreviewItem, type GeneratedFile, generateCollectionFromSpec, generateFile, previewCollection, withResolvedSalt, withResolvedSeed, writeCollectionFromSpec, } from './collection/writer.js';
|
|
2
2
|
export { type DescribeOptions, type DescribeResult, describeDirectory, } from './describe/describe.js';
|
|
3
3
|
export { defaultPublicCasesPath, loadCaseById, loadCasesFromJson, loadDefaultCases, type PublicCaseRecord, type PublicCaseSource, } from './public-fixtures/catalog.js';
|
|
4
4
|
export { caseCachePath, fetchPublicCaseToCache, verifySha256, } from './public-fixtures/fetch.js';
|
|
@@ -8,3 +8,4 @@ export type { DatasetLayout, DatasetSpec, DicomdirSpec, DicomTagOverrides, FakeS
|
|
|
8
8
|
export { validateDatasetSpec, validateParametricSpec, } from './schema/validate.js';
|
|
9
9
|
export { type LargeFileEncoding, type LargeFileResult, type LargeImageSpec, type StreamLargeImageOptions, streamLargeImage, writeLargeImageFile, } from './syntheticFixtures/streamWrite.js';
|
|
10
10
|
export { resolveTagTemplates, type TagContext, TEMPLATE_VOCAB, } from './syntheticFixtures/tagTemplate.js';
|
|
11
|
+
export { hashUid, seedToSalt } from './syntheticFixtures/uid.js';
|
|
@@ -74,6 +74,7 @@ export type DatasetSpec = {
|
|
|
74
74
|
entries?: EntrySpec[];
|
|
75
75
|
studies?: StudySpec[];
|
|
76
76
|
seed?: number;
|
|
77
|
+
uidSalt?: string;
|
|
77
78
|
layout?: DatasetLayout;
|
|
78
79
|
pathQuirks?: PathQuirk[];
|
|
79
80
|
};
|
|
@@ -96,6 +97,7 @@ export type ParametricEdgeCase = EntrySpec & {
|
|
|
96
97
|
};
|
|
97
98
|
export type ParametricSpec = {
|
|
98
99
|
seed?: number;
|
|
100
|
+
uidSalt?: string;
|
|
99
101
|
studies: ParametricStudies;
|
|
100
102
|
edgeCases?: ParametricEdgeCase[];
|
|
101
103
|
layout?: DatasetLayout;
|
|
@@ -4,7 +4,9 @@ export type LargeImageSpec = {
|
|
|
4
4
|
/** Approximate total file size in bytes (must exceed the 512 MB cap). */
|
|
5
5
|
targetBytes: number;
|
|
6
6
|
modality?: Modality;
|
|
7
|
-
/**
|
|
7
|
+
/** UID namespace (hash-derived UIDs); random when both salt and seed omitted. */
|
|
8
|
+
uidSalt?: string;
|
|
9
|
+
/** Bridged to a salt when uidSalt is omitted; fixes the instance UIDs. */
|
|
8
10
|
seed?: number;
|
|
9
11
|
};
|
|
10
12
|
export type LargeFileEncoding = 'native' | 'encapsulated';
|
|
@@ -18,7 +20,9 @@ export type StreamLargeImageOptions = {
|
|
|
18
20
|
/** Approximate total file size in bytes (no floor — small values are valid). */
|
|
19
21
|
targetBytes: number;
|
|
20
22
|
modality?: Modality;
|
|
21
|
-
/**
|
|
23
|
+
/** UID namespace (hash-derived UIDs); random when both salt and seed omitted. */
|
|
24
|
+
uidSalt?: string;
|
|
25
|
+
/** Bridged to a salt when uidSalt is omitted; fixes the instance UIDs. */
|
|
22
26
|
seed?: number;
|
|
23
27
|
/** Explicit UID set — used as-is (e.g. collection threading shared study/series UIDs). */
|
|
24
28
|
uid?: UidSet;
|
|
@@ -3,12 +3,14 @@ export type UidSet = {
|
|
|
3
3
|
series: string;
|
|
4
4
|
sop: string;
|
|
5
5
|
};
|
|
6
|
+
export declare function hashUid(salt: string, key: string): string;
|
|
7
|
+
export declare function seedToSalt(seed: number | undefined): string | undefined;
|
|
6
8
|
export declare const PARAMETRIC_STREAM_OFFSET = 2246822507;
|
|
7
9
|
export declare function seededStream(seed: number, offset: number, a: number, b: number): () => number;
|
|
8
10
|
export type GroupUidGenerator = {
|
|
9
11
|
study: (studyIndex: number) => string;
|
|
10
12
|
series: (studyIndex: number, seriesIndex: number) => string;
|
|
11
13
|
};
|
|
12
|
-
export declare function makeGroupUidGenerator(
|
|
14
|
+
export declare function makeGroupUidGenerator(salt?: string): GroupUidGenerator;
|
|
13
15
|
export type UidGenerator = (fileIndex: number) => UidSet;
|
|
14
|
-
export declare function makeUidGenerator(
|
|
16
|
+
export declare function makeUidGenerator(salt?: string): UidGenerator;
|