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,491 @@
1
+ import { inflateRawSync } from "../platform/compression.js";
2
+ import { BinaryReader } from "../binary/reader.js";
3
+ import { FormatError, UnsupportedFormatError } from "../binary/errors.js";
4
+ import { MetadataHeaps, parseMetadataRoot } from "../metadata/streams.js";
5
+ import { parseTablesStream } from "../metadata/tables.js";
6
+ import { parsePortablePdbTables } from "../metadata/portable-pdb.js";
7
+
8
+ const maxEmbeddedPortablePdbSize = 0x10000000;
9
+
10
+ function requireSignature(actual, expected, offset, name) {
11
+ if (actual !== expected) throw new FormatError(`Invalid ${name} signature`, { offset });
12
+ }
13
+
14
+ function isPowerOfTwo(value) {
15
+ return value !== 0 && (value & (value - 1)) === 0;
16
+ }
17
+
18
+ function parseCodeViewData(bytes, offset, size) {
19
+ if (size < 4) {
20
+ throw new FormatError("CodeView debug record is shorter than its signature", {
21
+ offset,
22
+ context: "PE CodeView data"
23
+ });
24
+ }
25
+ const signature = new TextDecoder("ascii").decode(bytes.subarray(offset, offset + 4));
26
+ const view = new DataView(bytes.buffer, bytes.byteOffset + offset, size);
27
+ let format;
28
+ let pathOffset;
29
+ let identity;
30
+ if (signature === "RSDS") {
31
+ if (size < 25) {
32
+ throw new FormatError("RSDS CodeView debug record is truncated", {
33
+ offset,
34
+ context: "PE CodeView data"
35
+ });
36
+ }
37
+ const data1 = view.getUint32(4, true).toString(16).padStart(8, "0");
38
+ const data2 = view.getUint16(8, true).toString(16).padStart(4, "0");
39
+ const data3 = view.getUint16(10, true).toString(16).padStart(4, "0");
40
+ const data4 = [...bytes.subarray(offset + 12, offset + 20)]
41
+ .map((byte) => byte.toString(16).padStart(2, "0"))
42
+ .join("");
43
+ identity = {
44
+ guid: `${data1}-${data2}-${data3}-${data4.slice(0, 4)}-${data4.slice(4)}`,
45
+ age: view.getUint32(20, true)
46
+ };
47
+ format = "RSDS";
48
+ pathOffset = 24;
49
+ } else if (signature === "NB10") {
50
+ if (size < 17) {
51
+ throw new FormatError("NB10 CodeView debug record is truncated", {
52
+ offset,
53
+ context: "PE CodeView data"
54
+ });
55
+ }
56
+ identity = {
57
+ offset: view.getUint32(4, true),
58
+ timeDateStamp: view.getUint32(8, true),
59
+ age: view.getUint32(12, true)
60
+ };
61
+ format = "NB10";
62
+ pathOffset = 16;
63
+ } else {
64
+ return { format: "unknown", signature };
65
+ }
66
+
67
+ const pathBytes = bytes.subarray(offset + pathOffset, offset + size);
68
+ const terminator = pathBytes.indexOf(0);
69
+ if (terminator < 0 || pathBytes.subarray(terminator + 1).some((byte) => byte !== 0)) {
70
+ throw new FormatError("CodeView PDB path is not null-terminated or has nonzero trailing data", {
71
+ offset: offset + pathOffset,
72
+ context: "PE CodeView data"
73
+ });
74
+ }
75
+ return {
76
+ format,
77
+ ...identity,
78
+ path: new TextDecoder("utf-8", { ignoreBOM: true }).decode(pathBytes.subarray(0, terminator)),
79
+ pathOffset,
80
+ pathByteLength: terminator
81
+ };
82
+ }
83
+
84
+ function parsePortablePdbStream(metadataRoot) {
85
+ const stream = metadataRoot.streams.get("#Pdb");
86
+ if (!stream) throw new FormatError("Embedded Portable PDB is missing its #Pdb stream");
87
+ if (!metadataRoot.streams.has("#~")) {
88
+ throw new FormatError("Embedded Portable PDB is missing its #~ tables stream");
89
+ }
90
+ const reader = new BinaryReader(stream.bytes, { context: "Portable PDB #Pdb stream" });
91
+ if (reader.remaining < 32) throw new FormatError("Portable PDB #Pdb stream is truncated");
92
+ const id = Uint8Array.from(reader.take(20));
93
+ const entryPointToken = reader.u4();
94
+ if (entryPointToken !== 0 &&
95
+ ((entryPointToken >>> 24) !== 6 || (entryPointToken & 0x00ffffff) === 0)) {
96
+ throw new FormatError("Portable PDB entry point must be a MethodDef token or nil");
97
+ }
98
+ const referencedTypeSystemTables = BigInt(reader.u4()) | (BigInt(reader.u4()) << 32n);
99
+ const rowCounts = new Map();
100
+ for (let table = 0; table < 64; table++) {
101
+ if ((referencedTypeSystemTables & (1n << BigInt(table))) !== 0n) {
102
+ if (table >= 45) {
103
+ throw new FormatError("Portable PDB #Pdb references a non-type-system metadata table");
104
+ }
105
+ rowCounts.set(table, reader.u4());
106
+ }
107
+ }
108
+ if (reader.remaining !== 0) {
109
+ throw new FormatError("Trailing bytes after Portable PDB #Pdb stream data");
110
+ }
111
+ return { id, entryPointToken, referencedTypeSystemTables, rowCounts };
112
+ }
113
+
114
+ export function parseEmbeddedPortablePdbData(bytes, offset, size) {
115
+ const reader = new BinaryReader(bytes, {
116
+ start: offset,
117
+ length: size,
118
+ baseOffset: offset,
119
+ context: "embedded Portable PDB"
120
+ });
121
+ if (reader.remaining < 9 || new TextDecoder("ascii").decode(reader.take(4)) !== "MPDB") {
122
+ throw new FormatError("Embedded Portable PDB has an invalid or truncated MPDB header", {
123
+ offset,
124
+ context: "embedded Portable PDB"
125
+ });
126
+ }
127
+ const uncompressedSize = reader.u4();
128
+ if (uncompressedSize === 0) {
129
+ throw new FormatError("Embedded Portable PDB declares an empty metadata root", {
130
+ offset: offset + 4,
131
+ context: "embedded Portable PDB"
132
+ });
133
+ }
134
+ if (uncompressedSize > maxEmbeddedPortablePdbSize) {
135
+ throw new UnsupportedFormatError(
136
+ `Embedded Portable PDB exceeds the ${maxEmbeddedPortablePdbSize}-byte decompression limit`,
137
+ { offset: offset + 4, context: "embedded Portable PDB" }
138
+ );
139
+ }
140
+ let metadataBytes;
141
+ try {
142
+ metadataBytes = inflateRawSync(reader.take(reader.remaining), {
143
+ maxOutputLength: uncompressedSize
144
+ });
145
+ } catch (error) {
146
+ throw new FormatError(`Embedded Portable PDB deflate stream is invalid: ${error.message}`, {
147
+ offset: reader.absoluteOffset,
148
+ context: "embedded Portable PDB"
149
+ });
150
+ }
151
+ if (metadataBytes.length !== uncompressedSize) {
152
+ throw new FormatError("Embedded Portable PDB decompressed size does not match its header", {
153
+ offset,
154
+ context: "embedded Portable PDB"
155
+ });
156
+ }
157
+ const metadataRoot = parseMetadataRoot(metadataBytes, 0, metadataBytes.length);
158
+ const pdb = parsePortablePdbStream(metadataRoot);
159
+ const externalCounts = new Array(64).fill(0);
160
+ for (const [table, count] of pdb.rowCounts) externalCounts[table] = count;
161
+ const tables = parseTablesStream(metadataRoot.streams.get("#~"), externalCounts);
162
+ if ((tables.valid & ((1n << 45n) - 1n)) !== 0n) {
163
+ throw new FormatError("Portable PDB #~ stream contains type-system metadata tables");
164
+ }
165
+ const heaps = new MetadataHeaps(metadataRoot.streams);
166
+ const decoded = parsePortablePdbTables(tables, heaps, externalCounts);
167
+ return {
168
+ uncompressedSize,
169
+ bytes: Uint8Array.from(metadataBytes),
170
+ metadataRoot,
171
+ tables,
172
+ heaps,
173
+ ...decoded,
174
+ ...pdb
175
+ };
176
+ }
177
+
178
+ export function parsePE(input) {
179
+ const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
180
+ const reader = new BinaryReader(bytes, { context: "PE headers" });
181
+ requireSignature(reader.u2(), 0x5a4d, 0, "DOS");
182
+ reader.seek(0x3c);
183
+ const peOffset = reader.u4();
184
+ if (peOffset > bytes.length - 24) {
185
+ throw new FormatError("PE header offset is outside the file", { offset: 0x3c, context: "DOS header" });
186
+ }
187
+
188
+ reader.seek(peOffset);
189
+ requireSignature(reader.u4(), 0x00004550, peOffset, "PE");
190
+ const machine = reader.u2();
191
+ const sectionCount = reader.u2();
192
+ reader.u4();
193
+ const symbolTablePointer = reader.u4();
194
+ const symbolCount = reader.u4();
195
+ const optionalHeaderSize = reader.u2();
196
+ const characteristics = reader.u2();
197
+ const optionalOffset = reader.position;
198
+ const optional = reader.slice(optionalHeaderSize, "PE optional header");
199
+ const magic = optional.u2();
200
+ let directoryCountOffset;
201
+ let directoryOffset;
202
+ if (magic === 0x10b) {
203
+ directoryCountOffset = 92;
204
+ directoryOffset = 96;
205
+ } else if (magic === 0x20b) {
206
+ directoryCountOffset = 108;
207
+ directoryOffset = 112;
208
+ } else {
209
+ throw new UnsupportedFormatError(`Unsupported PE optional-header magic 0x${magic.toString(16)}`, {
210
+ offset: optionalOffset
211
+ });
212
+ }
213
+ if (optionalHeaderSize < directoryCountOffset + 4) {
214
+ throw new FormatError("Optional header is too short to contain data directories", { offset: optionalOffset });
215
+ }
216
+ optional.seek(directoryCountOffset);
217
+ const declaredDirectories = optional.u4();
218
+ const availableDirectories = Math.floor((optionalHeaderSize - directoryOffset) / 8);
219
+ if (declaredDirectories > availableDirectories) {
220
+ throw new FormatError("Optional header declares data directories that do not fit in the header", {
221
+ offset: optionalOffset + directoryCountOffset,
222
+ context: "PE optional header"
223
+ });
224
+ }
225
+ const directoryCount = declaredDirectories;
226
+ optional.seek(directoryOffset);
227
+ const directories = Array.from({ length: directoryCount }, () => ({
228
+ rva: optional.u4(),
229
+ size: optional.u4()
230
+ }));
231
+ const sectionOffset = optionalOffset + optionalHeaderSize;
232
+ const sectionsReader = new BinaryReader(bytes, {
233
+ start: sectionOffset,
234
+ length: bytes.length - sectionOffset,
235
+ baseOffset: sectionOffset,
236
+ context: "PE section table"
237
+ });
238
+ const sections = [];
239
+ for (let i = 0; i < sectionCount; i++) {
240
+ const nameBytes = sectionsReader.take(8);
241
+ const nul = nameBytes.indexOf(0);
242
+ const name = new TextDecoder().decode(nul < 0 ? nameBytes : nameBytes.subarray(0, nul));
243
+ const virtualSize = sectionsReader.u4();
244
+ const virtualAddress = sectionsReader.u4();
245
+ const rawSize = sectionsReader.u4();
246
+ const rawPointer = sectionsReader.u4();
247
+ sectionsReader.skip(16);
248
+ sections.push({ name, virtualSize, virtualAddress, rawSize, rawPointer });
249
+ }
250
+ optional.seek(4);
251
+ const sizeOfCode = optional.u4();
252
+ const sizeOfInitializedData = optional.u4();
253
+ optional.seek(32);
254
+ const sectionAlignment = optional.u4();
255
+ const fileAlignment = optional.u4();
256
+ if (!isPowerOfTwo(sectionAlignment) || !isPowerOfTwo(fileAlignment)) {
257
+ throw new FormatError("PE section and file alignments must be nonzero powers of two", {
258
+ offset: optionalOffset + 32,
259
+ context: "PE optional header"
260
+ });
261
+ }
262
+ if ((sectionAlignment < 0x1000 && sectionAlignment !== fileAlignment) ||
263
+ (sectionAlignment >= 0x1000 && sectionAlignment < fileAlignment)) {
264
+ throw new FormatError("PE section and file alignments are inconsistent", {
265
+ offset: optionalOffset + 32,
266
+ context: "PE optional header"
267
+ });
268
+ }
269
+ if (sectionAlignment >= 0x1000 && (fileAlignment < 0x200 || fileAlignment > 0x10000)) {
270
+ throw new FormatError("PE file alignment is outside the supported range", {
271
+ offset: optionalOffset + 36,
272
+ context: "PE optional header"
273
+ });
274
+ }
275
+ optional.seek(56);
276
+ const sizeOfImage = optional.u4();
277
+ const sizeOfHeaders = optional.u4();
278
+ const checksumOffset = optionalOffset + 64;
279
+ const sectionTableEnd = sectionOffset + sectionCount * 40;
280
+ if (sizeOfHeaders < sectionTableEnd || sizeOfHeaders > bytes.length) {
281
+ throw new FormatError("PE SizeOfHeaders does not cover the PE headers", {
282
+ offset: optionalOffset + 60,
283
+ context: "PE optional header"
284
+ });
285
+ }
286
+ if (sizeOfHeaders % fileAlignment !== 0 || sizeOfImage === 0 ||
287
+ sizeOfImage % sectionAlignment !== 0) {
288
+ throw new FormatError("PE header and image sizes do not follow the declared alignments", {
289
+ offset: optionalOffset + 56,
290
+ context: "PE optional header"
291
+ });
292
+ }
293
+
294
+ const rawRanges = [];
295
+ const virtualRanges = [];
296
+ const firstSectionAddress = Math.ceil(sizeOfHeaders / sectionAlignment) * sectionAlignment;
297
+ let virtualImageEnd = sizeOfHeaders;
298
+ for (const section of sections) {
299
+ if (section.virtualAddress % sectionAlignment !== 0) {
300
+ throw new FormatError(`Section ${section.name} virtual address is not section-aligned`);
301
+ }
302
+ const virtualSize = Math.max(section.virtualSize, section.rawSize);
303
+ if (virtualSize > 0) {
304
+ if (section.virtualAddress < firstSectionAddress) {
305
+ throw new FormatError(`Section ${section.name} virtual range overlaps the PE headers`);
306
+ }
307
+ const end = section.virtualAddress + virtualSize;
308
+ if (end > 0x100000000) {
309
+ throw new FormatError(`Section ${section.name} virtual range overflows the RVA space`);
310
+ }
311
+ virtualImageEnd = Math.max(virtualImageEnd, end);
312
+ virtualRanges.push({ start: section.virtualAddress, end, name: section.name });
313
+ }
314
+ if (section.rawSize > 0) {
315
+ if (section.rawPointer % fileAlignment !== 0 || section.rawSize % fileAlignment !== 0) {
316
+ throw new FormatError(`Section ${section.name} raw range is not file-aligned`);
317
+ }
318
+ const end = section.rawPointer + section.rawSize;
319
+ if (section.rawPointer < sizeOfHeaders || end > bytes.length) {
320
+ throw new FormatError(`Section ${section.name} raw range is outside file data`);
321
+ }
322
+ rawRanges.push({ start: section.rawPointer, end, name: section.name });
323
+ }
324
+ }
325
+ const minimumImageSize = Math.ceil(virtualImageEnd / sectionAlignment) * sectionAlignment;
326
+ if (sizeOfImage < minimumImageSize) {
327
+ throw new FormatError("PE SizeOfImage does not cover the aligned headers and section ranges", {
328
+ offset: optionalOffset + 56,
329
+ context: "PE optional header"
330
+ });
331
+ }
332
+ for (const [ranges, label] of [[rawRanges, "raw"], [virtualRanges, "virtual"]]) {
333
+ ranges.sort((left, right) => left.start - right.start || left.end - right.end);
334
+ for (let index = 1; index < ranges.length; index++) {
335
+ if (ranges[index - 1].end > ranges[index].start) {
336
+ throw new FormatError(`PE sections ${ranges[index - 1].name} and ${ranges[index].name} have overlapping ${label} ranges`);
337
+ }
338
+ }
339
+ }
340
+
341
+ function rvaToOffset(rva, size = 1) {
342
+ if (!Number.isInteger(rva) || !Number.isInteger(size) || rva < 0 || size < 0) {
343
+ throw new FormatError("Invalid RVA range", { context: "RVA mapping" });
344
+ }
345
+ if (rva === 0 && size === 0) return 0;
346
+ for (const section of sections) {
347
+ const span = Math.max(section.virtualSize, section.rawSize);
348
+ if (rva < section.virtualAddress || rva - section.virtualAddress >= span) continue;
349
+ const delta = rva - section.virtualAddress;
350
+ if (delta + size > section.rawSize || section.rawPointer + delta + size > bytes.length) {
351
+ throw new FormatError("RVA range is not backed by file data", {
352
+ offset: section.rawPointer + delta,
353
+ context: `section ${section.name}`
354
+ });
355
+ }
356
+ return section.rawPointer + delta;
357
+ }
358
+ if (rva < sectionOffset && rva + size <= bytes.length) return rva;
359
+ throw new FormatError(`RVA 0x${rva.toString(16)} is not mapped by a PE section`, { context: "RVA mapping" });
360
+ }
361
+
362
+ const debugDirectory = directories[6] ?? { rva: 0, size: 0 };
363
+ if ((debugDirectory.rva === 0) !== (debugDirectory.size === 0)) {
364
+ throw new FormatError("PE debug directory must have both an RVA and a size", {
365
+ context: "PE debug directory"
366
+ });
367
+ }
368
+ const debugDirectoryEntries = [];
369
+ if (debugDirectory.size > 0) {
370
+ if (debugDirectory.size % 28 !== 0) {
371
+ throw new FormatError("Debug directory size is not a multiple of IMAGE_DEBUG_DIRECTORY", {
372
+ context: "PE debug directory"
373
+ });
374
+ }
375
+ const debugOffset = rvaToOffset(debugDirectory.rva, debugDirectory.size);
376
+ const debugReader = new BinaryReader(bytes, {
377
+ start: debugOffset,
378
+ length: debugDirectory.size,
379
+ baseOffset: debugOffset,
380
+ context: "PE debug directory"
381
+ });
382
+ while (debugReader.remaining > 0) {
383
+ const entryOffset = debugReader.position;
384
+ const characteristics = debugReader.u4();
385
+ const timeDateStamp = debugReader.u4();
386
+ const majorVersion = debugReader.u2();
387
+ const minorVersion = debugReader.u2();
388
+ const type = debugReader.u4();
389
+ const sizeOfData = debugReader.u4();
390
+ const addressOfRawData = debugReader.u4();
391
+ const pointerToRawData = debugReader.u4();
392
+ if (sizeOfData > 0) {
393
+ if (addressOfRawData !== 0) rvaToOffset(addressOfRawData, sizeOfData);
394
+ if (pointerToRawData !== 0 && pointerToRawData + sizeOfData > bytes.length) {
395
+ throw new FormatError("Debug data raw range is outside file data", {
396
+ offset: pointerToRawData,
397
+ context: "PE debug directory"
398
+ });
399
+ }
400
+ if (addressOfRawData === 0 && pointerToRawData === 0) {
401
+ throw new FormatError("Debug entry has data size but no data location", {
402
+ offset: entryOffset,
403
+ context: "PE debug directory"
404
+ });
405
+ }
406
+ }
407
+ const debugDataOffset = pointerToRawData !== 0
408
+ ? pointerToRawData
409
+ : sizeOfData > 0
410
+ ? rvaToOffset(addressOfRawData, sizeOfData)
411
+ : 0;
412
+ const codeView = type === 2 && sizeOfData > 0
413
+ ? parseCodeViewData(bytes, debugDataOffset, sizeOfData)
414
+ : null;
415
+ const embeddedPortablePdb = type === 17 && sizeOfData > 0
416
+ ? parseEmbeddedPortablePdbData(bytes, debugDataOffset, sizeOfData)
417
+ : null;
418
+ debugDirectoryEntries.push({
419
+ characteristics,
420
+ timeDateStamp,
421
+ majorVersion,
422
+ minorVersion,
423
+ type,
424
+ sizeOfData,
425
+ addressOfRawData,
426
+ pointerToRawData,
427
+ codeView,
428
+ embeddedPortablePdb
429
+ });
430
+ }
431
+ }
432
+
433
+ return {
434
+ bytes,
435
+ peOffset,
436
+ machine,
437
+ sectionCount,
438
+ symbolTablePointer,
439
+ symbolCount,
440
+ characteristics,
441
+ optionalHeaderOffset: optionalOffset,
442
+ optionalHeaderSize,
443
+ optionalHeaderMagic: magic,
444
+ sectionOffset,
445
+ sizeOfCode,
446
+ sizeOfInitializedData,
447
+ sectionAlignment,
448
+ fileAlignment,
449
+ sizeOfImage,
450
+ sizeOfHeaders,
451
+ checksumOffset,
452
+ directories,
453
+ debugDirectoryEntries,
454
+ sections,
455
+ rvaToOffset
456
+ };
457
+ }
458
+
459
+ export function parseCliHeader(pe) {
460
+ const directory = pe.directories[14];
461
+ if (!directory || directory.rva === 0 || directory.size < 72) {
462
+ throw new FormatError("PE does not contain a complete CLI data directory", { context: "CLI header" });
463
+ }
464
+ const offset = pe.rvaToOffset(directory.rva, Math.min(directory.size, 72));
465
+ const reader = new BinaryReader(pe.bytes, { start: offset, length: 72, baseOffset: offset, context: "CLI header" });
466
+ const headerSize = reader.u4();
467
+ if (headerSize < 72) throw new FormatError("CLI header is smaller than 72 bytes", { offset, context: "CLI header" });
468
+ if (headerSize > directory.size) throw new FormatError("CLI header exceeds its data directory", { offset, context: "CLI header" });
469
+ const majorRuntimeVersion = reader.u2();
470
+ const minorRuntimeVersion = reader.u2();
471
+ const metadataRva = reader.u4();
472
+ const metadataSize = reader.u4();
473
+ const flags = reader.u4();
474
+ const entryPointToken = reader.u4();
475
+ const resources = { rva: reader.u4(), size: reader.u4() };
476
+ const strongNameSignature = { rva: reader.u4(), size: reader.u4() };
477
+ return {
478
+ offset,
479
+ headerSize,
480
+ majorRuntimeVersion,
481
+ minorRuntimeVersion,
482
+ metadataRva,
483
+ metadataSize,
484
+ flags,
485
+ entryPointToken,
486
+ resources,
487
+ strongNameSignature,
488
+ strongNameFlagOffset: offset + 16,
489
+ strongNameDirectoryOffset: offset + 32
490
+ };
491
+ }
@@ -0,0 +1,21 @@
1
+ export function bytesToBinaryString(bytes) {
2
+ const chunks = [];
3
+ const chunkSize = 0x8000;
4
+ for (let offset = 0; offset < bytes.length; offset += chunkSize) {
5
+ chunks.push(String.fromCharCode(...bytes.subarray(offset, offset + chunkSize)));
6
+ }
7
+ return chunks.join("");
8
+ }
9
+
10
+ export function binaryStringToBytes(value) {
11
+ const bytes = new Uint8Array(value.length);
12
+ for (let index = 0; index < value.length; index++) bytes[index] = value.charCodeAt(index);
13
+ return bytes;
14
+ }
15
+
16
+ export function equalBytes(left, right) {
17
+ if (left.length !== right.length) return false;
18
+ let difference = 0;
19
+ for (let index = 0; index < left.length; index++) difference |= left[index] ^ right[index];
20
+ return difference === 0;
21
+ }
@@ -0,0 +1,24 @@
1
+ import { deflateSync, inflateSync } from "fflate";
2
+
3
+ export function deflateRawSync(bytes) {
4
+ return deflateSync(bytes);
5
+ }
6
+
7
+ export function inflateRawSync(bytes, options = {}) {
8
+ const { maxOutputLength } = options;
9
+ if (maxOutputLength === undefined) return inflateSync(bytes);
10
+ if (!Number.isSafeInteger(maxOutputLength) || maxOutputLength < 0 ||
11
+ maxOutputLength === Number.MAX_SAFE_INTEGER) {
12
+ throw new RangeError("maxOutputLength must be a nonnegative safe integer");
13
+ }
14
+ const maximum = maxOutputLength + 1;
15
+ let capacity = Math.min(maximum, 0x10000);
16
+ while (true) {
17
+ const output = inflateSync(bytes, { out: new Uint8Array(capacity) });
18
+ if (output.length < capacity) return output;
19
+ if (capacity === maximum) {
20
+ throw new RangeError(`Decompressed data exceeds ${maxOutputLength} bytes`);
21
+ }
22
+ capacity = Math.min(maximum, capacity * 2);
23
+ }
24
+ }
@@ -0,0 +1,17 @@
1
+ import forge from "node-forge";
2
+ import { binaryStringToBytes, bytesToBinaryString } from "./bytes.js";
3
+
4
+ export function createHash(name) {
5
+ const factory = forge.md[name];
6
+ if (!factory) throw new RangeError(`Unsupported hash algorithm ${name}`);
7
+ const digest = factory.create();
8
+ return {
9
+ update(bytes) {
10
+ digest.update(bytesToBinaryString(bytes));
11
+ return this;
12
+ },
13
+ digest() {
14
+ return binaryStringToBytes(digest.digest().getBytes());
15
+ }
16
+ };
17
+ }
@@ -0,0 +1,64 @@
1
+ import { FormatError } from "../binary/errors.js";
2
+
3
+ function listRids(tables, startRef, endRef, table, pointerTable) {
4
+ const start = startRef?.rid ?? 0;
5
+ const indexTable = startRef?.table === pointerTable ? pointerTable : table;
6
+ const end = endRef?.rid ?? tables.counts[indexTable] + 1;
7
+ if (start <= 0 || end < start) return [];
8
+ if (startRef?.table === pointerTable) {
9
+ const pointerRows = tables.get(pointerTable);
10
+ return pointerRows.slice(start - 1, end - 1).map((row) => row[table === 4 ? "Field" :
11
+ table === 6 ? "Method" : table === 8 ? "Param" : table === 20 ? "Event" : "Property"].rid);
12
+ }
13
+ return Array.from({ length: end - start }, (_, index) => start + index);
14
+ }
15
+
16
+ export function resolveTypes(metadata, heaps) {
17
+ const typeDefs = metadata.get("TypeDef");
18
+ const types = [];
19
+ for (let index = 0; index < typeDefs.length; index++) {
20
+ const row = typeDefs[index];
21
+ const next = typeDefs[index + 1];
22
+ types.push({
23
+ rid: row.rid,
24
+ flags: row.Flags,
25
+ name: heaps.getString(row.TypeName),
26
+ namespace: heaps.getString(row.TypeNamespace),
27
+ extends: row.Extends.target,
28
+ fieldRids: listRids(metadata, row.FieldList, next?.FieldList, 4, 3),
29
+ methodRids: listRids(metadata, row.MethodList, next?.MethodList, 6, 5),
30
+ eventRids: [],
31
+ propertyRids: []
32
+ });
33
+ }
34
+ const eventMaps = metadata.get("EventMap");
35
+ const eventOwners = new Set();
36
+ for (let index = 0; index < eventMaps.length; index++) {
37
+ const row = eventMaps[index];
38
+ const next = eventMaps[index + 1];
39
+ const type = types[row.Parent.rid - 1];
40
+ if (!type) throw new FormatError(`EventMap row ${row.rid} references a missing TypeDef`);
41
+ if (eventOwners.has(type.rid)) throw new FormatError(`TypeDef row ${type.rid} has multiple EventMap rows`);
42
+ eventOwners.add(type.rid);
43
+ type.eventRids = listRids(metadata, row.EventList, next?.EventList, 20, 19);
44
+ }
45
+ const propertyMaps = metadata.get("PropertyMap");
46
+ const propertyOwners = new Set();
47
+ for (let index = 0; index < propertyMaps.length; index++) {
48
+ const row = propertyMaps[index];
49
+ const next = propertyMaps[index + 1];
50
+ const type = types[row.Parent.rid - 1];
51
+ if (!type) throw new FormatError(`PropertyMap row ${row.rid} references a missing TypeDef`);
52
+ if (propertyOwners.has(type.rid)) throw new FormatError(`TypeDef row ${type.rid} has multiple PropertyMap rows`);
53
+ propertyOwners.add(type.rid);
54
+ type.propertyRids = listRids(metadata, row.PropertyList, next?.PropertyList, 23, 22);
55
+ }
56
+ return types;
57
+ }
58
+
59
+ export function resolveMethodParameters(metadata) {
60
+ const methods = metadata.get("MethodDef");
61
+ return methods.map((row, index) =>
62
+ listRids(metadata, row.ParamList, methods[index + 1]?.ParamList, 8, 7)
63
+ );
64
+ }
@@ -0,0 +1,33 @@
1
+ export function createInstructionEditor(assembly: any, hooks: any): {
2
+ setInstructionOperand(methodRid: any, instructionOffset: any, value: any): any;
3
+ setInstructionOpcode(methodRid: any, instructionOffset: any, opcodeNameOrCode: any): any;
4
+ insertInstruction(methodRid: any, beforeOffset: any, opcodeNameOrCode: any, operand: any): {
5
+ opcode: any;
6
+ operand: {
7
+ kind: string;
8
+ targetOffset: any;
9
+ targetOffsets?: undefined;
10
+ value?: undefined;
11
+ } | {
12
+ kind: string;
13
+ targetOffsets: any[];
14
+ targetOffset?: undefined;
15
+ value?: undefined;
16
+ } | {
17
+ kind: any;
18
+ value: any;
19
+ targetOffset?: undefined;
20
+ targetOffsets?: undefined;
21
+ };
22
+ offset: number;
23
+ size: any;
24
+ opcodeSize: number;
25
+ opcodeOffset: any;
26
+ operandOffset: any;
27
+ patchOffsets: any[];
28
+ originalOffset: any;
29
+ isNew: boolean;
30
+ };
31
+ removeInstruction(methodRid: any, instructionOffset: any): any;
32
+ setMethodMaxStack(methodRid: any, maxStack: any): any;
33
+ };
@@ -0,0 +1,3 @@
1
+ export function createMethodRemovalEditor(hooks: any): {
2
+ removeMethod(rid: any): Map<any, any>;
3
+ };
@@ -0,0 +1,3 @@
1
+ export function createPointerTableEditor(hooks: any): {
2
+ reorderPointerTable(pointerTable: any, targetRids: any): /*elided*/ any;
3
+ };
@@ -0,0 +1,23 @@
1
+ export function createPortablePdbEditor(hooks: any): {
2
+ addEmbeddedPortablePdbDocument(debugEntryIndex: any, name: any, options: any): any;
3
+ insertEmbeddedPortablePdbDocument(debugEntryIndex: any, options: any): any;
4
+ addEmbeddedPortablePdbTypeDefinitionDocuments(debugEntryIndex: any, typeRid: any, documentRids: any): any;
5
+ removeEmbeddedPortablePdbDocument(debugEntryIndex: any, documentRid: any): any;
6
+ replaceEmbeddedPortablePdbTypeDefinitionDocuments(debugEntryIndex: any, typeRid: any, documentRids: any): any;
7
+ replaceEmbeddedPortablePdbDocumentMetadata(debugEntryIndex: any, documentRid: any, options: any): any;
8
+ replaceEmbeddedPortablePdbDocumentName(debugEntryIndex: any, documentRid: any, name: any, separator: any): any;
9
+ replaceEmbeddedPortablePdbSequencePoints(debugEntryIndex: any, methodRid: any, sequencePoints: any): any;
10
+ replaceEmbeddedPortablePdbMethodDocument(debugEntryIndex: any, methodRid: any, documentRid: any): any;
11
+ replaceEmbeddedPortablePdbImportScope(debugEntryIndex: any, importScopeRid: any, options: any): any;
12
+ replaceEmbeddedPortablePdbLocalScope(debugEntryIndex: any, localScopeRid: any, options: any): any;
13
+ replaceEmbeddedPortablePdbLocalVariable(debugEntryIndex: any, localVariableRid: any, options: any): any;
14
+ replaceEmbeddedPortablePdbLocalConstant(debugEntryIndex: any, localConstantRid: any, options: any): any;
15
+ replaceEmbeddedPortablePdbCustomDebugInformation(debugEntryIndex: any, customDebugInformationRid: any, value: any): any;
16
+ replaceEmbeddedPortablePdbSource(debugEntryIndex: any, documentRid: any, source: any, options: any): any;
17
+ addEmbeddedPortablePdbSource(debugEntryIndex: any, documentRid: any, source: any, options: any): any;
18
+ removeEmbeddedPortablePdbSource(debugEntryIndex: any, documentRid: any): any;
19
+ removeEmbeddedPortablePdbCustomDebugInformation(debugEntryIndex: any, recordRid: any): any;
20
+ replaceEmbeddedPortablePdbSourceLink(debugEntryIndex: any, sourceLink: any): any;
21
+ removeEmbeddedPortablePdbSourceLink(debugEntryIndex: any): any;
22
+ addEmbeddedPortablePdbSourceLink(debugEntryIndex: any, sourceLink: any): any;
23
+ };