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.
- package/dist/attach.d.ts +10 -0
- package/dist/attach.js +12 -0
- package/dist/constants.d.ts +53 -0
- package/dist/constants.js +75 -0
- package/dist/detect.d.ts +53 -0
- package/dist/detect.js +128 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/inspect.d.ts +2 -0
- package/dist/inspect.js +14 -0
- package/dist/session.d.ts +117 -0
- package/dist/session.js +143 -0
- package/dist/types.d.ts +147 -0
- package/dist/types.js +1 -0
- package/dist/wasm/iso2x_wasm.d.ts +216 -0
- package/dist/wasm/iso2x_wasm.js +1111 -0
- package/dist/wasm/iso2x_wasm_bg.wasm +0 -0
- package/dist/wasm/iso2x_wasm_bg.wasm.d.ts +37 -0
- package/package.json +39 -0
package/dist/attach.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/detect.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
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';
|
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';
|
package/dist/inspect.js
ADDED
|
@@ -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>;
|
package/dist/session.js
ADDED
|
@@ -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
|
+
}
|