jsilnet 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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
+ }