jsilnet 0.0.0-stage → 0.2.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.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +98 -3
  3. package/REFERENCE.md +191 -0
  4. package/dist/browser/index.js +345 -0
  5. package/dist/browser/worker.js +42480 -0
  6. package/example/README.md +22 -0
  7. package/example/Sample.cs +10 -0
  8. package/example/Sample.csproj +7 -0
  9. package/example/edit-constant.js +48 -0
  10. package/example/inspect.js +37 -0
  11. package/package.json +61 -6
  12. package/src/assembly/instruction-editing.js +165 -0
  13. package/src/assembly/method-removal.js +789 -0
  14. package/src/assembly/pointer-table-editing.js +506 -0
  15. package/src/assembly/portable-pdb-editing.js +441 -0
  16. package/src/binary/errors.js +17 -0
  17. package/src/binary/reader.js +206 -0
  18. package/src/browser/index.d.ts +123 -0
  19. package/src/browser/index.js +373 -0
  20. package/src/browser/worker-runtime.js +287 -0
  21. package/src/browser/worker.js +47 -0
  22. package/src/il/body.js +165 -0
  23. package/src/il/instructions.js +217 -0
  24. package/src/il/opcodes.js +102 -0
  25. package/src/il/rewrite.js +331 -0
  26. package/src/index.d.ts +267 -0
  27. package/src/index.js +13307 -0
  28. package/src/metadata/classic-pdb.js +2530 -0
  29. package/src/metadata/custom-attributes.js +387 -0
  30. package/src/metadata/portable-pdb.js +3347 -0
  31. package/src/metadata/resources.js +37 -0
  32. package/src/metadata/root-writer.js +54 -0
  33. package/src/metadata/signatures.js +339 -0
  34. package/src/metadata/streams.js +285 -0
  35. package/src/metadata/strong-name.js +230 -0
  36. package/src/metadata/tables.js +390 -0
  37. package/src/metadata/writer.js +191 -0
  38. package/src/pe/headers.js +491 -0
  39. package/src/platform/bytes.js +21 -0
  40. package/src/platform/compression.js +24 -0
  41. package/src/platform/crypto.js +17 -0
  42. package/src/resolution/module.js +64 -0
  43. package/src/types/assembly/instruction-editing.d.ts +33 -0
  44. package/src/types/assembly/method-removal.d.ts +3 -0
  45. package/src/types/assembly/pointer-table-editing.d.ts +3 -0
  46. package/src/types/assembly/portable-pdb-editing.d.ts +23 -0
  47. package/src/types/binary/errors.d.ts +7 -0
  48. package/src/types/binary/reader.d.ts +30 -0
  49. package/src/types/il/body.d.ts +51 -0
  50. package/src/types/il/instructions.d.ts +33 -0
  51. package/src/types/il/opcodes.d.ts +2 -0
  52. package/src/types/il/rewrite.d.ts +38 -0
  53. package/src/types/index.d.ts +864 -0
  54. package/src/types/metadata/classic-pdb.d.ts +688 -0
  55. package/src/types/metadata/custom-attributes.d.ts +28 -0
  56. package/src/types/metadata/portable-pdb.d.ts +296 -0
  57. package/src/types/metadata/resources.d.ts +1 -0
  58. package/src/types/metadata/root-writer.d.ts +1 -0
  59. package/src/types/metadata/signatures.d.ts +5 -0
  60. package/src/types/metadata/streams.d.ts +26 -0
  61. package/src/types/metadata/strong-name.d.ts +11 -0
  62. package/src/types/metadata/tables.d.ts +25 -0
  63. package/src/types/metadata/writer.d.ts +16 -0
  64. package/src/types/pe/headers.d.ts +160 -0
  65. package/src/types/platform/bytes.d.ts +3 -0
  66. package/src/types/platform/compression.d.ts +2 -0
  67. package/src/types/platform/crypto.d.ts +4 -0
  68. package/src/types/resolution/module.d.ts +12 -0
  69. package/src/types/writer/patch.d.ts +4 -0
  70. package/src/types/writer/pe.d.ts +9 -0
  71. package/src/types/writer/structural.d.ts +4 -0
  72. package/src/writer/patch.js +42 -0
  73. package/src/writer/pe.js +196 -0
  74. package/src/writer/structural.js +241 -0
@@ -0,0 +1,22 @@
1
+ # Runnable example
2
+
3
+ This example builds a small managed assembly, inspects it with jsILNET, writes a
4
+ byte-identical round trip, and edits an existing IL constant. The library itself
5
+ does not require .NET; the .NET SDK is only used here to create the sample DLL.
6
+
7
+ From the repository root, run:
8
+
9
+ ```sh
10
+ dotnet build example/Sample.csproj -c Release
11
+ node example/inspect.js example/bin/Release/net10.0/Sample.dll example/bin/Release/net10.0/Sample.roundtrip.dll
12
+ node example/edit-constant.js example/bin/Release/net10.0/Sample.dll Answer 43 example/bin/Release/net10.0/Sample.edited.dll
13
+ ```
14
+
15
+ `inspect.js` lists metadata and IL instructions, then optionally saves and
16
+ reparses an unchanged output. `edit-constant.js` updates the `ldc.i4.s`
17
+ instruction in the named method, saves the rewritten DLL, and reparses it to
18
+ confirm the edit survived.
19
+
20
+ To try the scripts with your own assembly, replace the sample DLL path with a
21
+ managed `.dll` that matches jsILNET's supported input profile. Both scripts
22
+ require Node.js 20 or later.
@@ -0,0 +1,10 @@
1
+ namespace JsILNET.Example;
2
+
3
+ public static class Sample
4
+ {
5
+ public static int Answer() => 42;
6
+
7
+ public static int Add(int left, int right) => left + right;
8
+
9
+ public static string Greeting() => "Hello from a managed assembly";
10
+ }
@@ -0,0 +1,7 @@
1
+ <Project Sdk="Microsoft.NET.Sdk">
2
+ <PropertyGroup>
3
+ <TargetFramework>net10.0</TargetFramework>
4
+ <ImplicitUsings>enable</ImplicitUsings>
5
+ <Nullable>enable</Nullable>
6
+ </PropertyGroup>
7
+ </Project>
@@ -0,0 +1,48 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import { parseAssembly } from "jsilnet";
4
+
5
+ const [inputPath, methodName, rawValue, outputPath, ...extraArgs] = process.argv.slice(2);
6
+ const value = Number(rawValue);
7
+
8
+ if (!inputPath || !methodName || !/^-?\d+$/.test(rawValue ?? "") ||
9
+ !Number.isInteger(value) || value < -128 || value > 127 ||
10
+ !outputPath || extraArgs.length > 0) {
11
+ console.error(
12
+ "Usage: node example/edit-constant.js <assembly.dll> <method-name> <signed-byte> <output.dll>"
13
+ );
14
+ process.exitCode = 1;
15
+ } else {
16
+ if (resolve(inputPath) === resolve(outputPath)) {
17
+ throw new Error("Choose a separate output path so the input assembly is not overwritten");
18
+ }
19
+
20
+ const assembly = parseAssembly(new Uint8Array(await readFile(inputPath)));
21
+ const matches = assembly.methods.filter((method) => method.name === methodName);
22
+ if (matches.length !== 1) {
23
+ throw new Error(`Expected one method named ${methodName}, found ${matches.length}`);
24
+ }
25
+
26
+ const method = matches[0];
27
+ const instruction = assembly
28
+ .getInstructions(method.rid)
29
+ .find((item) => item.opcode.name === "ldc.i4.s");
30
+ if (!instruction) {
31
+ throw new Error(`Method ${methodName} has no ldc.i4.s instruction to edit`);
32
+ }
33
+
34
+ assembly.setInstructionOperand(method.rid, instruction.offset, value);
35
+ const output = assembly.toBuffer();
36
+ const reparsed = parseAssembly(output);
37
+ const updatedMethod = reparsed.methods.find((item) => item.rid === method.rid);
38
+ const updatedInstruction = reparsed
39
+ .getInstructions(updatedMethod.rid)
40
+ .find((item) => item.offset === instruction.offset);
41
+ if (updatedInstruction?.operand.value !== value) {
42
+ throw new Error("Edited constant did not survive serialization and reparsing");
43
+ }
44
+
45
+ await writeFile(outputPath, output);
46
+ console.log(`Changed ${methodName}'s ldc.i4.s constant to ${value}`);
47
+ console.log(`Wrote and reparsed ${outputPath}`);
48
+ }
@@ -0,0 +1,37 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import { parseAssembly } from "jsilnet";
4
+
5
+ const [inputPath, outputPath, ...extraArgs] = process.argv.slice(2);
6
+
7
+ if (!inputPath || extraArgs.length > 0) {
8
+ console.error("Usage: node example/inspect.js <assembly.dll> [roundtrip.dll]");
9
+ process.exitCode = 1;
10
+ } else {
11
+ if (outputPath && resolve(inputPath) === resolve(outputPath)) {
12
+ throw new Error("Choose a separate output path so the input assembly is not overwritten");
13
+ }
14
+
15
+ const input = new Uint8Array(await readFile(inputPath));
16
+ const assembly = parseAssembly(input);
17
+
18
+ console.log(`Types: ${assembly.types.length}`);
19
+ console.log(`Methods: ${assembly.methods.length}`);
20
+ for (const method of assembly.methods.slice(0, 10)) {
21
+ const instructions = method.rva === 0 ? [] : assembly.getInstructions(method.rid);
22
+ console.log(`\n${method.name} (RID ${method.rid})`);
23
+ for (const instruction of instructions.slice(0, 12)) {
24
+ const offset = instruction.offset.toString(16).padStart(4, "0");
25
+ console.log(` IL_${offset}: ${instruction.opcode.name}`);
26
+ }
27
+ if (instructions.length > 12) console.log(" ...");
28
+ }
29
+
30
+ if (outputPath) {
31
+ const output = assembly.toBuffer();
32
+ parseAssembly(output);
33
+ await writeFile(outputPath, output);
34
+ console.log(`\nWrote and reparsed ${outputPath}`);
35
+ console.log(`Byte-identical round trip: ${Buffer.compare(input, output) === 0}`);
36
+ }
37
+ }
package/package.json CHANGED
@@ -1,6 +1,61 @@
1
- {
2
- "name": "jsilnet",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {
2
+ "name": "jsilnet",
3
+ "version": "0.2.0",
4
+ "description": "Node.js and browser ECMA-335 / CLI assembly reader and writer",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/Th3C0D3R/jsILNET.git"
9
+ },
10
+ "homepage": "https://github.com/Th3C0D3R/jsILNET#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/Th3C0D3R/jsILNET/issues"
13
+ },
14
+ "keywords": [
15
+ "dotnet",
16
+ "ecma-335",
17
+ "cil",
18
+ "cli",
19
+ "pe",
20
+ "assembly",
21
+ "il",
22
+ "metadata",
23
+ "nodejs"
24
+ ],
25
+ "type": "module",
26
+ "engines": {
27
+ "node": ">=20"
28
+ },
29
+ "types": "./src/index.d.ts",
30
+ "files": [
31
+ "src",
32
+ "dist/browser",
33
+ "example",
34
+ "REFERENCE.md"
35
+ ],
36
+ "exports": {
37
+ ".": "./src/index.js",
38
+ "./browser": {
39
+ "types": "./src/browser/index.d.ts",
40
+ "default": "./dist/browser/index.js"
41
+ }
42
+ },
43
+ "scripts": {
44
+ "test": "node --test",
45
+ "check": "node --check src/index.js",
46
+ "build:browser": "node scripts/build-browser.js",
47
+ "check:browser": "npm run build:browser && node scripts/check-browser-bundle.js",
48
+ "build:types": "tsc --allowJs --checkJs false --declaration --emitDeclarationOnly --module NodeNext --moduleResolution NodeNext --target ES2022 --outDir src/types src/index.js",
49
+ "precheck:types": "npm run build:types",
50
+ "check:types": "tsc --noEmit --strict --skipLibCheck --module NodeNext --moduleResolution NodeNext type-tests/core-api.ts type-tests/browser-api.ts && node scripts/check-type-coverage.js",
51
+ "prepublishOnly": "npm test && npm run check && npm run check:types && npm run check:browser"
52
+ },
53
+ "devDependencies": {
54
+ "esbuild": "^0.28.2",
55
+ "typescript": "^5.8.3"
56
+ },
57
+ "dependencies": {
58
+ "fflate": "^0.8.3",
59
+ "node-forge": "^1.4.3"
60
+ }
61
+ }
@@ -0,0 +1,165 @@
1
+ import { FormatError } from "../binary/errors.js";
2
+ import { encodeOpcode } from "../il/instructions.js";
3
+ import { listOpcodes } from "../il/opcodes.js";
4
+ import {
5
+ insertInstruction,
6
+ layoutStructuralState,
7
+ removeInstruction,
8
+ updateStructuralOperand
9
+ } from "../il/rewrite.js";
10
+ import { createOperandPatches } from "../writer/patch.js";
11
+ import { metadataTableNames } from "../metadata/tables.js";
12
+
13
+ export function createInstructionEditor(assembly, hooks) {
14
+ return {
15
+ setInstructionOperand(methodRid, instructionOffset, value) {
16
+ hooks.ensureEditable();
17
+ const body = assembly.getMethodBody(methodRid);
18
+ if (!body) throw new FormatError("Method has no CIL body");
19
+ const instruction = body.instructions.find((candidate) => candidate.offset === instructionOffset);
20
+ if (!instruction) throw new RangeError(`No instruction begins at IL_${instructionOffset.toString(16)}`);
21
+ if (!instruction.operand) throw new TypeError("Instruction has no operand");
22
+
23
+ if (instruction.operand.kind === "branch") {
24
+ hooks.validateBranchTarget(body, value);
25
+ } else if (instruction.operand.kind === "switch") {
26
+ if (!Array.isArray(value)) throw new TypeError("Switch operand must be an array of IL target offsets");
27
+ for (const target of value) hooks.validateBranchTarget(body, target);
28
+ } else if (instruction.opcode.name === "ldstr") {
29
+ if (!Number.isInteger(value) || (value >>> 24) !== 0x70) {
30
+ throw new TypeError("ldstr operand must be a 0x70-prefixed user-string token");
31
+ }
32
+ assembly.heaps.getUserString(value);
33
+ } else if (instruction.operand.kind === "token") {
34
+ if (!Number.isInteger(value) || value < 0 || value > 0xffffffff) {
35
+ throw new RangeError("Metadata token must be an unsigned 32-bit integer");
36
+ }
37
+ const table = value >>> 24;
38
+ const rid = value & 0x00ffffff;
39
+ if (!hooks.allowedTokenTables(instruction.opcode.name).includes(table)) {
40
+ throw new FormatError(`Metadata table ${table} is not valid for ${instruction.opcode.name}`);
41
+ }
42
+ if (rid === 0 || rid > (assembly.tables.counts[table] ?? 0)) {
43
+ throw new RangeError(
44
+ `${instruction.opcode.name} references missing ${metadataTableNames[table]} row ${rid}`
45
+ );
46
+ }
47
+ hooks.validateInstructionGenericContext(assembly, methodRid, instruction, value);
48
+ }
49
+
50
+ if (instruction.operand.kind === "branch" && instruction.operand.targetOffset === value) return assembly;
51
+ if (instruction.operand.kind === "switch" &&
52
+ instruction.operand.targetOffsets.length === value.length &&
53
+ instruction.operand.targetOffsets.every((target, index) => target === value[index])) return assembly;
54
+ if (instruction.operand.kind !== "branch" && instruction.operand.kind !== "switch" &&
55
+ instruction.operand.value === value) return assembly;
56
+
57
+ if (assembly._structuralBodies.has(methodRid) || assembly._addedMethodBodies.has(methodRid)) {
58
+ const state = hooks.createStructuralState(methodRid);
59
+ const editableInstruction = state.instructions.find((candidate) => candidate.offset === instructionOffset);
60
+ updateStructuralOperand(state, editableInstruction, value);
61
+ hooks.commitStructuralState(methodRid, state);
62
+ } else {
63
+ const patches = createOperandPatches(instruction, value);
64
+ for (const patch of patches) assembly._patches.push(patch);
65
+ instruction.operand = instruction.operand.kind === "branch"
66
+ ? { kind: "branch", targetOffset: value }
67
+ : instruction.operand.kind === "switch"
68
+ ? { kind: "switch", targetOffsets: [...value] }
69
+ : { ...instruction.operand, value };
70
+ }
71
+ return assembly;
72
+ },
73
+
74
+ setInstructionOpcode(methodRid, instructionOffset, opcodeNameOrCode) {
75
+ hooks.ensureEditable();
76
+ const body = assembly.getMethodBody(methodRid);
77
+ if (!body) throw new FormatError("Method has no CIL body");
78
+ const instruction = body.instructions.find((candidate) => candidate.offset === instructionOffset);
79
+ if (!instruction) throw new RangeError(`No instruction begins at IL_${instructionOffset.toString(16)}`);
80
+ const opcode = listOpcodes().find((candidate) =>
81
+ candidate.name === opcodeNameOrCode || candidate.code === opcodeNameOrCode
82
+ );
83
+ if (!opcode) throw new RangeError(`Unknown CIL opcode ${opcodeNameOrCode}`);
84
+ const encoded = encodeOpcode(opcode);
85
+ if (encoded.length !== instruction.opcodeSize || opcode.operand !== instruction.opcode.operand) {
86
+ throw new FormatError("Opcode edit must preserve the opcode and operand widths");
87
+ }
88
+ if (opcode.code === instruction.opcode.code) return assembly;
89
+ if (instruction.operand?.kind === "token") {
90
+ if (opcode.name !== "ldstr") {
91
+ const table = instruction.operand.value >>> 24;
92
+ const rid = instruction.operand.value & 0x00ffffff;
93
+ if (!hooks.allowedTokenTables(opcode.name).includes(table)) {
94
+ throw new FormatError(`Metadata table ${table} is not valid for ${opcode.name}`);
95
+ }
96
+ if (rid === 0 || rid > (assembly.tables.counts[table] ?? 0)) {
97
+ throw new RangeError(
98
+ `${opcode.name} references missing ${metadataTableNames[table]} row ${rid}`
99
+ );
100
+ }
101
+ hooks.validateInstructionGenericContext(
102
+ assembly, methodRid, { ...instruction, opcode }, instruction.operand.value
103
+ );
104
+ }
105
+ }
106
+ if (assembly._structuralBodies.has(methodRid) || assembly._addedMethodBodies.has(methodRid)) {
107
+ const state = hooks.createStructuralState(methodRid);
108
+ const editableInstruction = state.instructions.find((candidate) => candidate.offset === instructionOffset);
109
+ editableInstruction.opcode = opcode;
110
+ layoutStructuralState(state);
111
+ hooks.commitStructuralState(methodRid, state);
112
+ } else {
113
+ assembly._patches.push({ offset: instruction.opcodeOffset, bytes: encoded });
114
+ instruction.opcode = opcode;
115
+ }
116
+ return assembly;
117
+ },
118
+
119
+ insertInstruction(methodRid, beforeOffset, opcodeNameOrCode, operand) {
120
+ hooks.ensureEditable();
121
+ const opcode = listOpcodes().find((candidate) =>
122
+ candidate.name === opcodeNameOrCode || candidate.code === opcodeNameOrCode
123
+ );
124
+ if (!opcode) throw new RangeError(`Unknown CIL opcode ${opcodeNameOrCode}`);
125
+ const state = hooks.createStructuralState(methodRid);
126
+ const instruction = insertInstruction(state, beforeOffset, opcode, operand);
127
+ if (instruction.operand?.kind === "token" && opcode.name !== "ldstr") {
128
+ const token = instruction.operand.value;
129
+ const table = token >>> 24;
130
+ const rid = token & 0x00ffffff;
131
+ if (!hooks.allowedTokenTables(opcode.name).includes(table)) {
132
+ throw new FormatError(`Metadata table ${table} is not valid for ${opcode.name}`);
133
+ }
134
+ if (rid === 0 || rid > (assembly.tables.counts[table] ?? 0)) {
135
+ throw new RangeError(
136
+ `${opcode.name} references missing ${metadataTableNames[table]} row ${rid}`
137
+ );
138
+ }
139
+ hooks.validateInstructionGenericContext(assembly, methodRid, instruction, token);
140
+ }
141
+ hooks.commitStructuralState(methodRid, state);
142
+ return instruction;
143
+ },
144
+
145
+ removeInstruction(methodRid, instructionOffset) {
146
+ hooks.ensureEditable();
147
+ const state = hooks.createStructuralState(methodRid);
148
+ const instruction = removeInstruction(state, instructionOffset);
149
+ hooks.commitStructuralState(methodRid, state);
150
+ return instruction;
151
+ },
152
+
153
+ setMethodMaxStack(methodRid, maxStack) {
154
+ hooks.ensureEditable();
155
+ if (!Number.isInteger(maxStack) || maxStack < 0 || maxStack > 0xffff) {
156
+ throw new RangeError("maxStack must be an unsigned 16-bit integer");
157
+ }
158
+ const state = hooks.createStructuralState(methodRid);
159
+ state.maxStack = maxStack;
160
+ layoutStructuralState(state);
161
+ hooks.commitStructuralState(methodRid, state);
162
+ return assembly;
163
+ }
164
+ };
165
+ }