iso2x-wasm 0.1.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.
@@ -0,0 +1,10 @@
1
+ import type { SourceOptions, SourcePart, SourceReadFn } from './types.js';
2
+ /**
3
+ * Builds a bootable "default.xbe" attach stub for an OGX (original Xbox)
4
+ * source.
5
+ *
6
+ * Throws if `source` resolves to a GoD (Xbox 360 / XEX) source - this
7
+ * only makes sense for OGX, and the wasm side enforces that regardless
8
+ * of what the UI does.
9
+ */
10
+ export declare function generateAttachXbe(readFn: SourceReadFn, fileSize: number, source: SourceOptions, sourceParts?: SourcePart[]): Uint8Array;
package/dist/attach.js ADDED
@@ -0,0 +1,12 @@
1
+ import { generateAttachXbe as rawGenerateAttachXbe } from './wasm/iso2x_wasm.js';
2
+ /**
3
+ * Builds a bootable "default.xbe" attach stub for an OGX (original Xbox)
4
+ * source.
5
+ *
6
+ * Throws if `source` resolves to a GoD (Xbox 360 / XEX) source - this
7
+ * only makes sense for OGX, and the wasm side enforces that regardless
8
+ * of what the UI does.
9
+ */
10
+ export function generateAttachXbe(readFn, fileSize, source, sourceParts) {
11
+ return rawGenerateAttachXbe(readFn, fileSize, source, sourceParts);
12
+ }
@@ -0,0 +1,53 @@
1
+ import type { IsoRootOffsetCandidate } from './types.js';
2
+ export declare function mhtSize(): number;
3
+ export declare function cisoSectorSize(): number;
4
+ export declare function cisoSizingBatchSectors(): number;
5
+ export declare function cciSectorSize(): number;
6
+ /**
7
+ * Sectors processed per `hashNextPart()` call while sizing 'cci' output -
8
+ * same role as `cisoSizingBatchSectors()`, kept separate since the two
9
+ * formats' sizing loops are independent and their batch sizes aren't
10
+ * guaranteed to match.
11
+ */
12
+ export declare function cciSizingBatchSectors(): number;
13
+ /**
14
+ * Threshold (0xffbf6000, ~4.29 GB) at which ciso output splits across
15
+ * "name.1.cso", "name.2.cso", etc.
16
+ */
17
+ export declare function cisoFileSplitPoint(): number;
18
+ /**
19
+ * Byte modulus (0x400) every ciso split part's on-disk size is padded up
20
+ * to.
21
+ */
22
+ export declare function cisoFilePaddingModulus(): number;
23
+ /**
24
+ * Threshold (0xFF000000, ~4.28 GB) at which cci output splits across
25
+ * "name.1.cci", "name.2.cci", etc., once a part's output crosses it.
26
+ * Unlike ciso, each cci part is fully self-contained - its own header and
27
+ * index - rather than sharing one index in part 1.
28
+ */
29
+ export declare function cciFileSplitPoint(): number;
30
+ /**
31
+ * Threshold (0xFF000000, ~4.28 GB) at which xiso output splits across
32
+ * "name.1.xiso.iso", "name.2.xiso.iso", etc., when the `split` option is
33
+ * enabled. Numerically the same boundary as `cciFileSplitPoint()` - both
34
+ * are chosen to keep each part under the ~4 GiB FATX/FAT32 single-file
35
+ * limit - but xiso applies it to a raw sector stream with no per-part
36
+ * header, unlike cci's self-contained parts.
37
+ */
38
+ export declare function xisoSplitMargin(): number;
39
+ /**
40
+ * The fixed set of root_offset values probed when detecting where the
41
+ * XDVDFS volume starts within a file, returned in the order they're
42
+ * checked. Shared by 'xiso', 'god', and 'extracted' detection - not an
43
+ * arbitrary or sector-by-sector scan, only these candidates are ever
44
+ * tried. Exposed so tests/tooling needing a real, detectable nonzero
45
+ * offset can pull one from here instead of hardcoding the magic number.
46
+ */
47
+ export declare function isoRootOffsetCandidates(): IsoRootOffsetCandidate[];
48
+ /**
49
+ * Uncompressed size of one ZAR data block (`_ZARCHIVE::COMPRESSED_BLOCK_SIZE`
50
+ * in the reference implementation) - every block of a ZAR archive's input
51
+ * data is compressed independently at this size.
52
+ */
53
+ export declare function zarBlockSize(): number;
@@ -0,0 +1,75 @@
1
+ import { mhtSize as rawMhtSize, cisoSectorSize as rawCisoSectorSize, cciSectorSize as rawCciSectorSize, cisoSizingBatchSectors as rawCisoSizingBatchSectors, cciSizingBatchSectors as rawCciSizingBatchSectors, cisoFileSplitPoint as rawCisoFileSplitPoint, cciFileSplitPoint as rawCciFileSplitPoint, cisoFilePaddingModulus as rawCisoFilePaddingModulus, xisoSplitMargin as rawXisoSplitMargin, isoRootOffsetCandidates as rawIsoRootOffsetCandidates, zarBlockSize as rawZarBlockSize, } from './wasm/iso2x_wasm.js';
2
+ export function mhtSize() {
3
+ return rawMhtSize();
4
+ }
5
+ export function cisoSectorSize() {
6
+ return rawCisoSectorSize();
7
+ }
8
+ export function cisoSizingBatchSectors() {
9
+ return rawCisoSizingBatchSectors();
10
+ }
11
+ export function cciSectorSize() {
12
+ return rawCciSectorSize();
13
+ }
14
+ /**
15
+ * Sectors processed per `hashNextPart()` call while sizing 'cci' output -
16
+ * same role as `cisoSizingBatchSectors()`, kept separate since the two
17
+ * formats' sizing loops are independent and their batch sizes aren't
18
+ * guaranteed to match.
19
+ */
20
+ export function cciSizingBatchSectors() {
21
+ return rawCciSizingBatchSectors();
22
+ }
23
+ /**
24
+ * Threshold (0xffbf6000, ~4.29 GB) at which ciso output splits across
25
+ * "name.1.cso", "name.2.cso", etc.
26
+ */
27
+ export function cisoFileSplitPoint() {
28
+ return rawCisoFileSplitPoint();
29
+ }
30
+ /**
31
+ * Byte modulus (0x400) every ciso split part's on-disk size is padded up
32
+ * to.
33
+ */
34
+ export function cisoFilePaddingModulus() {
35
+ return rawCisoFilePaddingModulus();
36
+ }
37
+ /**
38
+ * Threshold (0xFF000000, ~4.28 GB) at which cci output splits across
39
+ * "name.1.cci", "name.2.cci", etc., once a part's output crosses it.
40
+ * Unlike ciso, each cci part is fully self-contained - its own header and
41
+ * index - rather than sharing one index in part 1.
42
+ */
43
+ export function cciFileSplitPoint() {
44
+ return rawCciFileSplitPoint();
45
+ }
46
+ /**
47
+ * Threshold (0xFF000000, ~4.28 GB) at which xiso output splits across
48
+ * "name.1.xiso.iso", "name.2.xiso.iso", etc., when the `split` option is
49
+ * enabled. Numerically the same boundary as `cciFileSplitPoint()` - both
50
+ * are chosen to keep each part under the ~4 GiB FATX/FAT32 single-file
51
+ * limit - but xiso applies it to a raw sector stream with no per-part
52
+ * header, unlike cci's self-contained parts.
53
+ */
54
+ export function xisoSplitMargin() {
55
+ return rawXisoSplitMargin();
56
+ }
57
+ /**
58
+ * The fixed set of root_offset values probed when detecting where the
59
+ * XDVDFS volume starts within a file, returned in the order they're
60
+ * checked. Shared by 'xiso', 'god', and 'extracted' detection - not an
61
+ * arbitrary or sector-by-sector scan, only these candidates are ever
62
+ * tried. Exposed so tests/tooling needing a real, detectable nonzero
63
+ * offset can pull one from here instead of hardcoding the magic number.
64
+ */
65
+ export function isoRootOffsetCandidates() {
66
+ return rawIsoRootOffsetCandidates();
67
+ }
68
+ /**
69
+ * Uncompressed size of one ZAR data block (`_ZARCHIVE::COMPRESSED_BLOCK_SIZE`
70
+ * in the reference implementation) - every block of a ZAR archive's input
71
+ * data is compressed independently at this size.
72
+ */
73
+ export function zarBlockSize() {
74
+ return rawZarBlockSize();
75
+ }
@@ -0,0 +1,53 @@
1
+ import type { ConversionFormat, ResolvedSource, SourcePart, SourceReadFn, SplitCciResult, SplitXisoResult } from './types.js';
2
+ /**
3
+ * Single dropped file: cheap magic-byte detection (xiso/ciso/cci). Reads
4
+ * only the first 4 bytes via `readFn`, so it's safe to call speculatively
5
+ * on every file drop.
6
+ */
7
+ export declare function detectFormat(readFn: SourceReadFn, fileSize: number): ConversionFormat;
8
+ /**
9
+ * Dropped folder: shape-based detection (god/extracted). `entries` is the
10
+ * path of every regular file in the folder, relative to the folder root
11
+ * and forward-slash separated - e.g. `.webkitRelativePath` values from a
12
+ * `webkitdirectory` input.
13
+ *
14
+ * Returns `undefined` if the folder is neither god- nor extracted-shaped -
15
+ * treat that as a batch dir and resolve entries individually with
16
+ * `detectFormat`.
17
+ */
18
+ export declare function detectDirFormat(entries: string[]): ConversionFormat | undefined;
19
+ /**
20
+ * Resolves a dropped input to a format once, before anything is opened.
21
+ * Every downstream call (`inspectSource`, `ConversionSession.open`) should
22
+ * be given the `format` this returns rather than being left to guess.
23
+ */
24
+ export declare function resolveSource(input: {
25
+ readFn: SourceReadFn;
26
+ fileSize: number;
27
+ } | {
28
+ entries: string[];
29
+ parts: SourcePart[];
30
+ }): ResolvedSource;
31
+ /**
32
+ * CCI-specific split detection. Unlike a raw XISO split - where part 2 is
33
+ * just a continuation of part 1's byte stream and carries no header of its
34
+ * own - each CCI part is fully self-contained (own magic, own header, own
35
+ * index). A genuine ".1.cci"/".2.cci" pair should have both files
36
+ * independently detect as 'cci'; if not, surface that as an error rather
37
+ * than silently opening a broken source.
38
+ */
39
+ export declare function findCciSplitParts(entries: string[], makeReadFn: (name: string) => SourceReadFn, fileSize: (name: string) => number): SplitCciResult | null;
40
+ /**
41
+ * Raw XISO split detection. Part 2+ carry no magic of their own - just the
42
+ * continuation of part 1's sector stream - so there's nothing to
43
+ * independently verify the way findCciSplitParts does. This only confirms
44
+ * the pair exists; detectFormat still only needs to run against part 1.
45
+ */
46
+ export declare function findXisoSplitParts(entries: string[]): SplitXisoResult | null;
47
+ /**
48
+ * Resolves one entry of a batch dir: checks for a CCI split pair first
49
+ * (self-verifying, so a mislabeled pair fails loudly here rather than
50
+ * deeper inside the writer), then an XISO split pair, then falls back to
51
+ * plain single-file resolution.
52
+ */
53
+ export declare function resolveBatchEntry(entries: string[], makeReadFn: (name: string) => SourceReadFn, fileSize: (name: string) => number): Promise<ResolvedSource>;
package/dist/detect.js ADDED
@@ -0,0 +1,128 @@
1
+ import { detectFormat as rawDetectFormat, detectDirFormat as rawDetectDirFormat, } from './wasm/iso2x_wasm.js';
2
+ /**
3
+ * Single dropped file: cheap magic-byte detection (xiso/ciso/cci). Reads
4
+ * only the first 4 bytes via `readFn`, so it's safe to call speculatively
5
+ * on every file drop.
6
+ */
7
+ export function detectFormat(readFn, fileSize) {
8
+ return rawDetectFormat(readFn, fileSize);
9
+ }
10
+ /**
11
+ * Dropped folder: shape-based detection (god/extracted). `entries` is the
12
+ * path of every regular file in the folder, relative to the folder root
13
+ * and forward-slash separated - e.g. `.webkitRelativePath` values from a
14
+ * `webkitdirectory` input.
15
+ *
16
+ * Returns `undefined` if the folder is neither god- nor extracted-shaped -
17
+ * treat that as a batch dir and resolve entries individually with
18
+ * `detectFormat`.
19
+ */
20
+ export function detectDirFormat(entries) {
21
+ return rawDetectDirFormat(entries);
22
+ }
23
+ /**
24
+ * Resolves a dropped input to a format once, before anything is opened.
25
+ * Every downstream call (`inspectSource`, `ConversionSession.open`) should
26
+ * be given the `format` this returns rather than being left to guess.
27
+ */
28
+ export function resolveSource(input) {
29
+ if ('readFn' in input) {
30
+ const format = detectFormat(input.readFn, input.fileSize);
31
+ return {
32
+ kind: 'file',
33
+ format,
34
+ readFn: input.readFn,
35
+ fileSize: input.fileSize,
36
+ };
37
+ }
38
+ const format = detectDirFormat(input.entries);
39
+ if (!format) {
40
+ throw new Error('folder does not look like a God or Extracted source');
41
+ }
42
+ return { kind: 'dir', format, parts: input.parts };
43
+ }
44
+ /**
45
+ * Locates a two-file split source named "<stem>.1.<ext>" / "<stem>.2.<ext>"
46
+ * for one specific extension (case-insensitive). Generic building block -
47
+ * findCciSplitParts/findXisoSplitParts add their own format-specific
48
+ * validation on top, since a matching part 2 name means different things
49
+ * for each format.
50
+ */
51
+ function findSplitParts(entries, extension) {
52
+ const ext = extension.replace(/^\./, '').toLowerCase();
53
+ const re = new RegExp(`\\.1\\.${ext}$`, 'i');
54
+ const part1 = entries.find((e) => re.test(e));
55
+ if (!part1)
56
+ return null;
57
+ const part2 = part1.replace(/\.1\.([^.]+)$/i, '.2.$1');
58
+ const match = entries.find((e) => e.toLowerCase() === part2.toLowerCase());
59
+ return match ? [part1, match] : null;
60
+ }
61
+ /**
62
+ * CCI-specific split detection. Unlike a raw XISO split - where part 2 is
63
+ * just a continuation of part 1's byte stream and carries no header of its
64
+ * own - each CCI part is fully self-contained (own magic, own header, own
65
+ * index). A genuine ".1.cci"/".2.cci" pair should have both files
66
+ * independently detect as 'cci'; if not, surface that as an error rather
67
+ * than silently opening a broken source.
68
+ */
69
+ export function findCciSplitParts(entries, makeReadFn, fileSize) {
70
+ const found = findSplitParts(entries, 'cci');
71
+ if (!found)
72
+ return null;
73
+ const [name1, name2] = found;
74
+ const format1 = detectFormat(makeReadFn(name1), fileSize(name1));
75
+ const format2 = detectFormat(makeReadFn(name2), fileSize(name2));
76
+ if (format1 !== 'cci' || format2 !== 'cci') {
77
+ throw new Error(`found "${name1}"/"${name2}" but one or both parts don't have a ` +
78
+ `valid CCI header - refusing to treat this as a split CCI source`);
79
+ }
80
+ return { kind: 'cci-split', parts: [name1, name2] };
81
+ }
82
+ /**
83
+ * Raw XISO split detection. Part 2+ carry no magic of their own - just the
84
+ * continuation of part 1's sector stream - so there's nothing to
85
+ * independently verify the way findCciSplitParts does. This only confirms
86
+ * the pair exists; detectFormat still only needs to run against part 1.
87
+ */
88
+ export function findXisoSplitParts(entries) {
89
+ const found = findSplitParts(entries, 'iso');
90
+ if (!found)
91
+ return null;
92
+ return { kind: 'xiso-split', parts: found };
93
+ }
94
+ /**
95
+ * Resolves one entry of a batch dir: checks for a CCI split pair first
96
+ * (self-verifying, so a mislabeled pair fails loudly here rather than
97
+ * deeper inside the writer), then an XISO split pair, then falls back to
98
+ * plain single-file resolution.
99
+ */
100
+ export async function resolveBatchEntry(entries, makeReadFn, fileSize) {
101
+ const cci = findCciSplitParts(entries, makeReadFn, fileSize);
102
+ if (cci) {
103
+ return {
104
+ kind: 'dir',
105
+ format: 'cci',
106
+ parts: cci.parts.map((name) => ({
107
+ name,
108
+ size: fileSize(name),
109
+ readFn: makeReadFn(name),
110
+ })),
111
+ };
112
+ }
113
+ const xiso = findXisoSplitParts(entries);
114
+ if (xiso) {
115
+ const format = detectFormat(makeReadFn(xiso.parts[0]), fileSize(xiso.parts[0]));
116
+ return {
117
+ kind: 'dir',
118
+ format,
119
+ parts: xiso.parts.map((name) => ({
120
+ name,
121
+ size: fileSize(name),
122
+ readFn: makeReadFn(name),
123
+ })),
124
+ };
125
+ }
126
+ const [name] = entries;
127
+ return resolveSource({ readFn: makeReadFn(name), fileSize: fileSize(name) });
128
+ }
@@ -0,0 +1,9 @@
1
+ import initWasm, { chainMhtDigest } from './wasm/iso2x_wasm.js';
2
+ export default initWasm;
3
+ export { chainMhtDigest };
4
+ export * from './types.js';
5
+ export * from './constants.js';
6
+ export * from './detect.js';
7
+ export * from './inspect.js';
8
+ export * from './session.js';
9
+ export * from './attach.js';
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ import initWasm, { chainMhtDigest } from './wasm/iso2x_wasm.js';
2
+ export default initWasm;
3
+ export { chainMhtDigest };
4
+ export * from './types.js';
5
+ export * from './constants.js';
6
+ export * from './detect.js';
7
+ export * from './inspect.js';
8
+ export * from './session.js';
9
+ export * from './attach.js';
@@ -0,0 +1,2 @@
1
+ import type { SourceInfoResult, SourceOptions, SourcePart, SourceReadFn } from './types.js';
2
+ export declare function inspectSource(readFn: SourceReadFn, fileSize: number, source: SourceOptions, sourceParts?: SourcePart[]): SourceInfoResult;
@@ -0,0 +1,14 @@
1
+ import { inspectSource as rawInspectSource } from './wasm/iso2x_wasm.js';
2
+ export function inspectSource(readFn, fileSize, source, sourceParts) {
3
+ const info = rawInspectSource(readFn, fileSize, source, sourceParts);
4
+ try {
5
+ return {
6
+ titleId: info.titleId(),
7
+ contentType: info.contentType(),
8
+ detectedTitle: info.detectedTitle(),
9
+ };
10
+ }
11
+ finally {
12
+ info.free();
13
+ }
14
+ }
@@ -0,0 +1,117 @@
1
+ import type { OpenConversionSessionOptions, SourceOptions, SourcePart, SourceReadFn } from './types.js';
2
+ /**
3
+ * One pull-based session type for every output format.
4
+ *
5
+ * Usage is identical regardless of format:
6
+ *
7
+ * const resolved = resolveSource({ readFn, fileSize });
8
+ * const session = ConversionSession.open(readFn, fileSize, options, { format: resolved.format });
9
+ * while (!session.isDone()) {
10
+ * const chunk = session.nextChunk(maxBytes);
11
+ * if (chunk) yield chunk;
12
+ * }
13
+ * session.free();
14
+ *
15
+ * For 'god', 'ciso', and 'cci', hashing/sizing must be driven to completion
16
+ * before any nextChunk() calls (see hashNextPart()). For 'extracted',
17
+ * 'ciso', and 'cci', call currentEntryName() after each nextChunk() to know
18
+ * which output file the chunk belongs to - always route by
19
+ * currentEntryName() for these three rather than special-casing "no split
20
+ * happened". 'god' and 'xiso' return null there, since they're always a
21
+ * single anonymous output stream. 'zar' also always returns a single
22
+ * stream, but currentEntryName() there is non-null - see the method's own
23
+ * doc comment.
24
+ */
25
+ export declare class ConversionSession {
26
+ private readonly raw;
27
+ private constructor();
28
+ /**
29
+ * `source` is required, not optional - an omitted `source` no longer
30
+ * defaults to xiso. Resolve it first via `resolveSource`/
31
+ * `detectFormat`/`detectDirFormat`.
32
+ */
33
+ static open(readFn: SourceReadFn, fileSize: number, options: OpenConversionSessionOptions, source: SourceOptions, sourceParts?: SourcePart[]): ConversionSession;
34
+ /**
35
+ * For 'god', 'ciso', and 'cci': hashes/sizes exactly one more part (or
36
+ * sizing batch) and returns whether that pre-streaming pass is now
37
+ * complete. For 'ciso' this drives the sizing pass that determines
38
+ * exact split boundaries and `outputManifest()`; for 'cci' it
39
+ * additionally has to finish sizing each split part before that part's
40
+ * header/index bytes can be built - nextChunk() throws if called before
41
+ * this returns true. For 'xiso', 'extracted', and 'zar' this is a no-op
42
+ * that returns true immediately, so it's safe to call unconditionally.
43
+ *
44
+ * Bounded cost per call is the point: call this in a loop with an await
45
+ * between calls (e.g. `await new Promise(r => setTimeout(r, 0))`) so
46
+ * the caller's event loop gets control back regularly.
47
+ */
48
+ hashNextPart(): boolean;
49
+ /** Returns the next chunk, or null once the session is exhausted. */
50
+ nextChunk(maxBytes: number): Uint8Array<ArrayBuffer> | null;
51
+ isDone(): boolean;
52
+ /**
53
+ * Authoritative count of `totalUnits()` completed so far, for formats
54
+ * where `nextChunk()`'s returned byte length isn't a reliable progress
55
+ * proxy (currently only 'zar' - its chunks are compressed output
56
+ * bytes, while `totalUnits()` is raw input bytes). `null` for every
57
+ * other format; callers should keep deriving progress from received
58
+ * chunk bytes in that case.
59
+ */
60
+ unitsDone(): number | null;
61
+ /**
62
+ * Sectors for xiso, parts for god, file count for extracted/ciso/cci,
63
+ * total input bytes for zar (there's no fixed-size unit to count -
64
+ * every file is addressed by name and packed byte-for-byte, so bytes
65
+ * is the most meaningful progress denominator).
66
+ */
67
+ totalUnits(): number;
68
+ /**
69
+ * `{ name, size }` per output file for formats that produce more than
70
+ * one (god, extracted, ciso, cci) - feed this straight into your
71
+ * zip-download tooling's file-list metadata. 'zar' also populates this,
72
+ * even though it only ever produces one output file: its final size
73
+ * isn't known until streaming reaches the footer phase (it depends on
74
+ * every block's actual compressed size), so - like ciso/cci - it stays
75
+ * empty until then rather than reporting a size up front.
76
+ *
77
+ * For 'god' and 'extracted', safe to call any time after open(). For
78
+ * 'ciso', 'cci', and 'zar', empty until hashNextPart()/streaming
79
+ * reaches completion: exact sizes depend on per-sector/per-block
80
+ * compression. Even a non-split ciso/cci output reports one entry once
81
+ * sizing is done, and zar always reports exactly one entry once done.
82
+ * Empty array for 'xiso' - use `totalUnits() * 2048` for its predicted
83
+ * size instead.
84
+ */
85
+ outputManifest(): {
86
+ name: string;
87
+ size: number;
88
+ }[];
89
+ /**
90
+ * Name of the file the most recent nextChunk() call's bytes belong to.
91
+ * Meaningful for 'extracted', 'ciso', and 'cci'; null for 'god' and
92
+ * 'xiso'. For 'zar' this is also non-null, but - unlike
93
+ * extracted/ciso/cci - it never changes across the session's lifetime,
94
+ * since zar only ever produces the one output stream; it's still worth
95
+ * reading (rather than treated as null-equivalent) since it reports the
96
+ * output filename stem rather than being merely anonymous.
97
+ */
98
+ currentEntryName(): string | null;
99
+ free(): void;
100
+ }
101
+ /**
102
+ * Drop-in async generator for a chunk-streaming loop. Yields bytes in
103
+ * order for any format.
104
+ *
105
+ * This generator doesn't route chunks to different output files - it just
106
+ * yields bytes in order. For 'extracted', 'ciso', and 'cci', the caller
107
+ * must additionally check session.currentEntryName() after each chunk and
108
+ * switch output files when it changes; a chunk never straddles two entry
109
+ * names, so it's safe to check once per yielded chunk. 'zar' also reports
110
+ * a non-null currentEntryName(), but it never changes, so no switching
111
+ * logic is needed for it.
112
+ *
113
+ * For 'god', 'ciso', and 'cci' sessions, the caller is responsible for
114
+ * driving session.hashNextPart() to completion before iterating this
115
+ * generator.
116
+ */
117
+ export declare function chunksFromSession(session: ConversionSession, maxBytes: number, waitForRoom: () => Promise<void>): AsyncGenerator<Uint8Array>;
@@ -0,0 +1,143 @@
1
+ import { openConversionSession as rawOpenConversionSession, } from './wasm/iso2x_wasm.js';
2
+ /**
3
+ * One pull-based session type for every output format.
4
+ *
5
+ * Usage is identical regardless of format:
6
+ *
7
+ * const resolved = resolveSource({ readFn, fileSize });
8
+ * const session = ConversionSession.open(readFn, fileSize, options, { format: resolved.format });
9
+ * while (!session.isDone()) {
10
+ * const chunk = session.nextChunk(maxBytes);
11
+ * if (chunk) yield chunk;
12
+ * }
13
+ * session.free();
14
+ *
15
+ * For 'god', 'ciso', and 'cci', hashing/sizing must be driven to completion
16
+ * before any nextChunk() calls (see hashNextPart()). For 'extracted',
17
+ * 'ciso', and 'cci', call currentEntryName() after each nextChunk() to know
18
+ * which output file the chunk belongs to - always route by
19
+ * currentEntryName() for these three rather than special-casing "no split
20
+ * happened". 'god' and 'xiso' return null there, since they're always a
21
+ * single anonymous output stream. 'zar' also always returns a single
22
+ * stream, but currentEntryName() there is non-null - see the method's own
23
+ * doc comment.
24
+ */
25
+ export class ConversionSession {
26
+ raw;
27
+ constructor(raw) {
28
+ this.raw = raw;
29
+ }
30
+ /**
31
+ * `source` is required, not optional - an omitted `source` no longer
32
+ * defaults to xiso. Resolve it first via `resolveSource`/
33
+ * `detectFormat`/`detectDirFormat`.
34
+ */
35
+ static open(readFn, fileSize, options, source, sourceParts) {
36
+ return new ConversionSession(rawOpenConversionSession(readFn, fileSize, options, source, sourceParts));
37
+ }
38
+ /**
39
+ * For 'god', 'ciso', and 'cci': hashes/sizes exactly one more part (or
40
+ * sizing batch) and returns whether that pre-streaming pass is now
41
+ * complete. For 'ciso' this drives the sizing pass that determines
42
+ * exact split boundaries and `outputManifest()`; for 'cci' it
43
+ * additionally has to finish sizing each split part before that part's
44
+ * header/index bytes can be built - nextChunk() throws if called before
45
+ * this returns true. For 'xiso', 'extracted', and 'zar' this is a no-op
46
+ * that returns true immediately, so it's safe to call unconditionally.
47
+ *
48
+ * Bounded cost per call is the point: call this in a loop with an await
49
+ * between calls (e.g. `await new Promise(r => setTimeout(r, 0))`) so
50
+ * the caller's event loop gets control back regularly.
51
+ */
52
+ hashNextPart() {
53
+ return this.raw.hashNextPart();
54
+ }
55
+ /** Returns the next chunk, or null once the session is exhausted. */
56
+ nextChunk(maxBytes) {
57
+ const chunk = this.raw.nextChunk(maxBytes);
58
+ return chunk ? new Uint8Array(chunk) : null;
59
+ }
60
+ isDone() {
61
+ return this.raw.isDone();
62
+ }
63
+ /**
64
+ * Authoritative count of `totalUnits()` completed so far, for formats
65
+ * where `nextChunk()`'s returned byte length isn't a reliable progress
66
+ * proxy (currently only 'zar' - its chunks are compressed output
67
+ * bytes, while `totalUnits()` is raw input bytes). `null` for every
68
+ * other format; callers should keep deriving progress from received
69
+ * chunk bytes in that case.
70
+ */
71
+ unitsDone() {
72
+ return this.raw.unitsDone() ?? null;
73
+ }
74
+ /**
75
+ * Sectors for xiso, parts for god, file count for extracted/ciso/cci,
76
+ * total input bytes for zar (there's no fixed-size unit to count -
77
+ * every file is addressed by name and packed byte-for-byte, so bytes
78
+ * is the most meaningful progress denominator).
79
+ */
80
+ totalUnits() {
81
+ return this.raw.totalUnits();
82
+ }
83
+ /**
84
+ * `{ name, size }` per output file for formats that produce more than
85
+ * one (god, extracted, ciso, cci) - feed this straight into your
86
+ * zip-download tooling's file-list metadata. 'zar' also populates this,
87
+ * even though it only ever produces one output file: its final size
88
+ * isn't known until streaming reaches the footer phase (it depends on
89
+ * every block's actual compressed size), so - like ciso/cci - it stays
90
+ * empty until then rather than reporting a size up front.
91
+ *
92
+ * For 'god' and 'extracted', safe to call any time after open(). For
93
+ * 'ciso', 'cci', and 'zar', empty until hashNextPart()/streaming
94
+ * reaches completion: exact sizes depend on per-sector/per-block
95
+ * compression. Even a non-split ciso/cci output reports one entry once
96
+ * sizing is done, and zar always reports exactly one entry once done.
97
+ * Empty array for 'xiso' - use `totalUnits() * 2048` for its predicted
98
+ * size instead.
99
+ */
100
+ outputManifest() {
101
+ return this.raw.outputManifest();
102
+ }
103
+ /**
104
+ * Name of the file the most recent nextChunk() call's bytes belong to.
105
+ * Meaningful for 'extracted', 'ciso', and 'cci'; null for 'god' and
106
+ * 'xiso'. For 'zar' this is also non-null, but - unlike
107
+ * extracted/ciso/cci - it never changes across the session's lifetime,
108
+ * since zar only ever produces the one output stream; it's still worth
109
+ * reading (rather than treated as null-equivalent) since it reports the
110
+ * output filename stem rather than being merely anonymous.
111
+ */
112
+ currentEntryName() {
113
+ return this.raw.currentEntryName() ?? null;
114
+ }
115
+ free() {
116
+ this.raw.free();
117
+ }
118
+ }
119
+ /**
120
+ * Drop-in async generator for a chunk-streaming loop. Yields bytes in
121
+ * order for any format.
122
+ *
123
+ * This generator doesn't route chunks to different output files - it just
124
+ * yields bytes in order. For 'extracted', 'ciso', and 'cci', the caller
125
+ * must additionally check session.currentEntryName() after each chunk and
126
+ * switch output files when it changes; a chunk never straddles two entry
127
+ * names, so it's safe to check once per yielded chunk. 'zar' also reports
128
+ * a non-null currentEntryName(), but it never changes, so no switching
129
+ * logic is needed for it.
130
+ *
131
+ * For 'god', 'ciso', and 'cci' sessions, the caller is responsible for
132
+ * driving session.hashNextPart() to completion before iterating this
133
+ * generator.
134
+ */
135
+ export async function* chunksFromSession(session, maxBytes, waitForRoom) {
136
+ while (!session.isDone()) {
137
+ await waitForRoom();
138
+ const chunk = session.nextChunk(maxBytes);
139
+ if (chunk === null)
140
+ break;
141
+ yield chunk;
142
+ }
143
+ }