jsilnet 0.1.0 → 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/README.md +25 -6
- package/REFERENCE.md +4 -0
- package/dist/browser/index.js +345 -0
- package/dist/browser/worker.js +42480 -0
- package/package.json +23 -4
- 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/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/index.d.ts +267 -0
- package/src/index.js +107 -1574
- package/src/metadata/portable-pdb.js +2 -2
- package/src/metadata/strong-name.js +84 -83
- package/src/pe/headers.js +1 -1
- package/src/platform/bytes.js +21 -0
- package/src/platform/compression.js +24 -0
- package/src/platform/crypto.js +17 -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/README.md
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# jsILNET
|
|
2
2
|
|
|
3
|
-
`jsILNET` is a
|
|
3
|
+
`jsILNET` is a JavaScript library for inspecting and rewriting .NET assemblies. It reads PE/CLI formats directly and does not require a .NET runtime. The package provides a synchronous Node.js API and a Promise-based browser API backed by a Web Worker.
|
|
4
4
|
|
|
5
5
|
## Requirements
|
|
6
6
|
|
|
7
7
|
- Node.js 20 or later
|
|
8
8
|
- ECMAScript modules
|
|
9
|
+
- Web Workers for the browser entry point (`jsilnet/browser`)
|
|
9
10
|
|
|
10
11
|
## Install
|
|
11
12
|
|
|
@@ -53,15 +54,32 @@ await writeFile("output.dll", assembly.toBuffer());
|
|
|
53
54
|
|
|
54
55
|
Edits are validated against the supported input profile; unsupported or unsafe rewrites fail explicitly. See the [Current capabilities](REFERENCE.md#current-capabilities) and [API surface](REFERENCE.md#api-surface) references for supported features, APIs, and limitations.
|
|
55
56
|
|
|
57
|
+
## Browser usage
|
|
56
58
|
|
|
57
|
-
|
|
58
|
-
A [Live-Demo](https://jsilnet-live-demo.vercel.app/) is available.
|
|
59
|
+
In a browser application using a package-aware bundler with module Worker support, import the separate browser entry point:
|
|
59
60
|
|
|
60
|
-
|
|
61
|
+
```js
|
|
62
|
+
import { parseAssembly } from "jsilnet/browser";
|
|
63
|
+
|
|
64
|
+
async function roundTripFile(file) {
|
|
65
|
+
const assembly = await parseAssembly(file); // File and Blob inputs are supported.
|
|
66
|
+
try {
|
|
67
|
+
const state = await assembly.getState();
|
|
68
|
+
console.log(`Assembly modified: ${state.modified}`);
|
|
69
|
+
const bytes = await assembly.serialize();
|
|
70
|
+
return new Blob([bytes], { type: "application/octet-stream" });
|
|
71
|
+
} finally {
|
|
72
|
+
assembly.dispose();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
61
76
|
|
|
62
|
-
The
|
|
77
|
+
The browser API is Promise-based and runs parsing, editing, and serialization in a Web Worker; the default Node.js entry point remains synchronous. Assembly methods must be awaited, `getState()` returns a snapshot, and `serialize()` returns a `Uint8Array`. Resolver callbacks such as `resolveEnum` and `resolveType` may be synchronous or asynchronous. Calling `dispose()` terminates the Worker and releases its Assembly state.
|
|
63
78
|
|
|
64
|
-
|
|
79
|
+
`parseAssembly` also accepts `Uint8Array` and `ArrayBuffer`. For full-buffer byte inputs, `{ transferInput: true }` transfers the input to the Worker and detaches it from the caller; subarray views are copied.
|
|
80
|
+
|
|
81
|
+
## Live Demo
|
|
82
|
+
The browser-only demo in `live-demo/` inspects and patches .NET assemblies without uploading them. Build it with `npm run build --prefix live-demo`, then serve `live-demo/dist/` from any static web host. See [`live-demo/README.md`](live-demo/README.md) for local preview instructions.
|
|
65
83
|
|
|
66
84
|
## Documentation
|
|
67
85
|
|
|
@@ -76,4 +94,5 @@ Source and documentation of this Demo-App is inside `live-demo/`
|
|
|
76
94
|
npm install
|
|
77
95
|
npm test
|
|
78
96
|
npm run check
|
|
97
|
+
npm run check:browser
|
|
79
98
|
```
|
package/REFERENCE.md
CHANGED
|
@@ -64,6 +64,10 @@ This document records detailed feature coverage, exported APIs, behavioral limit
|
|
|
64
64
|
|
|
65
65
|
The package exports `parseAssembly` and `Assembly`, plus lower-level PE, metadata, heap, signature, binary-reader, coded-index, CIL-instruction, and method-body parsing utilities.
|
|
66
66
|
|
|
67
|
+
The `jsilnet/browser` package subpath exports a Promise-based `parseAssembly` and `Assembly` proxy. It accepts a `Blob` for worker-side file reading, or a `Uint8Array`/`ArrayBuffer`; for a full-buffer `Uint8Array` or `ArrayBuffer`, `transferInput: true` transfers ownership and detaches the input from the caller. Assembly operations execute in a dedicated module Worker; `getState()` retrieves a structured-cloneable snapshot, `serialize()` returns a `Uint8Array`, and `dispose()` terminates the Worker. `callUtility(name, ...args)` runs an exported standalone utility in a Worker. Browser support is not yet fully at parity: callback-valued `resolveEnum`/`resolveType` options cannot be structured-cloned, and standalone utility results omit function-valued properties other than the restored `parsePE().rvaToOffset` mapping.
|
|
68
|
+
|
|
69
|
+
The package declarations cover all package exports and public `Assembly` methods. Run `npm run check:types` to regenerate the declarations, compile a consumer type check, and verify API coverage.
|
|
70
|
+
|
|
67
71
|
An `Assembly` exposes parsed PE/CLI metadata, `module`, `types`, `typeRefs`, `typeSpecs`, `methods`, `memberRefs`, `moduleRefs`, `methodSpecs`, `genericParams`, `genericParamConstraints`, `customAttributes`, `assemblyRefs`, `files`, and `resources`. Type records include their fields, methods, events, properties, and generic parameters; method records include their parameter rows. Method bodies are loaded with `getMethodBody` or `getInstructions`. Editing methods include `setInstructionOperand`, `setInstructionOpcode`, `insertInstruction`, `removeInstruction`, `setMethodMaxStack`, `replaceMethodSignature`, `replaceCodeViewPath`, `replaceModule`, `replaceField`, `replaceFieldSignature`, `addField`, `insertField`, `removeField`, `addFieldRva`, `replaceFieldRva`, `removeFieldRva`, `addMethod`, `insertMethod`, `addConstant`, `replaceConstant`, `addParam`, `insertParam`, `replaceParam`, `removeParam`, `addProperty`, `insertProperty`, `removeProperty`, `addEvent`, `insertEvent`, `removeEvent`, `addMethodSemantics`, `replaceMethodSemantics`, `removeMethodSemantics`, `replaceAssemblyIdentity`, `signStrongName`, `replaceStrongNamePublicKey`, `verifyStrongNameSignature`, `parseCustomAttribute`, `replaceCustomAttribute`, `addCustomAttribute`, `removeCustomAttribute`, `addManifestResource`, `replaceManifestResource`, `replaceLinkedResource`, `removeManifestResource`, `addTypeRef`, `insertTypeRefSorted`, `removeTypeRef`, `addModuleRef`, `replaceModuleRef`, `removeModuleRef`, `addTypeSpec`, `replaceTypeSpecSignature`, `addStandAloneSig`, `insertStandAloneSig`, `replaceStandAloneSig`, `removeStandAloneSig`, `addMemberRef`, `insertMemberRefSorted`, `addMethodSpec`, `replaceMethodSpecInstantiation`, `insertMethodSpecSorted`, `removeMethodSpec`, `addGenericParam`, `replaceGenericParam`, `removeGenericParam`, `addGenericParamConstraint`, `insertGenericParamConstraint`, `replaceGenericParamConstraint`, `removeGenericParamConstraint`, `addAssemblyRef`, `insertAssemblyRef`, `removeAssemblyRef`, `addAssemblyProcessor`, `replaceAssemblyProcessor`, `removeAssemblyProcessor`, `addAssemblyOS`, `replaceAssemblyOS`, `removeAssemblyOS`, `addAssemblyRefProcessor`, `replaceAssemblyRefProcessor`, `removeAssemblyRefProcessor`, `addAssemblyRefOS`, `replaceAssemblyRefOS`, `removeAssemblyRefOS`, `addFile`, `insertFile`, `removeFile`, and `addUserString`; sorted insertion and removal methods return a token map when existing token RIDs change. `addConstant` and `replaceConstant` accept the ECMA element type and its raw encoded value bytes; adding requires an existing Field, Param, or Property parent and rejects duplicate parents or an append that violates declared table ordering. `replaceModule({ name, generation })` updates the module name or unsigned 16-bit generation while preserving its MVID. `signStrongName(privateKey)` accepts an RSA PEM or Node.js private `KeyObject` and signs during serialization; the key must match the assembly's existing full public key, and a correctly sized signature slot is reused or allocated in a new PE section. `replaceStrongNamePublicKey(privateKey, { hashAlgorithm })` explicitly changes the full Assembly public key/hash ID to match the supplied private key, then requests signing; the Assembly name, version, and culture remain unchanged. `replaceAssemblyIdentity({ name, culture, version })` changes those three manifest identity fields. `verifyStrongNameSignature()` verifies signatures using the embedded RSA public key. Signing supports the standard 16-slot PE optional header when 15 or 16 data directories are declared, and SHA-1, SHA-256, SHA-384, or SHA-512 assembly hash IDs; other directory-count profiles are refused. Custom-attribute and PermissionSet parse/edit methods accept `resolveEnum(name)` and `resolveType(name, context)` options (or properties on their input objects); a type resolver returns `{ assembly, rid }` for a TypeDef from another parsed assembly and can enable external enum, constructor, inheritance, and named-member validation. `addCustomAttribute` appends only when the declared parent ordering remains valid. `replaceLinkedResource(rid, data)` supports resources whose `Implementation` points directly to a File row, updates that File row's hash using the containing Assembly hash algorithm, and leaves the new external payload available as `resources[rid - 1].data`; it does not write the external file. `insertGenericParamConstraint({ ownerRid, constraint })` inserts into an already Owner-ordered GenericParamConstraint table and returns `{ rid, tokenMap }`. `Resource, File, TypeRef, ModuleRef, AssemblyRef, StandAloneSig, MethodSpec, GenericParamConstraint, GenericParam, Property, Event, and CustomAttribute removal rejects rows with live references and remaps surviving references. `serialize` returns a `Uint8Array`, and `toBuffer` returns a Node.js `Buffer`.
|
|
68
72
|
Sorted row insertions are available through `insertConstant`, `insertFieldMarshal`, `insertClassLayout`, `insertFieldLayout`, `insertFieldRva`, `insertImplMap`, `insertMethodSemantics`, `insertMethodImpl`, `insertNestedClass`, `insertGenericParam`, `insertDeclSecurity`, `insertInterfaceImplementation`, `insertCustomAttribute`, and the TypeRef/MemberRef/MethodSpec insertion APIs. Token-row removals for Constant, FieldMarshal, FieldRVA, ClassLayout, FieldLayout, DeclSecurity, MethodSemantics, MethodImpl, ImplMap, and NestedClass reject live references, remap surviving token references, and return a token map; edits that shift tokens are refused with debug data, opaque metadata streams, or incompatible method-body edits.
|
|
69
73
|
`insertFile({ beforeRid, name, flags, hashValue })` inserts at a 1-based File row position (use `files.length + 1` to append), remaps File references, and returns `{ token, tokenMap }`.
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
// src/binary/errors.js
|
|
2
|
+
var FormatError = class extends Error {
|
|
3
|
+
constructor(message, { offset, context } = {}) {
|
|
4
|
+
const location = Number.isInteger(offset) ? ` at file offset 0x${offset.toString(16)}` : "";
|
|
5
|
+
const scope = context ? ` (${context})` : "";
|
|
6
|
+
super(`${message}${location}${scope}`);
|
|
7
|
+
this.name = "FormatError";
|
|
8
|
+
this.offset = offset;
|
|
9
|
+
this.context = context;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var UnsupportedFormatError = class extends FormatError {
|
|
13
|
+
constructor(message, options = {}) {
|
|
14
|
+
super(message, options);
|
|
15
|
+
this.name = "UnsupportedFormatError";
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// src/browser/index.js
|
|
20
|
+
var nextRequestId = 0;
|
|
21
|
+
function createWorker(workerFactory) {
|
|
22
|
+
if (workerFactory) return workerFactory();
|
|
23
|
+
if (typeof Worker === "undefined") {
|
|
24
|
+
throw new Error("jsILNET browser APIs require Web Worker support");
|
|
25
|
+
}
|
|
26
|
+
return new Worker(new URL("./worker.js", import.meta.url), { type: "module" });
|
|
27
|
+
}
|
|
28
|
+
function normalizeInput(input, transferInput) {
|
|
29
|
+
if (typeof Blob !== "undefined" && input instanceof Blob) {
|
|
30
|
+
return { input, transfer: [] };
|
|
31
|
+
}
|
|
32
|
+
if (input instanceof ArrayBuffer) {
|
|
33
|
+
const buffer = transferInput ? input : input.slice(0);
|
|
34
|
+
return { input: buffer, transfer: [buffer] };
|
|
35
|
+
}
|
|
36
|
+
if (input instanceof Uint8Array) {
|
|
37
|
+
if (transferInput && input.buffer instanceof ArrayBuffer && input.byteOffset === 0 && input.byteLength === input.buffer.byteLength) {
|
|
38
|
+
return { input: input.buffer, transfer: [input.buffer] };
|
|
39
|
+
}
|
|
40
|
+
const bytes = Uint8Array.from(input);
|
|
41
|
+
return { input: bytes.buffer, transfer: [bytes.buffer] };
|
|
42
|
+
}
|
|
43
|
+
throw new TypeError("Assembly input must be a Uint8Array, ArrayBuffer, or Blob");
|
|
44
|
+
}
|
|
45
|
+
function encodeCallbacks(value, callbacks, seen = /* @__PURE__ */ new WeakMap()) {
|
|
46
|
+
if (typeof value === "function") {
|
|
47
|
+
const id = callbacks.size + 1;
|
|
48
|
+
callbacks.set(id, value);
|
|
49
|
+
return { __jsilnetCallback: id };
|
|
50
|
+
}
|
|
51
|
+
if (value === null || typeof value !== "object") return value;
|
|
52
|
+
if (seen.has(value)) return seen.get(value);
|
|
53
|
+
if (typeof Blob !== "undefined" && value instanceof Blob) return value;
|
|
54
|
+
if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) return value;
|
|
55
|
+
const output = value instanceof Map ? /* @__PURE__ */ new Map() : value instanceof Set ? /* @__PURE__ */ new Set() : Array.isArray(value) ? [] : {};
|
|
56
|
+
seen.set(value, output);
|
|
57
|
+
if (value instanceof Map) {
|
|
58
|
+
for (const [key, item] of value) {
|
|
59
|
+
output.set(encodeCallbacks(key, callbacks, seen), encodeCallbacks(item, callbacks, seen));
|
|
60
|
+
}
|
|
61
|
+
} else if (value instanceof Set) {
|
|
62
|
+
for (const item of value) output.add(encodeCallbacks(item, callbacks, seen));
|
|
63
|
+
} else if (Array.isArray(value)) {
|
|
64
|
+
for (const item of value) output.push(encodeCallbacks(item, callbacks, seen));
|
|
65
|
+
} else {
|
|
66
|
+
for (const key of Object.keys(value)) {
|
|
67
|
+
output[key] = encodeCallbacks(value[key], callbacks, seen);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return output;
|
|
71
|
+
}
|
|
72
|
+
function decodeAssemblyReferences(value, assembly, seen = /* @__PURE__ */ new WeakMap()) {
|
|
73
|
+
if (value === null || typeof value !== "object") return value;
|
|
74
|
+
if (value.__jsilnetAssembly === true) return assembly;
|
|
75
|
+
if (seen.has(value)) return seen.get(value);
|
|
76
|
+
if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) return value;
|
|
77
|
+
const output = value instanceof Map ? /* @__PURE__ */ new Map() : value instanceof Set ? /* @__PURE__ */ new Set() : Array.isArray(value) ? [] : {};
|
|
78
|
+
seen.set(value, output);
|
|
79
|
+
if (value instanceof Map) {
|
|
80
|
+
for (const [key, item] of value) {
|
|
81
|
+
output.set(
|
|
82
|
+
decodeAssemblyReferences(key, assembly, seen),
|
|
83
|
+
decodeAssemblyReferences(item, assembly, seen)
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
} else if (value instanceof Set) {
|
|
87
|
+
for (const item of value) output.add(decodeAssemblyReferences(item, assembly, seen));
|
|
88
|
+
} else if (Array.isArray(value)) {
|
|
89
|
+
for (const item of value) output.push(decodeAssemblyReferences(item, assembly, seen));
|
|
90
|
+
} else {
|
|
91
|
+
for (const key of Object.keys(value)) {
|
|
92
|
+
output[key] = decodeAssemblyReferences(value[key], assembly, seen);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return output;
|
|
96
|
+
}
|
|
97
|
+
async function encodeResolverResult(value, currentAssembly, seen = /* @__PURE__ */ new WeakMap()) {
|
|
98
|
+
if (value instanceof BrowserAssembly) {
|
|
99
|
+
return value === currentAssembly ? { __jsilnetAssemblyReference: true } : { __jsilnetSerializedAssembly: await value.serialize() };
|
|
100
|
+
}
|
|
101
|
+
if (value === null || typeof value !== "object") {
|
|
102
|
+
if (typeof value === "function" || typeof value === "symbol") {
|
|
103
|
+
throw new TypeError("Resolver results must be structured-cloneable values");
|
|
104
|
+
}
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
if (seen.has(value)) return seen.get(value);
|
|
108
|
+
if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) return value;
|
|
109
|
+
const output = value instanceof Map ? /* @__PURE__ */ new Map() : value instanceof Set ? /* @__PURE__ */ new Set() : Array.isArray(value) ? [] : {};
|
|
110
|
+
seen.set(value, output);
|
|
111
|
+
if (value instanceof Map) {
|
|
112
|
+
for (const [key, item] of value) {
|
|
113
|
+
output.set(
|
|
114
|
+
await encodeResolverResult(key, currentAssembly, seen),
|
|
115
|
+
await encodeResolverResult(item, currentAssembly, seen)
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
} else if (value instanceof Set) {
|
|
119
|
+
for (const item of value) {
|
|
120
|
+
output.add(await encodeResolverResult(item, currentAssembly, seen));
|
|
121
|
+
}
|
|
122
|
+
} else if (Array.isArray(value)) {
|
|
123
|
+
for (const item of value) output.push(await encodeResolverResult(item, currentAssembly, seen));
|
|
124
|
+
} else {
|
|
125
|
+
for (const key of Object.keys(value)) {
|
|
126
|
+
output[key] = await encodeResolverResult(value[key], currentAssembly, seen);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return output;
|
|
130
|
+
}
|
|
131
|
+
function makeError(data) {
|
|
132
|
+
const error = data?.name === "FormatError" ? new FormatError(data.message) : data?.name === "UnsupportedFormatError" ? new UnsupportedFormatError(data.message) : new Error(data?.message ?? "Browser worker request failed");
|
|
133
|
+
if (data?.name && data.name !== "FormatError" && data.name !== "UnsupportedFormatError") {
|
|
134
|
+
error.name = data.name;
|
|
135
|
+
}
|
|
136
|
+
if (data && "offset" in data) error.offset = data.offset;
|
|
137
|
+
if (data && "context" in data) error.context = data.context;
|
|
138
|
+
return error;
|
|
139
|
+
}
|
|
140
|
+
function addRvaMapper(pe) {
|
|
141
|
+
if (!pe || !Array.isArray(pe.sections) || !Number.isInteger(pe.sectionOffset)) return pe;
|
|
142
|
+
pe.rvaToOffset = (rva, size = 1) => {
|
|
143
|
+
if (!Number.isInteger(rva) || !Number.isInteger(size) || rva < 0 || size < 0) {
|
|
144
|
+
throw new FormatError("Invalid RVA range (RVA mapping)");
|
|
145
|
+
}
|
|
146
|
+
if (rva === 0 && size === 0) return 0;
|
|
147
|
+
for (const section of pe.sections) {
|
|
148
|
+
const span = Math.max(section.virtualSize, section.rawSize);
|
|
149
|
+
if (rva < section.virtualAddress || rva - section.virtualAddress >= span) continue;
|
|
150
|
+
const delta = rva - section.virtualAddress;
|
|
151
|
+
if (delta + size > section.rawSize || section.rawPointer + delta + size > pe.bytes.length) {
|
|
152
|
+
throw new FormatError(
|
|
153
|
+
"RVA range is not backed by file data",
|
|
154
|
+
{ offset: section.rawPointer + delta, context: `section ${section.name}` }
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
return section.rawPointer + delta;
|
|
158
|
+
}
|
|
159
|
+
if (rva < pe.sectionOffset && rva + size <= pe.bytes.length) return rva;
|
|
160
|
+
throw new FormatError(`RVA 0x${rva.toString(16)} is not mapped by a PE section (RVA mapping)`);
|
|
161
|
+
};
|
|
162
|
+
return pe;
|
|
163
|
+
}
|
|
164
|
+
var WorkerClient = class {
|
|
165
|
+
#worker;
|
|
166
|
+
#pending = /* @__PURE__ */ new Map();
|
|
167
|
+
#onMessage;
|
|
168
|
+
#onError;
|
|
169
|
+
#onMessageError;
|
|
170
|
+
#closed = false;
|
|
171
|
+
constructor(worker) {
|
|
172
|
+
this.#worker = worker;
|
|
173
|
+
this.#onMessage = (event) => {
|
|
174
|
+
const response = event.data;
|
|
175
|
+
const pending = this.#pending.get(response?.id);
|
|
176
|
+
if (!pending) return;
|
|
177
|
+
if (response.type === "resolver") {
|
|
178
|
+
void this.#resolveCallback(response, pending);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
this.#pending.delete(response.id);
|
|
182
|
+
if (response.ok) pending.resolve(response.value);
|
|
183
|
+
else pending.reject(makeError(response.error));
|
|
184
|
+
};
|
|
185
|
+
this.#onError = (event) => {
|
|
186
|
+
const error = event.error ?? new Error(event.message || "Browser worker failed");
|
|
187
|
+
for (const pending of this.#pending.values()) pending.reject(error);
|
|
188
|
+
this.#pending.clear();
|
|
189
|
+
this.#close();
|
|
190
|
+
};
|
|
191
|
+
this.#onMessageError = () => {
|
|
192
|
+
const error = new Error("Browser worker sent an uncloneable response");
|
|
193
|
+
for (const pending of this.#pending.values()) pending.reject(error);
|
|
194
|
+
this.#pending.clear();
|
|
195
|
+
this.#close();
|
|
196
|
+
};
|
|
197
|
+
worker.addEventListener("message", this.#onMessage);
|
|
198
|
+
worker.addEventListener("error", this.#onError);
|
|
199
|
+
worker.addEventListener("messageerror", this.#onMessageError);
|
|
200
|
+
}
|
|
201
|
+
request(type, payload = {}, transfer = [], resolverOptions = {}) {
|
|
202
|
+
if (this.#closed) {
|
|
203
|
+
return Promise.reject(new Error("Browser worker is closed"));
|
|
204
|
+
}
|
|
205
|
+
const id = ++nextRequestId;
|
|
206
|
+
return new Promise((resolve, reject) => {
|
|
207
|
+
const callbacks = /* @__PURE__ */ new Map();
|
|
208
|
+
const encodedPayload = encodeCallbacks(payload, callbacks);
|
|
209
|
+
this.#pending.set(id, { resolve, reject, callbacks, resolverOptions });
|
|
210
|
+
try {
|
|
211
|
+
this.#worker.postMessage({ id, type, ...encodedPayload }, transfer);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
this.#pending.delete(id);
|
|
214
|
+
if (error?.name === "DataCloneError") {
|
|
215
|
+
reject(new TypeError("Browser Worker request contains a non-cloneable value"));
|
|
216
|
+
} else {
|
|
217
|
+
reject(error);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
async #resolveCallback(response, pending) {
|
|
223
|
+
const callback = pending.callbacks.get(response.callbackId);
|
|
224
|
+
try {
|
|
225
|
+
if (!callback) throw new TypeError("Worker requested an unknown resolver callback");
|
|
226
|
+
const args = pending.resolverOptions.decodeArguments ? pending.resolverOptions.decodeArguments(response.args) : response.args;
|
|
227
|
+
const result = await callback(...args);
|
|
228
|
+
if (this.#closed || !this.#pending.has(response.id)) return;
|
|
229
|
+
const value = pending.resolverOptions.encodeResult ? await pending.resolverOptions.encodeResult(result) : result;
|
|
230
|
+
if (this.#closed || !this.#pending.has(response.id)) return;
|
|
231
|
+
this.#worker.postMessage({
|
|
232
|
+
type: "resolverResult",
|
|
233
|
+
id: response.id,
|
|
234
|
+
resolverRequestId: response.resolverRequestId,
|
|
235
|
+
ok: true,
|
|
236
|
+
value
|
|
237
|
+
});
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if (this.#closed || !this.#pending.has(response.id)) return;
|
|
240
|
+
this.#worker.postMessage({
|
|
241
|
+
type: "resolverResult",
|
|
242
|
+
id: response.id,
|
|
243
|
+
resolverRequestId: response.resolverRequestId,
|
|
244
|
+
ok: false,
|
|
245
|
+
error: {
|
|
246
|
+
name: error?.name ?? "Error",
|
|
247
|
+
message: error?.message ?? String(error)
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
dispose() {
|
|
253
|
+
if (this.#closed) return;
|
|
254
|
+
for (const pending of this.#pending.values()) {
|
|
255
|
+
pending.reject(new Error("Browser worker was disposed"));
|
|
256
|
+
}
|
|
257
|
+
this.#pending.clear();
|
|
258
|
+
this.#close();
|
|
259
|
+
}
|
|
260
|
+
#close() {
|
|
261
|
+
if (this.#closed) return;
|
|
262
|
+
this.#closed = true;
|
|
263
|
+
this.#worker.removeEventListener("message", this.#onMessage);
|
|
264
|
+
this.#worker.removeEventListener("error", this.#onError);
|
|
265
|
+
this.#worker.removeEventListener("messageerror", this.#onMessageError);
|
|
266
|
+
this.#worker.terminate();
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
var BrowserAssembly = class _BrowserAssembly {
|
|
270
|
+
#client;
|
|
271
|
+
#proxy;
|
|
272
|
+
constructor(client) {
|
|
273
|
+
this.#client = client;
|
|
274
|
+
}
|
|
275
|
+
static async from(input, { workerFactory, transferInput = false } = {}) {
|
|
276
|
+
const normalizedInput = normalizeInput(input, transferInput);
|
|
277
|
+
const worker = createWorker(workerFactory);
|
|
278
|
+
const client = new WorkerClient(worker);
|
|
279
|
+
const { input: inputValue, transfer } = normalizedInput;
|
|
280
|
+
try {
|
|
281
|
+
await client.request("parse", { input: inputValue }, transfer);
|
|
282
|
+
const instance = new _BrowserAssembly(client);
|
|
283
|
+
instance.#proxy = new Proxy(instance, {
|
|
284
|
+
get(target, property, receiver) {
|
|
285
|
+
if (property === "then") return void 0;
|
|
286
|
+
if (Reflect.has(target, property)) {
|
|
287
|
+
const value = Reflect.get(target, property, target);
|
|
288
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
289
|
+
}
|
|
290
|
+
if (typeof property === "string") {
|
|
291
|
+
return (...args) => target.#call(property, args);
|
|
292
|
+
}
|
|
293
|
+
return void 0;
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
return instance.#proxy;
|
|
297
|
+
} catch (error) {
|
|
298
|
+
client.dispose();
|
|
299
|
+
throw error;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
async getState() {
|
|
303
|
+
return this.#client.request("state");
|
|
304
|
+
}
|
|
305
|
+
async callUtility(name, ...args) {
|
|
306
|
+
const result = await this.#client.request("utility", { name, args });
|
|
307
|
+
return name === "parsePE" ? addRvaMapper(result) : result;
|
|
308
|
+
}
|
|
309
|
+
async #call(name, args) {
|
|
310
|
+
const result = await this.#client.request(
|
|
311
|
+
"call",
|
|
312
|
+
{ name, args },
|
|
313
|
+
[],
|
|
314
|
+
{
|
|
315
|
+
decodeArguments: (value) => decodeAssemblyReferences(value, this.#proxy),
|
|
316
|
+
encodeResult: (value) => encodeResolverResult(value, this.#proxy)
|
|
317
|
+
}
|
|
318
|
+
);
|
|
319
|
+
return result?.__jsilnetAssembly === true ? this.#proxy : result;
|
|
320
|
+
}
|
|
321
|
+
dispose() {
|
|
322
|
+
this.#client.dispose();
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
async function parseAssembly(input, options) {
|
|
326
|
+
return BrowserAssembly.from(input, options);
|
|
327
|
+
}
|
|
328
|
+
async function callUtility(name, ...args) {
|
|
329
|
+
const worker = createWorker();
|
|
330
|
+
const client = new WorkerClient(worker);
|
|
331
|
+
try {
|
|
332
|
+
const result = await client.request("utility", { name, args });
|
|
333
|
+
return name === "parsePE" ? addRvaMapper(result) : result;
|
|
334
|
+
} finally {
|
|
335
|
+
client.dispose();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
export {
|
|
339
|
+
BrowserAssembly as Assembly,
|
|
340
|
+
BrowserAssembly,
|
|
341
|
+
FormatError,
|
|
342
|
+
UnsupportedFormatError,
|
|
343
|
+
callUtility,
|
|
344
|
+
parseAssembly
|
|
345
|
+
};
|