@tootallnate/cnmt 0.0.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/.turbo/turbo-build.log +4 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +99 -0
- package/dist/index.js.map +1 -0
- package/package.json +17 -0
- package/src/index.ts +144 -0
- package/test/index.test.ts +105 -0
- package/tsconfig.json +14 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CNMT (Content Meta) builder for Nintendo Switch.
|
|
3
|
+
*
|
|
4
|
+
* Builds CNMT binary data used inside Meta NCAs to describe
|
|
5
|
+
* the contents (NCAs) of an application package.
|
|
6
|
+
*
|
|
7
|
+
* Reference: hacbrewpack/cnmt.c, hacbrewpack/cnmt.h
|
|
8
|
+
*/
|
|
9
|
+
/** CNMT content types */
|
|
10
|
+
export declare enum ContentType {
|
|
11
|
+
Meta = 0,
|
|
12
|
+
Program = 1,
|
|
13
|
+
Data = 2,
|
|
14
|
+
Control = 3,
|
|
15
|
+
HtmlDocument = 4,
|
|
16
|
+
LegalInformation = 5,
|
|
17
|
+
DeltaFragment = 6
|
|
18
|
+
}
|
|
19
|
+
/** CNMT meta type */
|
|
20
|
+
export declare enum MetaType {
|
|
21
|
+
SystemProgram = 1,
|
|
22
|
+
SystemData = 2,
|
|
23
|
+
SystemUpdate = 3,
|
|
24
|
+
BootImagePackage = 4,
|
|
25
|
+
BootImagePackageSafe = 5,
|
|
26
|
+
Application = 128,
|
|
27
|
+
Patch = 129,
|
|
28
|
+
AddOnContent = 130,
|
|
29
|
+
Delta = 131
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A content record describing one NCA.
|
|
33
|
+
*/
|
|
34
|
+
export interface ContentRecord {
|
|
35
|
+
/** SHA-256 hash of the entire NCA file (32 bytes) */
|
|
36
|
+
hash: Uint8Array;
|
|
37
|
+
/** NCA ID: first 16 bytes of the hash */
|
|
38
|
+
ncaId: Uint8Array;
|
|
39
|
+
/** NCA file size (up to 6 bytes / 48 bits) */
|
|
40
|
+
size: number;
|
|
41
|
+
/** Content type */
|
|
42
|
+
type: ContentType;
|
|
43
|
+
/** ID offset (usually 0) */
|
|
44
|
+
idOffset?: number;
|
|
45
|
+
}
|
|
46
|
+
export interface CnmtOptions {
|
|
47
|
+
/** Application title ID */
|
|
48
|
+
titleId: bigint;
|
|
49
|
+
/** Title version (default: 0) */
|
|
50
|
+
titleVersion?: number;
|
|
51
|
+
/** Content records for the NCAs in this package */
|
|
52
|
+
contentRecords: ContentRecord[];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Build a CNMT binary blob.
|
|
56
|
+
*
|
|
57
|
+
* @param options - CNMT configuration
|
|
58
|
+
* @returns CNMT binary data as ArrayBuffer
|
|
59
|
+
*/
|
|
60
|
+
export declare function build(options: CnmtOptions): ArrayBuffer;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CNMT (Content Meta) builder for Nintendo Switch.
|
|
3
|
+
*
|
|
4
|
+
* Builds CNMT binary data used inside Meta NCAs to describe
|
|
5
|
+
* the contents (NCAs) of an application package.
|
|
6
|
+
*
|
|
7
|
+
* Reference: hacbrewpack/cnmt.c, hacbrewpack/cnmt.h
|
|
8
|
+
*/
|
|
9
|
+
/** CNMT content types */
|
|
10
|
+
export var ContentType;
|
|
11
|
+
(function (ContentType) {
|
|
12
|
+
ContentType[ContentType["Meta"] = 0] = "Meta";
|
|
13
|
+
ContentType[ContentType["Program"] = 1] = "Program";
|
|
14
|
+
ContentType[ContentType["Data"] = 2] = "Data";
|
|
15
|
+
ContentType[ContentType["Control"] = 3] = "Control";
|
|
16
|
+
ContentType[ContentType["HtmlDocument"] = 4] = "HtmlDocument";
|
|
17
|
+
ContentType[ContentType["LegalInformation"] = 5] = "LegalInformation";
|
|
18
|
+
ContentType[ContentType["DeltaFragment"] = 6] = "DeltaFragment";
|
|
19
|
+
})(ContentType || (ContentType = {}));
|
|
20
|
+
/** CNMT meta type */
|
|
21
|
+
export var MetaType;
|
|
22
|
+
(function (MetaType) {
|
|
23
|
+
MetaType[MetaType["SystemProgram"] = 1] = "SystemProgram";
|
|
24
|
+
MetaType[MetaType["SystemData"] = 2] = "SystemData";
|
|
25
|
+
MetaType[MetaType["SystemUpdate"] = 3] = "SystemUpdate";
|
|
26
|
+
MetaType[MetaType["BootImagePackage"] = 4] = "BootImagePackage";
|
|
27
|
+
MetaType[MetaType["BootImagePackageSafe"] = 5] = "BootImagePackageSafe";
|
|
28
|
+
MetaType[MetaType["Application"] = 128] = "Application";
|
|
29
|
+
MetaType[MetaType["Patch"] = 129] = "Patch";
|
|
30
|
+
MetaType[MetaType["AddOnContent"] = 130] = "AddOnContent";
|
|
31
|
+
MetaType[MetaType["Delta"] = 131] = "Delta";
|
|
32
|
+
})(MetaType || (MetaType = {}));
|
|
33
|
+
/** CNMT header size: 0x20 bytes */
|
|
34
|
+
const HEADER_SIZE = 0x20;
|
|
35
|
+
/** Extended application header size: 0x10 bytes */
|
|
36
|
+
const EXTENDED_APP_HEADER_SIZE = 0x10;
|
|
37
|
+
/** Content record size: 0x38 bytes */
|
|
38
|
+
const CONTENT_RECORD_SIZE = 0x38;
|
|
39
|
+
/** Digest size: 0x20 bytes */
|
|
40
|
+
const DIGEST_SIZE = 0x20;
|
|
41
|
+
/**
|
|
42
|
+
* Build a CNMT binary blob.
|
|
43
|
+
*
|
|
44
|
+
* @param options - CNMT configuration
|
|
45
|
+
* @returns CNMT binary data as ArrayBuffer
|
|
46
|
+
*/
|
|
47
|
+
export function build(options) {
|
|
48
|
+
const { titleId, titleVersion = 0, contentRecords } = options;
|
|
49
|
+
// Total size = header + extended header + content records + digest
|
|
50
|
+
const totalSize = HEADER_SIZE +
|
|
51
|
+
EXTENDED_APP_HEADER_SIZE +
|
|
52
|
+
contentRecords.length * CONTENT_RECORD_SIZE +
|
|
53
|
+
DIGEST_SIZE;
|
|
54
|
+
const buffer = new ArrayBuffer(totalSize);
|
|
55
|
+
const view = new DataView(buffer);
|
|
56
|
+
const bytes = new Uint8Array(buffer);
|
|
57
|
+
let offset = 0;
|
|
58
|
+
// --- CNMT Header (0x20 bytes) ---
|
|
59
|
+
// title_id (8 bytes)
|
|
60
|
+
view.setBigUint64(offset + 0x00, titleId, true);
|
|
61
|
+
// title_version (4 bytes)
|
|
62
|
+
view.setUint32(offset + 0x08, titleVersion, true);
|
|
63
|
+
// type (1 byte)
|
|
64
|
+
view.setUint8(offset + 0x0c, MetaType.Application);
|
|
65
|
+
// padding (1 byte) — already zero
|
|
66
|
+
// extended_header_size (2 bytes)
|
|
67
|
+
view.setUint16(offset + 0x0e, EXTENDED_APP_HEADER_SIZE, true);
|
|
68
|
+
// content_entry_count (2 bytes)
|
|
69
|
+
view.setUint16(offset + 0x10, contentRecords.length, true);
|
|
70
|
+
// meta_entry_count (2 bytes) — 0
|
|
71
|
+
// padding (remaining bytes of header) — already zero
|
|
72
|
+
offset += HEADER_SIZE;
|
|
73
|
+
// --- Extended Application Header (0x10 bytes) ---
|
|
74
|
+
// patch_title_id = title_id + 0x800
|
|
75
|
+
view.setBigUint64(offset + 0x00, titleId + 0x800n, true);
|
|
76
|
+
// required_system_version (4 bytes) — 0
|
|
77
|
+
// padding (4 bytes) — already zero
|
|
78
|
+
offset += EXTENDED_APP_HEADER_SIZE;
|
|
79
|
+
// --- Content Records (0x38 bytes each) ---
|
|
80
|
+
for (const record of contentRecords) {
|
|
81
|
+
// hash (0x20 bytes)
|
|
82
|
+
bytes.set(record.hash.subarray(0, 0x20), offset + 0x00);
|
|
83
|
+
// ncaid (0x10 bytes)
|
|
84
|
+
bytes.set(record.ncaId.subarray(0, 0x10), offset + 0x20);
|
|
85
|
+
// size (6 bytes, little-endian)
|
|
86
|
+
const size = record.size;
|
|
87
|
+
view.setUint32(offset + 0x30, size & 0xffffffff, true);
|
|
88
|
+
view.setUint16(offset + 0x34, Math.floor(size / 0x100000000) & 0xffff, true);
|
|
89
|
+
// type (1 byte)
|
|
90
|
+
view.setUint8(offset + 0x36, record.type);
|
|
91
|
+
// id_offset (1 byte)
|
|
92
|
+
view.setUint8(offset + 0x37, record.idOffset ?? 0);
|
|
93
|
+
offset += CONTENT_RECORD_SIZE;
|
|
94
|
+
}
|
|
95
|
+
// --- Digest (0x20 bytes) ---
|
|
96
|
+
// All zeros (already zeroed from ArrayBuffer allocation)
|
|
97
|
+
return buffer;
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,yBAAyB;AACzB,MAAM,CAAN,IAAY,WAQX;AARD,WAAY,WAAW;IACtB,6CAAQ,CAAA;IACR,mDAAW,CAAA;IACX,6CAAQ,CAAA;IACR,mDAAW,CAAA;IACX,6DAAgB,CAAA;IAChB,qEAAoB,CAAA;IACpB,+DAAiB,CAAA;AAClB,CAAC,EARW,WAAW,KAAX,WAAW,QAQtB;AAED,qBAAqB;AACrB,MAAM,CAAN,IAAY,QAUX;AAVD,WAAY,QAAQ;IACnB,yDAAoB,CAAA;IACpB,mDAAiB,CAAA;IACjB,uDAAmB,CAAA;IACnB,+DAAuB,CAAA;IACvB,uEAA2B,CAAA;IAC3B,uDAAkB,CAAA;IAClB,2CAAY,CAAA;IACZ,yDAAmB,CAAA;IACnB,2CAAY,CAAA;AACb,CAAC,EAVW,QAAQ,KAAR,QAAQ,QAUnB;AAED,mCAAmC;AACnC,MAAM,WAAW,GAAG,IAAI,CAAC;AAEzB,mDAAmD;AACnD,MAAM,wBAAwB,GAAG,IAAI,CAAC;AAEtC,sCAAsC;AACtC,MAAM,mBAAmB,GAAG,IAAI,CAAC;AAEjC,8BAA8B;AAC9B,MAAM,WAAW,GAAG,IAAI,CAAC;AA2BzB;;;;;GAKG;AACH,MAAM,UAAU,KAAK,CAAC,OAAoB;IACzC,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,CAAC,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC;IAE9D,mEAAmE;IACnE,MAAM,SAAS,GACd,WAAW;QACX,wBAAwB;QACxB,cAAc,CAAC,MAAM,GAAG,mBAAmB;QAC3C,WAAW,CAAC;IAEb,MAAM,MAAM,GAAG,IAAI,WAAW,CAAC,SAAS,CAAC,CAAC;IAC1C,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,MAAM,CAAC,CAAC;IAClC,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC;IAErC,IAAI,MAAM,GAAG,CAAC,CAAC;IAEf,mCAAmC;IACnC,qBAAqB;IACrB,IAAI,CAAC,YAAY,CAAC,MAAM,GAAG,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;IAChD,0BAA0B;IAC1B,IAAI,CAAC,SAAS,CAAC,MAAM,GAAG,IAAI,EAAE,YAAY,EAAE,IAAI,CAAC,CAAC;IAClD,gBAAgB;IAChB,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,IAAI,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAC;IACnD,kCAAkC;IAClC,iCAAiC;IACjC,IAAI,CAAC,SAAS,CAAC,MAAM,GAAG,IAAI,EAAE,wBAAwB,EAAE,IAAI,CAAC,CAAC;IAC9D,gCAAgC;IAChC,IAAI,CAAC,SAAS,CAAC,MAAM,GAAG,IAAI,EAAE,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAC3D,iCAAiC;IACjC,qDAAqD;IAErD,MAAM,IAAI,WAAW,CAAC;IAEtB,mDAAmD;IACnD,oCAAoC;IACpC,IAAI,CAAC,YAAY,CAAC,MAAM,GAAG,IAAI,EAAE,OAAO,GAAG,MAAM,EAAE,IAAI,CAAC,CAAC;IACzD,wCAAwC;IACxC,mCAAmC;IAEnC,MAAM,IAAI,wBAAwB,CAAC;IAEnC,4CAA4C;IAC5C,KAAK,MAAM,MAAM,IAAI,cAAc,EAAE,CAAC;QACrC,oBAAoB;QACpB,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;QACxD,qBAAqB;QACrB,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;QACzD,gCAAgC;QAChC,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;QACzB,IAAI,CAAC,SAAS,CAAC,MAAM,GAAG,IAAI,EAAE,IAAI,GAAG,UAAU,EAAE,IAAI,CAAC,CAAC;QACvD,IAAI,CAAC,SAAS,CACb,MAAM,GAAG,IAAI,EACb,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,WAAW,CAAC,GAAG,MAAM,EACvC,IAAI,CACJ,CAAC;QACF,gBAAgB;QAChB,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;QAC1C,qBAAqB;QACrB,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,IAAI,EAAE,MAAM,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC;QAEnD,MAAM,IAAI,mBAAmB,CAAC;IAC/B,CAAC;IAED,8BAA8B;IAC9B,yDAAyD;IAEzD,OAAO,MAAM,CAAC;AACf,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tootallnate/cnmt",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "CNMT (Content Meta) builder for Nintendo Switch NCAs",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"test": "vitest run"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [],
|
|
12
|
+
"author": "Nathan Rajlich <n@n8.io>",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"typescript": "^5.3.3"
|
|
16
|
+
}
|
|
17
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CNMT (Content Meta) builder for Nintendo Switch.
|
|
3
|
+
*
|
|
4
|
+
* Builds CNMT binary data used inside Meta NCAs to describe
|
|
5
|
+
* the contents (NCAs) of an application package.
|
|
6
|
+
*
|
|
7
|
+
* Reference: hacbrewpack/cnmt.c, hacbrewpack/cnmt.h
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** CNMT content types */
|
|
11
|
+
export enum ContentType {
|
|
12
|
+
Meta = 0,
|
|
13
|
+
Program = 1,
|
|
14
|
+
Data = 2,
|
|
15
|
+
Control = 3,
|
|
16
|
+
HtmlDocument = 4,
|
|
17
|
+
LegalInformation = 5,
|
|
18
|
+
DeltaFragment = 6,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** CNMT meta type */
|
|
22
|
+
export enum MetaType {
|
|
23
|
+
SystemProgram = 0x01,
|
|
24
|
+
SystemData = 0x02,
|
|
25
|
+
SystemUpdate = 0x03,
|
|
26
|
+
BootImagePackage = 0x04,
|
|
27
|
+
BootImagePackageSafe = 0x05,
|
|
28
|
+
Application = 0x80,
|
|
29
|
+
Patch = 0x81,
|
|
30
|
+
AddOnContent = 0x82,
|
|
31
|
+
Delta = 0x83,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** CNMT header size: 0x20 bytes */
|
|
35
|
+
const HEADER_SIZE = 0x20;
|
|
36
|
+
|
|
37
|
+
/** Extended application header size: 0x10 bytes */
|
|
38
|
+
const EXTENDED_APP_HEADER_SIZE = 0x10;
|
|
39
|
+
|
|
40
|
+
/** Content record size: 0x38 bytes */
|
|
41
|
+
const CONTENT_RECORD_SIZE = 0x38;
|
|
42
|
+
|
|
43
|
+
/** Digest size: 0x20 bytes */
|
|
44
|
+
const DIGEST_SIZE = 0x20;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A content record describing one NCA.
|
|
48
|
+
*/
|
|
49
|
+
export interface ContentRecord {
|
|
50
|
+
/** SHA-256 hash of the entire NCA file (32 bytes) */
|
|
51
|
+
hash: Uint8Array;
|
|
52
|
+
/** NCA ID: first 16 bytes of the hash */
|
|
53
|
+
ncaId: Uint8Array;
|
|
54
|
+
/** NCA file size (up to 6 bytes / 48 bits) */
|
|
55
|
+
size: number;
|
|
56
|
+
/** Content type */
|
|
57
|
+
type: ContentType;
|
|
58
|
+
/** ID offset (usually 0) */
|
|
59
|
+
idOffset?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface CnmtOptions {
|
|
63
|
+
/** Application title ID */
|
|
64
|
+
titleId: bigint;
|
|
65
|
+
/** Title version (default: 0) */
|
|
66
|
+
titleVersion?: number;
|
|
67
|
+
/** Content records for the NCAs in this package */
|
|
68
|
+
contentRecords: ContentRecord[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Build a CNMT binary blob.
|
|
73
|
+
*
|
|
74
|
+
* @param options - CNMT configuration
|
|
75
|
+
* @returns CNMT binary data as ArrayBuffer
|
|
76
|
+
*/
|
|
77
|
+
export function build(options: CnmtOptions): ArrayBuffer {
|
|
78
|
+
const { titleId, titleVersion = 0, contentRecords } = options;
|
|
79
|
+
|
|
80
|
+
// Total size = header + extended header + content records + digest
|
|
81
|
+
const totalSize =
|
|
82
|
+
HEADER_SIZE +
|
|
83
|
+
EXTENDED_APP_HEADER_SIZE +
|
|
84
|
+
contentRecords.length * CONTENT_RECORD_SIZE +
|
|
85
|
+
DIGEST_SIZE;
|
|
86
|
+
|
|
87
|
+
const buffer = new ArrayBuffer(totalSize);
|
|
88
|
+
const view = new DataView(buffer);
|
|
89
|
+
const bytes = new Uint8Array(buffer);
|
|
90
|
+
|
|
91
|
+
let offset = 0;
|
|
92
|
+
|
|
93
|
+
// --- CNMT Header (0x20 bytes) ---
|
|
94
|
+
// title_id (8 bytes)
|
|
95
|
+
view.setBigUint64(offset + 0x00, titleId, true);
|
|
96
|
+
// title_version (4 bytes)
|
|
97
|
+
view.setUint32(offset + 0x08, titleVersion, true);
|
|
98
|
+
// type (1 byte)
|
|
99
|
+
view.setUint8(offset + 0x0c, MetaType.Application);
|
|
100
|
+
// padding (1 byte) — already zero
|
|
101
|
+
// extended_header_size (2 bytes)
|
|
102
|
+
view.setUint16(offset + 0x0e, EXTENDED_APP_HEADER_SIZE, true);
|
|
103
|
+
// content_entry_count (2 bytes)
|
|
104
|
+
view.setUint16(offset + 0x10, contentRecords.length, true);
|
|
105
|
+
// meta_entry_count (2 bytes) — 0
|
|
106
|
+
// padding (remaining bytes of header) — already zero
|
|
107
|
+
|
|
108
|
+
offset += HEADER_SIZE;
|
|
109
|
+
|
|
110
|
+
// --- Extended Application Header (0x10 bytes) ---
|
|
111
|
+
// patch_title_id = title_id + 0x800
|
|
112
|
+
view.setBigUint64(offset + 0x00, titleId + 0x800n, true);
|
|
113
|
+
// required_system_version (4 bytes) — 0
|
|
114
|
+
// padding (4 bytes) — already zero
|
|
115
|
+
|
|
116
|
+
offset += EXTENDED_APP_HEADER_SIZE;
|
|
117
|
+
|
|
118
|
+
// --- Content Records (0x38 bytes each) ---
|
|
119
|
+
for (const record of contentRecords) {
|
|
120
|
+
// hash (0x20 bytes)
|
|
121
|
+
bytes.set(record.hash.subarray(0, 0x20), offset + 0x00);
|
|
122
|
+
// ncaid (0x10 bytes)
|
|
123
|
+
bytes.set(record.ncaId.subarray(0, 0x10), offset + 0x20);
|
|
124
|
+
// size (6 bytes, little-endian)
|
|
125
|
+
const size = record.size;
|
|
126
|
+
view.setUint32(offset + 0x30, size & 0xffffffff, true);
|
|
127
|
+
view.setUint16(
|
|
128
|
+
offset + 0x34,
|
|
129
|
+
Math.floor(size / 0x100000000) & 0xffff,
|
|
130
|
+
true
|
|
131
|
+
);
|
|
132
|
+
// type (1 byte)
|
|
133
|
+
view.setUint8(offset + 0x36, record.type);
|
|
134
|
+
// id_offset (1 byte)
|
|
135
|
+
view.setUint8(offset + 0x37, record.idOffset ?? 0);
|
|
136
|
+
|
|
137
|
+
offset += CONTENT_RECORD_SIZE;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// --- Digest (0x20 bytes) ---
|
|
141
|
+
// All zeros (already zeroed from ArrayBuffer allocation)
|
|
142
|
+
|
|
143
|
+
return buffer;
|
|
144
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { build, ContentType, MetaType } from '../src/index.js';
|
|
3
|
+
|
|
4
|
+
function bytesToHex(bytes: Uint8Array): string {
|
|
5
|
+
return Array.from(bytes)
|
|
6
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
7
|
+
.join('');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('CNMT builder', () => {
|
|
11
|
+
it('should build a CNMT with correct header', () => {
|
|
12
|
+
const titleId = 0x0100000000001000n;
|
|
13
|
+
const fakeHash = new Uint8Array(32);
|
|
14
|
+
for (let i = 0; i < 32; i++) fakeHash[i] = i;
|
|
15
|
+
const fakeNcaId = fakeHash.subarray(0, 16);
|
|
16
|
+
|
|
17
|
+
const cnmt = build({
|
|
18
|
+
titleId,
|
|
19
|
+
contentRecords: [
|
|
20
|
+
{
|
|
21
|
+
hash: fakeHash,
|
|
22
|
+
ncaId: fakeNcaId,
|
|
23
|
+
size: 0x100000,
|
|
24
|
+
type: ContentType.Program,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
hash: fakeHash,
|
|
28
|
+
ncaId: fakeNcaId,
|
|
29
|
+
size: 0x80000,
|
|
30
|
+
type: ContentType.Control,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const view = new DataView(cnmt);
|
|
36
|
+
const bytes = new Uint8Array(cnmt);
|
|
37
|
+
|
|
38
|
+
// Header (0x20 bytes)
|
|
39
|
+
expect(view.getBigUint64(0x00, true)).toBe(titleId);
|
|
40
|
+
expect(view.getUint32(0x08, true)).toBe(0); // title_version
|
|
41
|
+
expect(view.getUint8(0x0c)).toBe(MetaType.Application);
|
|
42
|
+
expect(view.getUint16(0x0e, true)).toBe(0x10); // extended_header_size
|
|
43
|
+
expect(view.getUint16(0x10, true)).toBe(2); // content_entry_count
|
|
44
|
+
|
|
45
|
+
// Extended header (0x10 bytes starting at 0x20)
|
|
46
|
+
expect(view.getBigUint64(0x20, true)).toBe(titleId + 0x800n); // patch_title_id
|
|
47
|
+
|
|
48
|
+
// Content record 0 (at offset 0x30, 0x38 bytes)
|
|
49
|
+
expect(view.getUint8(0x30 + 0x36)).toBe(ContentType.Program);
|
|
50
|
+
|
|
51
|
+
// Content record 1 (at offset 0x30 + 0x38 = 0x68)
|
|
52
|
+
expect(view.getUint8(0x68 + 0x36)).toBe(ContentType.Control);
|
|
53
|
+
|
|
54
|
+
// Total size = 0x20 (header) + 0x10 (ext header) + 2 * 0x38 (records) + 0x20 (digest)
|
|
55
|
+
expect(cnmt.byteLength).toBe(0x20 + 0x10 + 2 * 0x38 + 0x20);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should store NCA size as 6-byte little-endian', () => {
|
|
59
|
+
const fakeHash = new Uint8Array(32).fill(0xab);
|
|
60
|
+
const fakeNcaId = fakeHash.subarray(0, 16);
|
|
61
|
+
const size = 0x0102030405; // 5-byte value
|
|
62
|
+
|
|
63
|
+
const cnmt = build({
|
|
64
|
+
titleId: 0x0100000000001000n,
|
|
65
|
+
contentRecords: [
|
|
66
|
+
{
|
|
67
|
+
hash: fakeHash,
|
|
68
|
+
ncaId: fakeNcaId,
|
|
69
|
+
size,
|
|
70
|
+
type: ContentType.Program,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const view = new DataView(cnmt);
|
|
76
|
+
const recordOffset = 0x30; // header + ext header
|
|
77
|
+
|
|
78
|
+
// Read the 6-byte size at record + 0x30
|
|
79
|
+
const low = view.getUint32(recordOffset + 0x30, true);
|
|
80
|
+
const high = view.getUint16(recordOffset + 0x34, true);
|
|
81
|
+
const reconstructed = low + high * 0x100000000;
|
|
82
|
+
expect(reconstructed).toBe(size);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should end with a 0x20-byte zero digest', () => {
|
|
86
|
+
const fakeHash = new Uint8Array(32).fill(0xff);
|
|
87
|
+
const fakeNcaId = fakeHash.subarray(0, 16);
|
|
88
|
+
|
|
89
|
+
const cnmt = build({
|
|
90
|
+
titleId: 0x0100000000001000n,
|
|
91
|
+
contentRecords: [
|
|
92
|
+
{
|
|
93
|
+
hash: fakeHash,
|
|
94
|
+
ncaId: fakeNcaId,
|
|
95
|
+
size: 100,
|
|
96
|
+
type: ContentType.Program,
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const bytes = new Uint8Array(cnmt);
|
|
102
|
+
const digest = bytes.subarray(bytes.length - 0x20);
|
|
103
|
+
expect(bytesToHex(digest)).toBe('00'.repeat(0x20));
|
|
104
|
+
});
|
|
105
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"sourceMap": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"skipLibCheck": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*.ts"]
|
|
14
|
+
}
|