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.
- package/LICENSE +21 -0
- package/README.md +98 -3
- package/REFERENCE.md +191 -0
- package/dist/browser/index.js +345 -0
- package/dist/browser/worker.js +42480 -0
- package/example/README.md +22 -0
- package/example/Sample.cs +10 -0
- package/example/Sample.csproj +7 -0
- package/example/edit-constant.js +48 -0
- package/example/inspect.js +37 -0
- package/package.json +61 -6
- package/src/assembly/instruction-editing.js +165 -0
- package/src/assembly/method-removal.js +789 -0
- package/src/assembly/pointer-table-editing.js +506 -0
- package/src/assembly/portable-pdb-editing.js +441 -0
- package/src/binary/errors.js +17 -0
- package/src/binary/reader.js +206 -0
- package/src/browser/index.d.ts +123 -0
- package/src/browser/index.js +373 -0
- package/src/browser/worker-runtime.js +287 -0
- package/src/browser/worker.js +47 -0
- package/src/il/body.js +165 -0
- package/src/il/instructions.js +217 -0
- package/src/il/opcodes.js +102 -0
- package/src/il/rewrite.js +331 -0
- package/src/index.d.ts +267 -0
- package/src/index.js +13307 -0
- package/src/metadata/classic-pdb.js +2530 -0
- package/src/metadata/custom-attributes.js +387 -0
- package/src/metadata/portable-pdb.js +3347 -0
- package/src/metadata/resources.js +37 -0
- package/src/metadata/root-writer.js +54 -0
- package/src/metadata/signatures.js +339 -0
- package/src/metadata/streams.js +285 -0
- package/src/metadata/strong-name.js +230 -0
- package/src/metadata/tables.js +390 -0
- package/src/metadata/writer.js +191 -0
- package/src/pe/headers.js +491 -0
- package/src/platform/bytes.js +21 -0
- package/src/platform/compression.js +24 -0
- package/src/platform/crypto.js +17 -0
- package/src/resolution/module.js +64 -0
- package/src/types/assembly/instruction-editing.d.ts +33 -0
- package/src/types/assembly/method-removal.d.ts +3 -0
- package/src/types/assembly/pointer-table-editing.d.ts +3 -0
- package/src/types/assembly/portable-pdb-editing.d.ts +23 -0
- package/src/types/binary/errors.d.ts +7 -0
- package/src/types/binary/reader.d.ts +30 -0
- package/src/types/il/body.d.ts +51 -0
- package/src/types/il/instructions.d.ts +33 -0
- package/src/types/il/opcodes.d.ts +2 -0
- package/src/types/il/rewrite.d.ts +38 -0
- package/src/types/index.d.ts +864 -0
- package/src/types/metadata/classic-pdb.d.ts +688 -0
- package/src/types/metadata/custom-attributes.d.ts +28 -0
- package/src/types/metadata/portable-pdb.d.ts +296 -0
- package/src/types/metadata/resources.d.ts +1 -0
- package/src/types/metadata/root-writer.d.ts +1 -0
- package/src/types/metadata/signatures.d.ts +5 -0
- package/src/types/metadata/streams.d.ts +26 -0
- package/src/types/metadata/strong-name.d.ts +11 -0
- package/src/types/metadata/tables.d.ts +25 -0
- package/src/types/metadata/writer.d.ts +16 -0
- package/src/types/pe/headers.d.ts +160 -0
- package/src/types/platform/bytes.d.ts +3 -0
- package/src/types/platform/compression.d.ts +2 -0
- package/src/types/platform/crypto.d.ts +4 -0
- package/src/types/resolution/module.d.ts +12 -0
- package/src/types/writer/patch.d.ts +4 -0
- package/src/types/writer/pe.d.ts +9 -0
- package/src/types/writer/structural.d.ts +4 -0
- package/src/writer/patch.js +42 -0
- package/src/writer/pe.js +196 -0
- package/src/writer/structural.js +241 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { BinaryReader } from "../binary/reader.js";
|
|
2
|
+
import { FormatError } from "../binary/errors.js";
|
|
3
|
+
import { getOpcode } from "./opcodes.js";
|
|
4
|
+
|
|
5
|
+
const operandReaders = {
|
|
6
|
+
none: () => undefined,
|
|
7
|
+
u1: (reader) => reader.u1(),
|
|
8
|
+
i1: (reader) => reader.i1(),
|
|
9
|
+
u2: (reader) => reader.u2(),
|
|
10
|
+
i4: (reader) => reader.i4(),
|
|
11
|
+
i8: (reader) => reader.u8(),
|
|
12
|
+
f4: (reader) => reader.f4(),
|
|
13
|
+
f8: (reader) => reader.f8(),
|
|
14
|
+
token: (reader) => reader.u4()
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function decodeInstructions(input, { baseOffset = 0, ilOffset = 0 } = {}) {
|
|
18
|
+
const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
19
|
+
const reader = new BinaryReader(bytes, { baseOffset, context: "CIL instructions" });
|
|
20
|
+
const instructions = [];
|
|
21
|
+
while (reader.remaining > 0) {
|
|
22
|
+
const start = reader.position;
|
|
23
|
+
const first = reader.u1();
|
|
24
|
+
const code = first === 0xfe ? 0xfe00 | reader.u1() : first;
|
|
25
|
+
const opcode = getOpcode(code);
|
|
26
|
+
if (!opcode) {
|
|
27
|
+
throw new FormatError(`Unknown CIL opcode 0x${code.toString(16)}`, {
|
|
28
|
+
offset: baseOffset + start,
|
|
29
|
+
context: "CIL instructions"
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
const operandOffset = baseOffset + reader.position;
|
|
33
|
+
let operand;
|
|
34
|
+
let patchOffsets = [];
|
|
35
|
+
if (opcode.operand === "branch1" || opcode.operand === "branch4") {
|
|
36
|
+
const displacement = opcode.operand === "branch1" ? reader.i1() : reader.i4();
|
|
37
|
+
operand = { kind: "branch", targetOffset: reader.position + displacement + ilOffset };
|
|
38
|
+
patchOffsets = [operandOffset];
|
|
39
|
+
} else if (opcode.operand === "switch") {
|
|
40
|
+
const count = reader.u4();
|
|
41
|
+
if (count > Math.floor(reader.remaining / 4)) {
|
|
42
|
+
throw new FormatError("Switch target count exceeds remaining method body", {
|
|
43
|
+
offset: baseOffset + start,
|
|
44
|
+
context: "CIL switch operand"
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const offsetsStart = reader.position;
|
|
48
|
+
const displacements = Array.from({ length: count }, () => reader.i4());
|
|
49
|
+
const base = reader.position + ilOffset;
|
|
50
|
+
operand = {
|
|
51
|
+
kind: "switch",
|
|
52
|
+
targetOffsets: displacements.map((value) => base + value)
|
|
53
|
+
};
|
|
54
|
+
patchOffsets = displacements.map((_, index) => baseOffset + offsetsStart + index * 4);
|
|
55
|
+
} else {
|
|
56
|
+
const read = operandReaders[opcode.operand];
|
|
57
|
+
if (!read) throw new FormatError(`Unsupported operand kind ${opcode.operand}`, { offset: baseOffset + start });
|
|
58
|
+
const value = read(reader);
|
|
59
|
+
operand = value === undefined ? null : { kind: opcode.operand, value };
|
|
60
|
+
if (opcode.operand !== "none") patchOffsets = [operandOffset];
|
|
61
|
+
}
|
|
62
|
+
instructions.push({
|
|
63
|
+
opcode,
|
|
64
|
+
offset: start + ilOffset,
|
|
65
|
+
size: reader.position - start,
|
|
66
|
+
opcodeOffset: baseOffset + start,
|
|
67
|
+
opcodeSize: first === 0xfe ? 2 : 1,
|
|
68
|
+
operandOffset,
|
|
69
|
+
patchOffsets,
|
|
70
|
+
operand
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const boundaries = new Set(instructions.map((instruction) => instruction.offset));
|
|
74
|
+
for (const instruction of instructions) {
|
|
75
|
+
const targets = instruction.operand?.kind === "branch"
|
|
76
|
+
? [instruction.operand.targetOffset]
|
|
77
|
+
: instruction.operand?.kind === "switch"
|
|
78
|
+
? instruction.operand.targetOffsets
|
|
79
|
+
: [];
|
|
80
|
+
for (const target of targets) {
|
|
81
|
+
if (!boundaries.has(target)) {
|
|
82
|
+
throw new FormatError(`Branch target IL_${target.toString(16)} is not an instruction boundary`, {
|
|
83
|
+
offset: instruction.operandOffset,
|
|
84
|
+
context: "CIL branch operand"
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return instructions;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function encodeOpcode(opcode) {
|
|
93
|
+
if (!opcode || !Number.isInteger(opcode.code) || opcode.code < 0 || opcode.code > 0xffff) {
|
|
94
|
+
throw new TypeError("Invalid CIL opcode");
|
|
95
|
+
}
|
|
96
|
+
return opcode.code > 0xff
|
|
97
|
+
? Uint8Array.from([0xfe, opcode.code & 0xff])
|
|
98
|
+
: Uint8Array.from([opcode.code]);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function getInstructionSize(opcode, operand) {
|
|
102
|
+
const opcodeSize = opcode.code > 0xff ? 2 : 1;
|
|
103
|
+
const operandSize = {
|
|
104
|
+
none: 0,
|
|
105
|
+
u1: 1,
|
|
106
|
+
i1: 1,
|
|
107
|
+
u2: 2,
|
|
108
|
+
i4: 4,
|
|
109
|
+
i8: 8,
|
|
110
|
+
f4: 4,
|
|
111
|
+
f8: 8,
|
|
112
|
+
token: 4,
|
|
113
|
+
branch1: 1,
|
|
114
|
+
branch4: 4,
|
|
115
|
+
switch: 4 + 4 * (operand?.targetOffsets?.length ?? 0)
|
|
116
|
+
}[opcode.operand];
|
|
117
|
+
if (operandSize === undefined) throw new FormatError(`Unsupported operand kind ${opcode.operand}`);
|
|
118
|
+
return opcodeSize + operandSize;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function encodeInstruction(instruction) {
|
|
122
|
+
const opcodeBytes = encodeOpcode(instruction.opcode);
|
|
123
|
+
let operandBytes = new Uint8Array();
|
|
124
|
+
if (instruction.operand) {
|
|
125
|
+
const value = instruction.operand.kind === "branch"
|
|
126
|
+
? instruction.operand.targetOffset
|
|
127
|
+
: instruction.operand.kind === "switch"
|
|
128
|
+
? instruction.operand.targetOffsets
|
|
129
|
+
: instruction.operand.value;
|
|
130
|
+
operandBytes = encodeOperand(instruction, value);
|
|
131
|
+
if (instruction.operand.kind === "switch") {
|
|
132
|
+
const count = new Uint8Array(4);
|
|
133
|
+
new DataView(count.buffer).setUint32(0, instruction.operand.targetOffsets.length, true);
|
|
134
|
+
const combined = new Uint8Array(count.length + operandBytes.length);
|
|
135
|
+
combined.set(count);
|
|
136
|
+
combined.set(operandBytes, count.length);
|
|
137
|
+
operandBytes = combined;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (opcodeBytes.length + operandBytes.length !== instruction.size) {
|
|
141
|
+
throw new FormatError("Encoded instruction size does not match the layout");
|
|
142
|
+
}
|
|
143
|
+
const result = new Uint8Array(instruction.size);
|
|
144
|
+
result.set(opcodeBytes);
|
|
145
|
+
result.set(operandBytes, opcodeBytes.length);
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function formatInstruction(instruction) {
|
|
150
|
+
const operand = instruction.operand;
|
|
151
|
+
if (!operand) return `${instruction.opcode.name}`;
|
|
152
|
+
if (operand.kind === "branch") return `${instruction.opcode.name} IL_${operand.targetOffset.toString(16).padStart(4, "0")}`;
|
|
153
|
+
if (operand.kind === "switch") {
|
|
154
|
+
return `${instruction.opcode.name} (${operand.targetOffsets.map((target) => `IL_${target.toString(16).padStart(4, "0")}`).join(", ")})`;
|
|
155
|
+
}
|
|
156
|
+
const value = typeof operand.value === "bigint" ? `${operand.value}n` : String(operand.value);
|
|
157
|
+
return `${instruction.opcode.name} ${value}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function encodeOperand(instruction, value) {
|
|
161
|
+
const kind = instruction.operand?.kind;
|
|
162
|
+
const buffer = new ArrayBuffer(instruction.size);
|
|
163
|
+
const view = new DataView(buffer);
|
|
164
|
+
if (kind === "branch") {
|
|
165
|
+
const width = instruction.opcode.operand === "branch1" ? 1 : 4;
|
|
166
|
+
const displacement = value - (instruction.offset + instruction.size);
|
|
167
|
+
if (width === 1) {
|
|
168
|
+
if (!Number.isInteger(displacement) || displacement < -128 || displacement > 127) {
|
|
169
|
+
throw new RangeError("Branch target does not fit the existing short-branch operand");
|
|
170
|
+
}
|
|
171
|
+
view.setInt8(0, displacement);
|
|
172
|
+
return new Uint8Array(buffer, 0, 1);
|
|
173
|
+
}
|
|
174
|
+
if (!Number.isInteger(displacement) || displacement < -0x80000000 || displacement > 0x7fffffff) {
|
|
175
|
+
throw new RangeError("Branch target does not fit the existing branch operand");
|
|
176
|
+
}
|
|
177
|
+
view.setInt32(0, displacement, true);
|
|
178
|
+
return new Uint8Array(buffer, 0, 4);
|
|
179
|
+
}
|
|
180
|
+
if (kind === "switch") {
|
|
181
|
+
if (!Array.isArray(value) || value.length !== instruction.operand.targetOffsets.length) {
|
|
182
|
+
throw new RangeError("A fixed-size edit must preserve the switch target count");
|
|
183
|
+
}
|
|
184
|
+
const baseOffset = instruction.offset + instruction.size;
|
|
185
|
+
const result = new Uint8Array(value.length * 4);
|
|
186
|
+
const switchView = new DataView(result.buffer);
|
|
187
|
+
value.forEach((target, index) => {
|
|
188
|
+
const displacement = target - baseOffset;
|
|
189
|
+
if (!Number.isInteger(target) || displacement < -0x80000000 || displacement > 0x7fffffff) {
|
|
190
|
+
throw new RangeError("Switch target does not fit its 32-bit displacement");
|
|
191
|
+
}
|
|
192
|
+
switchView.setInt32(index * 4, displacement, true);
|
|
193
|
+
});
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
const operandInfo = {
|
|
197
|
+
u1: ["setUint8", 1], i1: ["setInt8", 1], u2: ["setUint16", 2], i4: ["setInt32", 4],
|
|
198
|
+
i8: ["setBigInt64", 8], f4: ["setFloat32", 4], f8: ["setFloat64", 8], token: ["setUint32", 4]
|
|
199
|
+
}[kind];
|
|
200
|
+
if (!operandInfo) throw new TypeError("Instruction does not have a patchable fixed-size operand");
|
|
201
|
+
const [setter, width] = operandInfo;
|
|
202
|
+
if (kind === "token" || kind === "u1" || kind === "u2") {
|
|
203
|
+
const max = width === 1 ? 0xff : width === 2 ? 0xffff : 0xffffffff;
|
|
204
|
+
if (!Number.isInteger(value) || value < 0 || value > max) throw new RangeError(`Value does not fit ${kind}`);
|
|
205
|
+
} else if (kind === "i1" && (!Number.isInteger(value) || value < -128 || value > 127)) {
|
|
206
|
+
throw new RangeError("Value does not fit i1");
|
|
207
|
+
} else if (kind === "i4" && (!Number.isInteger(value) || value < -0x80000000 || value > 0x7fffffff)) {
|
|
208
|
+
throw new RangeError("Value does not fit i4");
|
|
209
|
+
} else if (kind === "i8" && (typeof value !== "bigint" || value < -(1n << 63n) || value > (1n << 63n) - 1n)) {
|
|
210
|
+
throw new RangeError("Value does not fit i8");
|
|
211
|
+
} else if ((kind === "f4" || kind === "f8") && typeof value !== "number") {
|
|
212
|
+
throw new TypeError("Floating-point operands must be numbers");
|
|
213
|
+
}
|
|
214
|
+
const result = new Uint8Array(width);
|
|
215
|
+
new DataView(result.buffer)[setter](0, value, true);
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
function descriptor(code, name, operand = "none", shortBranch = null, longBranch = null) {
|
|
2
|
+
return { code, name, operand, shortBranch, longBranch };
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
const opcodes = new Map();
|
|
6
|
+
const add = (...args) => {
|
|
7
|
+
const entry = descriptor(...args);
|
|
8
|
+
opcodes.set(entry.code, entry);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
add(0x00, "nop");
|
|
12
|
+
add(0x01, "break");
|
|
13
|
+
for (let i = 0; i < 4; i++) {
|
|
14
|
+
add(0x02 + i, `ldarg.${i}`);
|
|
15
|
+
add(0x06 + i, `ldloc.${i}`);
|
|
16
|
+
add(0x0a + i, `stloc.${i}`);
|
|
17
|
+
}
|
|
18
|
+
add(0x0e, "ldarg.s", "u1");
|
|
19
|
+
add(0x0f, "ldarga.s", "u1");
|
|
20
|
+
add(0x10, "starg.s", "u1");
|
|
21
|
+
add(0x11, "ldloc.s", "u1");
|
|
22
|
+
add(0x12, "ldloca.s", "u1");
|
|
23
|
+
add(0x13, "stloc.s", "u1");
|
|
24
|
+
add(0x14, "ldnull");
|
|
25
|
+
add(0x15, "ldc.i4.m1");
|
|
26
|
+
for (let i = 0; i <= 8; i++) add(0x16 + i, `ldc.i4.${i}`);
|
|
27
|
+
add(0x1f, "ldc.i4.s", "i1");
|
|
28
|
+
add(0x20, "ldc.i4", "i4");
|
|
29
|
+
add(0x21, "ldc.i8", "i8");
|
|
30
|
+
add(0x22, "ldc.r4", "f4");
|
|
31
|
+
add(0x23, "ldc.r8", "f8");
|
|
32
|
+
add(0x25, "dup");
|
|
33
|
+
add(0x26, "pop");
|
|
34
|
+
add(0x27, "jmp", "token");
|
|
35
|
+
add(0x28, "call", "token");
|
|
36
|
+
add(0x29, "calli", "token");
|
|
37
|
+
add(0x2a, "ret");
|
|
38
|
+
|
|
39
|
+
const branchNames = ["br", "brfalse", "brtrue", "beq", "bge", "bgt", "ble", "blt", "bne.un", "bge.un", "bgt.un", "ble.un", "blt.un"];
|
|
40
|
+
for (let i = 0; i < branchNames.length; i++) {
|
|
41
|
+
add(0x2b + i, `${branchNames[i]}.s`, "branch1", null, 0x38 + i);
|
|
42
|
+
add(0x38 + i, branchNames[i], "branch4", 0x2b + i);
|
|
43
|
+
}
|
|
44
|
+
add(0x45, "switch", "switch");
|
|
45
|
+
|
|
46
|
+
const simpleNames = [
|
|
47
|
+
"ldind.i1", "ldind.u1", "ldind.i2", "ldind.u2", "ldind.i4", "ldind.u4", "ldind.i8", "ldind.i",
|
|
48
|
+
"ldind.r4", "ldind.r8", "ldind.ref", "stind.ref", "stind.i1", "stind.i2", "stind.i4", "stind.i8",
|
|
49
|
+
"stind.r4", "stind.r8", "add", "sub", "mul", "div", "div.un", "rem", "rem.un", "and", "or", "xor",
|
|
50
|
+
"shl", "shr", "shr.un", "neg", "not", "conv.i1", "conv.i2", "conv.i4", "conv.i8", "conv.r4",
|
|
51
|
+
"conv.r8", "conv.u4", "conv.u8"
|
|
52
|
+
];
|
|
53
|
+
for (let i = 0; i < simpleNames.length; i++) add(0x46 + i, simpleNames[i]);
|
|
54
|
+
|
|
55
|
+
[
|
|
56
|
+
[0x6f, "callvirt", "token"], [0x70, "cpobj", "token"], [0x71, "ldobj", "token"],
|
|
57
|
+
[0x72, "ldstr", "token"], [0x73, "newobj", "token"], [0x74, "castclass", "token"],
|
|
58
|
+
[0x75, "isinst", "token"], [0x76, "conv.r.un"], [0x79, "unbox", "token"],
|
|
59
|
+
[0x7a, "throw"], [0x7b, "ldfld", "token"], [0x7c, "ldflda", "token"],
|
|
60
|
+
[0x7d, "stfld", "token"], [0x7e, "ldsfld", "token"], [0x7f, "ldsflda", "token"],
|
|
61
|
+
[0x80, "stsfld", "token"], [0x81, "stobj", "token"], [0x8c, "box", "token"],
|
|
62
|
+
[0x8d, "newarr", "token"], [0x8e, "ldlen"], [0x8f, "ldelema", "token"],
|
|
63
|
+
[0xa3, "ldelem", "token"], [0xa4, "stelem", "token"], [0xa5, "unbox.any", "token"],
|
|
64
|
+
[0xc2, "refanyval", "token"], [0xc3, "ckfinite"], [0xc6, "mkrefany", "token"],
|
|
65
|
+
[0xd0, "ldtoken", "token"], [0xdc, "endfinally"], [0xdd, "leave", "branch4", 0xde],
|
|
66
|
+
[0xde, "leave.s", "branch1", null, 0xdd], [0xdf, "stind.i"], [0xe0, "conv.u"]
|
|
67
|
+
].forEach((item) => add(...item));
|
|
68
|
+
|
|
69
|
+
const conversionNames = [
|
|
70
|
+
"conv.ovf.i1.un", "conv.ovf.i2.un", "conv.ovf.i4.un", "conv.ovf.i8.un", "conv.ovf.u1.un",
|
|
71
|
+
"conv.ovf.u2.un", "conv.ovf.u4.un", "conv.ovf.u8.un", "conv.ovf.i.un", "conv.ovf.u.un"
|
|
72
|
+
];
|
|
73
|
+
for (let i = 0; i < conversionNames.length; i++) add(0x82 + i, conversionNames[i]);
|
|
74
|
+
for (let i = 0; i < 11; i++) add(0x90 + i, `ldelem.${["i1", "u1", "i2", "u2", "i4", "u4", "i8", "i", "r4", "r8", "ref"][i]}`);
|
|
75
|
+
for (let i = 0; i < 7; i++) add(0x9b + i, `stelem.${["i", "i1", "i2", "i4", "i8", "r4", "r8"][i]}`);
|
|
76
|
+
add(0xa2, "stelem.ref");
|
|
77
|
+
for (const [code, name] of [
|
|
78
|
+
[0xb3, "conv.ovf.i1"], [0xb4, "conv.ovf.u1"], [0xb5, "conv.ovf.i2"], [0xb6, "conv.ovf.u2"],
|
|
79
|
+
[0xb7, "conv.ovf.i4"], [0xb8, "conv.ovf.u4"], [0xb9, "conv.ovf.i8"], [0xba, "conv.ovf.u8"],
|
|
80
|
+
[0xd1, "conv.i"], [0xd2, "conv.ovf.i"],
|
|
81
|
+
[0xd3, "conv.ovf.u"], [0xd4, "add.ovf"], [0xd5, "add.ovf.un"], [0xd6, "mul.ovf"],
|
|
82
|
+
[0xd7, "mul.ovf.un"], [0xd8, "sub.ovf"], [0xd9, "sub.ovf.un"]
|
|
83
|
+
]) add(code, name);
|
|
84
|
+
|
|
85
|
+
for (const [code, name, operand] of [
|
|
86
|
+
[0xfe00, "arglist"], [0xfe01, "ceq"], [0xfe02, "cgt"], [0xfe03, "cgt.un"],
|
|
87
|
+
[0xfe04, "clt"], [0xfe05, "clt.un"], [0xfe06, "ldftn", "token"], [0xfe07, "ldvirtftn", "token"],
|
|
88
|
+
[0xfe09, "ldarg", "u2"], [0xfe0a, "ldarga", "u2"], [0xfe0b, "starg", "u2"],
|
|
89
|
+
[0xfe0c, "ldloc", "u2"], [0xfe0d, "ldloca", "u2"], [0xfe0e, "stloc", "u2"],
|
|
90
|
+
[0xfe0f, "localloc"], [0xfe11, "endfilter"], [0xfe12, "unaligned.", "u1"],
|
|
91
|
+
[0xfe13, "volatile."], [0xfe14, "tail."], [0xfe15, "initobj", "token"],
|
|
92
|
+
[0xfe16, "constrained.", "token"], [0xfe17, "cpblk"], [0xfe18, "initblk"],
|
|
93
|
+
[0xfe1a, "rethrow"], [0xfe1c, "sizeof", "token"], [0xfe1d, "refanytype"], [0xfe1e, "readonly."]
|
|
94
|
+
]) add(code, name, operand ?? "none");
|
|
95
|
+
|
|
96
|
+
export function getOpcode(code) {
|
|
97
|
+
return opcodes.get(code) ?? null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function listOpcodes() {
|
|
101
|
+
return [...opcodes.values()];
|
|
102
|
+
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { FormatError } from "../binary/errors.js";
|
|
2
|
+
import { encodeInstruction, encodeOperand, getInstructionSize } from "./instructions.js";
|
|
3
|
+
import { getOpcode } from "./opcodes.js";
|
|
4
|
+
|
|
5
|
+
function findInstruction(instructions, offset) {
|
|
6
|
+
const instruction = instructions.find((candidate) => candidate.offset === offset);
|
|
7
|
+
if (!instruction) throw new RangeError(`No instruction begins at IL_${offset.toString(16)}`);
|
|
8
|
+
return instruction;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function findBoundary(instructions, offset, codeSize, allowEnd = false) {
|
|
12
|
+
if (allowEnd && offset === codeSize) return null;
|
|
13
|
+
return findInstruction(instructions, offset);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function toClauseState(body, instructions) {
|
|
17
|
+
const clauses = [];
|
|
18
|
+
for (const section of body.sections) {
|
|
19
|
+
for (const clause of section.clauses) {
|
|
20
|
+
clauses.push({
|
|
21
|
+
...clause,
|
|
22
|
+
tryStart: findBoundary(instructions, clause.tryOffset, body.codeSize),
|
|
23
|
+
tryEnd: findBoundary(instructions, clause.tryOffset + clause.tryLength, body.codeSize, true),
|
|
24
|
+
handlerStart: findBoundary(instructions, clause.handlerOffset, body.codeSize),
|
|
25
|
+
handlerEnd: findBoundary(instructions, clause.handlerOffset + clause.handlerLength, body.codeSize, true),
|
|
26
|
+
filterStart: (clause.flags & 1) !== 0
|
|
27
|
+
? findBoundary(instructions, clause.classTokenOrFilterOffset, body.codeSize)
|
|
28
|
+
: null
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return clauses;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function updateRegionOffsets(state) {
|
|
36
|
+
const end = (instruction) => instruction?.offset ?? state.body.codeSize;
|
|
37
|
+
for (const clause of state.clauses) {
|
|
38
|
+
clause.tryOffset = clause.tryStart.offset;
|
|
39
|
+
clause.tryLength = end(clause.tryEnd) - clause.tryOffset;
|
|
40
|
+
clause.handlerOffset = clause.handlerStart.offset;
|
|
41
|
+
clause.handlerLength = end(clause.handlerEnd) - clause.handlerOffset;
|
|
42
|
+
if (clause.filterStart) clause.classTokenOrFilterOffset = clause.filterStart.offset;
|
|
43
|
+
}
|
|
44
|
+
state.body.sections = state.clauses.length > 0
|
|
45
|
+
? [{ clauses: state.clauses }]
|
|
46
|
+
: [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createStructuralState(body) {
|
|
50
|
+
const instructions = body.instructions.map((instruction) => ({
|
|
51
|
+
...instruction,
|
|
52
|
+
originalOffset: Object.hasOwn(instruction, "originalOffset")
|
|
53
|
+
? instruction.originalOffset
|
|
54
|
+
: instruction.offset,
|
|
55
|
+
operand: instruction.operand
|
|
56
|
+
? instruction.operand.kind === "switch"
|
|
57
|
+
? { kind: "switch", targetOffsets: [...instruction.operand.targetOffsets] }
|
|
58
|
+
: { ...instruction.operand }
|
|
59
|
+
: null,
|
|
60
|
+
isNew: false
|
|
61
|
+
}));
|
|
62
|
+
const byOffset = new Map(instructions.map((instruction) => [instruction.offset, instruction]));
|
|
63
|
+
const branchTargets = new Map();
|
|
64
|
+
for (const instruction of instructions) {
|
|
65
|
+
if (instruction.operand?.kind === "branch") {
|
|
66
|
+
const target = byOffset.get(instruction.operand.targetOffset);
|
|
67
|
+
if (!target) throw new FormatError("Branch target cannot be represented during structural editing");
|
|
68
|
+
branchTargets.set(instruction, target);
|
|
69
|
+
} else if (instruction.operand?.kind === "switch") {
|
|
70
|
+
const targets = instruction.operand.targetOffsets.map((offset) => {
|
|
71
|
+
const target = byOffset.get(offset);
|
|
72
|
+
if (!target) throw new FormatError("Switch target cannot be represented during structural editing");
|
|
73
|
+
return target;
|
|
74
|
+
});
|
|
75
|
+
branchTargets.set(instruction, targets);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const state = {
|
|
79
|
+
body: { ...body, instructions },
|
|
80
|
+
instructions,
|
|
81
|
+
branchTargets,
|
|
82
|
+
clauses: toClauseState(body, instructions),
|
|
83
|
+
maxStack: body.maxStack
|
|
84
|
+
};
|
|
85
|
+
layoutStructuralState(state);
|
|
86
|
+
return state;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function layoutStructuralState(state) {
|
|
90
|
+
const maxPasses = state.instructions.length + 1;
|
|
91
|
+
for (let pass = 0; pass < maxPasses; pass++) {
|
|
92
|
+
let offset = 0;
|
|
93
|
+
for (const instruction of state.instructions) {
|
|
94
|
+
instruction.offset = offset;
|
|
95
|
+
instruction.size = getInstructionSize(instruction.opcode, instruction.operand);
|
|
96
|
+
instruction.opcodeSize = instruction.opcode.code > 0xff ? 2 : 1;
|
|
97
|
+
offset += instruction.size;
|
|
98
|
+
}
|
|
99
|
+
state.body.codeSize = offset;
|
|
100
|
+
|
|
101
|
+
let promoted = false;
|
|
102
|
+
for (const instruction of state.instructions) {
|
|
103
|
+
if (instruction.operand?.kind !== "branch") continue;
|
|
104
|
+
const target = state.branchTargets.get(instruction);
|
|
105
|
+
if (!target) throw new FormatError("Branch instruction has no resolved target");
|
|
106
|
+
const displacement = target.offset - (instruction.offset + instruction.size);
|
|
107
|
+
if (instruction.opcode.operand === "branch1" && (displacement < -128 || displacement > 127)) {
|
|
108
|
+
if (instruction.opcode.longBranch === null || instruction.opcode.longBranch === undefined) {
|
|
109
|
+
throw new FormatError(`Short branch ${instruction.opcode.name} has no long form`);
|
|
110
|
+
}
|
|
111
|
+
instruction.opcode = getOpcode(instruction.opcode.longBranch);
|
|
112
|
+
promoted = true;
|
|
113
|
+
} else if (instruction.opcode.operand === "branch4" &&
|
|
114
|
+
(displacement < -0x80000000 || displacement > 0x7fffffff)) {
|
|
115
|
+
throw new RangeError("Branch target exceeds the 32-bit displacement range");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (!promoted) {
|
|
119
|
+
for (const instruction of state.instructions) {
|
|
120
|
+
if (instruction.operand?.kind === "branch") {
|
|
121
|
+
instruction.operand.targetOffset = state.branchTargets.get(instruction).offset;
|
|
122
|
+
} else if (instruction.operand?.kind === "switch") {
|
|
123
|
+
instruction.operand.targetOffsets = state.branchTargets.get(instruction).map((target) => target.offset);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
updateRegionOffsets(state);
|
|
127
|
+
return state;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
throw new FormatError("Branch layout did not converge");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function resolveTargets(state, opcode, value) {
|
|
134
|
+
if (opcode.operand === "branch1" || opcode.operand === "branch4") {
|
|
135
|
+
if (!Number.isInteger(value)) throw new TypeError("Branch operand must be an IL offset");
|
|
136
|
+
const target = state.instructions.find((instruction) => instruction.offset === value);
|
|
137
|
+
if (!target) throw new RangeError(`Branch target IL_${value.toString(16)} is not an instruction boundary`);
|
|
138
|
+
return { operand: { kind: "branch", targetOffset: value }, targets: target };
|
|
139
|
+
}
|
|
140
|
+
if (opcode.operand === "switch") {
|
|
141
|
+
if (!Array.isArray(value)) throw new TypeError("Switch operand must be an array of IL target offsets");
|
|
142
|
+
const targets = value.map((targetOffset) => {
|
|
143
|
+
const target = state.instructions.find((instruction) => instruction.offset === targetOffset);
|
|
144
|
+
if (!target) throw new RangeError(`Switch target IL_${targetOffset.toString(16)} is not an instruction boundary`);
|
|
145
|
+
return target;
|
|
146
|
+
});
|
|
147
|
+
return { operand: { kind: "switch", targetOffsets: [...value] }, targets };
|
|
148
|
+
}
|
|
149
|
+
if (opcode.operand === "none") {
|
|
150
|
+
if (value !== undefined) throw new TypeError(`${opcode.name} does not accept an operand`);
|
|
151
|
+
return { operand: null, targets: null };
|
|
152
|
+
}
|
|
153
|
+
return { operand: { kind: opcode.operand, value }, targets: null };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function makeInstruction(state, opcode, value) {
|
|
157
|
+
const { operand, targets } = resolveTargets(state, opcode, value);
|
|
158
|
+
const instruction = {
|
|
159
|
+
opcode,
|
|
160
|
+
operand,
|
|
161
|
+
offset: 0,
|
|
162
|
+
size: getInstructionSize(opcode, operand),
|
|
163
|
+
opcodeSize: opcode.code > 0xff ? 2 : 1,
|
|
164
|
+
opcodeOffset: null,
|
|
165
|
+
operandOffset: null,
|
|
166
|
+
patchOffsets: [],
|
|
167
|
+
originalOffset: null,
|
|
168
|
+
isNew: true
|
|
169
|
+
};
|
|
170
|
+
if (targets !== null) state.branchTargets.set(instruction, targets);
|
|
171
|
+
if (operand && operand.kind !== "branch" && operand.kind !== "switch") {
|
|
172
|
+
encodeOperand(instruction, operand.value);
|
|
173
|
+
}
|
|
174
|
+
return instruction;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function isExceptionBoundary(state, instruction, isEnd = false) {
|
|
178
|
+
if (isEnd && instruction === null) return state.clauses.some((clause) =>
|
|
179
|
+
clause.tryEnd === null || clause.handlerEnd === null
|
|
180
|
+
);
|
|
181
|
+
return state.clauses.some((clause) =>
|
|
182
|
+
clause.tryStart === instruction || clause.tryEnd === instruction ||
|
|
183
|
+
clause.handlerStart === instruction || clause.handlerEnd === instruction ||
|
|
184
|
+
clause.filterStart === instruction
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function insertInstruction(state, beforeOffset, opcode, value) {
|
|
189
|
+
const codeSize = state.body.codeSize;
|
|
190
|
+
let index;
|
|
191
|
+
let before;
|
|
192
|
+
if (beforeOffset === codeSize) {
|
|
193
|
+
index = state.instructions.length;
|
|
194
|
+
before = null;
|
|
195
|
+
} else {
|
|
196
|
+
before = findInstruction(state.instructions, beforeOffset);
|
|
197
|
+
index = state.instructions.indexOf(before);
|
|
198
|
+
}
|
|
199
|
+
if (isExceptionBoundary(state, before, before === null)) {
|
|
200
|
+
throw new FormatError("Insertions at exception-handler boundaries are ambiguous");
|
|
201
|
+
}
|
|
202
|
+
const instruction = makeInstruction(state, opcode, value);
|
|
203
|
+
state.instructions.splice(index, 0, instruction);
|
|
204
|
+
layoutStructuralState(state);
|
|
205
|
+
return instruction;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function removeInstruction(state, offset) {
|
|
209
|
+
const instruction = findInstruction(state.instructions, offset);
|
|
210
|
+
if (isExceptionBoundary(state, instruction)) {
|
|
211
|
+
throw new FormatError("Cannot remove an exception-handler boundary instruction");
|
|
212
|
+
}
|
|
213
|
+
for (const targets of state.branchTargets.values()) {
|
|
214
|
+
if (Array.isArray(targets) ? targets.includes(instruction) : targets === instruction) {
|
|
215
|
+
throw new FormatError("Cannot remove an instruction targeted by a branch");
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
state.instructions.splice(state.instructions.indexOf(instruction), 1);
|
|
219
|
+
state.branchTargets.delete(instruction);
|
|
220
|
+
layoutStructuralState(state);
|
|
221
|
+
return instruction;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function updateStructuralOperand(state, instruction, value) {
|
|
225
|
+
if (instruction.operand?.kind === "branch") {
|
|
226
|
+
const targets = resolveTargets(state, instruction.opcode, value);
|
|
227
|
+
state.branchTargets.set(instruction, targets.targets);
|
|
228
|
+
instruction.operand = targets.operand;
|
|
229
|
+
} else if (instruction.operand?.kind === "switch") {
|
|
230
|
+
const targets = resolveTargets(state, instruction.opcode, value);
|
|
231
|
+
state.branchTargets.set(instruction, targets.targets);
|
|
232
|
+
instruction.operand = targets.operand;
|
|
233
|
+
} else {
|
|
234
|
+
const operand = instruction.operand;
|
|
235
|
+
if (!operand) throw new TypeError("Instruction has no operand");
|
|
236
|
+
const candidate = { ...instruction, operand: { ...operand, value } };
|
|
237
|
+
encodeOperand(candidate, value);
|
|
238
|
+
instruction.operand = candidate.operand;
|
|
239
|
+
}
|
|
240
|
+
layoutStructuralState(state);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function appendU1(bytes, value) {
|
|
244
|
+
bytes.push(value & 0xff);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function appendU2(bytes, value) {
|
|
248
|
+
bytes.push(value & 0xff, (value >>> 8) & 0xff);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function appendU4(bytes, value) {
|
|
252
|
+
bytes.push(value & 0xff, (value >>> 8) & 0xff, (value >>> 16) & 0xff, (value >>> 24) & 0xff);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function alignBytes(bytes, alignment, rva = 0) {
|
|
256
|
+
while (((rva + bytes.length) & (alignment - 1)) !== 0) bytes.push(0);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function encodeExceptionSection(clauses, bodyBytes, methodRva) {
|
|
260
|
+
if (clauses.length === 0) return;
|
|
261
|
+
alignBytes(bodyBytes, 4, methodRva);
|
|
262
|
+
const small = clauses.length <= 20 && clauses.every((clause) =>
|
|
263
|
+
clause.flags <= 0xffff &&
|
|
264
|
+
clause.tryOffset <= 0xffff &&
|
|
265
|
+
clause.tryLength <= 0xff &&
|
|
266
|
+
clause.handlerOffset <= 0xffff &&
|
|
267
|
+
clause.handlerLength <= 0xff
|
|
268
|
+
);
|
|
269
|
+
const clauseSize = small ? 12 : 24;
|
|
270
|
+
const dataSize = 4 + clauses.length * clauseSize;
|
|
271
|
+
if (dataSize > 0xffffff) throw new RangeError("Exception-handler section exceeds the CLI size limit");
|
|
272
|
+
appendU1(bodyBytes, 0x01 | (small ? 0 : 0x40));
|
|
273
|
+
if (small) {
|
|
274
|
+
appendU1(bodyBytes, dataSize);
|
|
275
|
+
appendU2(bodyBytes, 0);
|
|
276
|
+
for (const clause of clauses) {
|
|
277
|
+
appendU2(bodyBytes, clause.flags);
|
|
278
|
+
appendU2(bodyBytes, clause.tryOffset);
|
|
279
|
+
appendU1(bodyBytes, clause.tryLength);
|
|
280
|
+
appendU2(bodyBytes, clause.handlerOffset);
|
|
281
|
+
appendU1(bodyBytes, clause.handlerLength);
|
|
282
|
+
appendU4(bodyBytes, clause.classTokenOrFilterOffset);
|
|
283
|
+
}
|
|
284
|
+
} else {
|
|
285
|
+
appendU1(bodyBytes, dataSize & 0xff);
|
|
286
|
+
appendU1(bodyBytes, (dataSize >>> 8) & 0xff);
|
|
287
|
+
appendU1(bodyBytes, (dataSize >>> 16) & 0xff);
|
|
288
|
+
for (const clause of clauses) {
|
|
289
|
+
appendU4(bodyBytes, clause.flags);
|
|
290
|
+
appendU4(bodyBytes, clause.tryOffset);
|
|
291
|
+
appendU4(bodyBytes, clause.tryLength);
|
|
292
|
+
appendU4(bodyBytes, clause.handlerOffset);
|
|
293
|
+
appendU4(bodyBytes, clause.handlerLength);
|
|
294
|
+
appendU4(bodyBytes, clause.classTokenOrFilterOffset);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function encodeMethodBody(state, methodRva) {
|
|
300
|
+
layoutStructuralState(state);
|
|
301
|
+
const encodedInstructions = state.instructions.map(encodeInstruction);
|
|
302
|
+
const codeSize = encodedInstructions.reduce((size, instruction) => size + instruction.length, 0);
|
|
303
|
+
const code = new Uint8Array(codeSize);
|
|
304
|
+
let codeOffset = 0;
|
|
305
|
+
for (const instruction of encodedInstructions) {
|
|
306
|
+
code.set(instruction, codeOffset);
|
|
307
|
+
codeOffset += instruction.length;
|
|
308
|
+
}
|
|
309
|
+
const clauses = state.clauses;
|
|
310
|
+
const hasSections = clauses.length > 0;
|
|
311
|
+
const initLocals = (state.body.flags & 0x10) !== 0;
|
|
312
|
+
const localVarSigToken = state.body.localVarSigToken;
|
|
313
|
+
const maxStack = Math.max(state.maxStack, hasSections ? 1 : 0);
|
|
314
|
+
const unknownFlags = state.body.flags & ~(0x03 | 0x08 | 0x10);
|
|
315
|
+
const tiny = codeSize < 64 && maxStack <= 8 && localVarSigToken === 0 &&
|
|
316
|
+
!hasSections && !initLocals && unknownFlags === 0;
|
|
317
|
+
const output = [];
|
|
318
|
+
if (tiny) {
|
|
319
|
+
appendU1(output, (codeSize << 2) | 0x02);
|
|
320
|
+
} else {
|
|
321
|
+
if (codeSize > 0xffffffff || maxStack > 0xffff) throw new RangeError("Method body exceeds CLI header limits");
|
|
322
|
+
const flagsAndSize = 0x3003 | (hasSections ? 0x08 : 0) | (initLocals ? 0x10 : 0);
|
|
323
|
+
appendU2(output, flagsAndSize);
|
|
324
|
+
appendU2(output, maxStack);
|
|
325
|
+
appendU4(output, codeSize);
|
|
326
|
+
appendU4(output, localVarSigToken);
|
|
327
|
+
}
|
|
328
|
+
for (const byte of code) output.push(byte);
|
|
329
|
+
if (hasSections) encodeExceptionSection(clauses, output, methodRva);
|
|
330
|
+
return Uint8Array.from(output);
|
|
331
|
+
}
|