133 lines
4.4 KiB
TypeScript
133 lines
4.4 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
|
|
import { z } from "zod";
|
|
|
|
const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u);
|
|
|
|
export const javascriptExportInstrumentationInputSchema = z.strictObject({
|
|
artifact_path: z.string().min(1).max(4_096),
|
|
artifact_sha256: digestSchema,
|
|
selection: z.strictObject({
|
|
byte_start: z.number().int().min(0),
|
|
byte_end: z.number().int().min(1),
|
|
selected_sha256: digestSchema,
|
|
export_name: z.string().regex(/^[A-Za-z_$][A-Za-z0-9_$]{0,199}$/u),
|
|
}),
|
|
});
|
|
|
|
export const javascriptExportTransformationManifestSchema = z.strictObject({
|
|
schema_version: z.literal(1),
|
|
kind: z.literal("commonjs-factory-export-v1"),
|
|
artifact_path: z.string().min(1).max(4_096),
|
|
original_sha256: digestSchema,
|
|
original_byte_length: z.number().int().min(0),
|
|
instrumented_sha256: digestSchema,
|
|
instrumented_byte_length: z.number().int().min(0),
|
|
selected_range: z.strictObject({
|
|
byte_start: z.number().int().min(0),
|
|
byte_end: z.number().int().min(1),
|
|
selected_sha256: digestSchema,
|
|
}),
|
|
inserted_ranges: z
|
|
.array(
|
|
z.strictObject({
|
|
byte_start: z.number().int().min(0),
|
|
byte_end: z.number().int().min(1),
|
|
inserted_sha256: digestSchema,
|
|
purpose: z.enum(["factory-prefix", "export-binding", "factory-suffix"]),
|
|
}),
|
|
)
|
|
.length(3),
|
|
output_format: z.literal("commonjs-factory"),
|
|
source_map: z.literal(null),
|
|
});
|
|
|
|
export interface JavaScriptExportInstrumentation {
|
|
readonly bytes: Uint8Array;
|
|
readonly manifest: z.infer<
|
|
typeof javascriptExportTransformationManifestSchema
|
|
>;
|
|
}
|
|
|
|
/** Instrument one exact UTF-8 expression range without mutating source bytes. */
|
|
export const instrumentJavaScriptExport = (
|
|
source: Uint8Array,
|
|
rawInput: unknown,
|
|
): JavaScriptExportInstrumentation => {
|
|
const input = javascriptExportInstrumentationInputSchema.parse(rawInput);
|
|
if (sha256(source) !== input.artifact_sha256)
|
|
throw new TypeError("JavaScript instrumentation artifact digest mismatch");
|
|
const { byte_start: start, byte_end: end } = input.selection;
|
|
if (end <= start || end > source.byteLength)
|
|
throw new TypeError(
|
|
"JavaScript instrumentation selection is out of bounds",
|
|
);
|
|
const selected = source.slice(start, end);
|
|
if (sha256(selected) !== input.selection.selected_sha256)
|
|
throw new TypeError("JavaScript instrumentation selection digest mismatch");
|
|
decodeUtf8(source);
|
|
decodeUtf8(selected);
|
|
|
|
const prefix = encode("function(module, exports, require) {\n");
|
|
const binding = encode(
|
|
`\nmodule.exports[${JSON.stringify(input.selection.export_name)}] = (${decodeUtf8(selected)});\n`,
|
|
);
|
|
const suffix = encode("}\n");
|
|
const bytes = concatenate([prefix, source, binding, suffix]);
|
|
const bindingStart = prefix.byteLength + source.byteLength;
|
|
const suffixStart = bindingStart + binding.byteLength;
|
|
const manifest = javascriptExportTransformationManifestSchema.parse({
|
|
schema_version: 1,
|
|
kind: "commonjs-factory-export-v1",
|
|
artifact_path: input.artifact_path,
|
|
original_sha256: input.artifact_sha256,
|
|
original_byte_length: source.byteLength,
|
|
instrumented_sha256: sha256(bytes),
|
|
instrumented_byte_length: bytes.byteLength,
|
|
selected_range: {
|
|
byte_start: start,
|
|
byte_end: end,
|
|
selected_sha256: input.selection.selected_sha256,
|
|
},
|
|
inserted_ranges: [
|
|
range(0, prefix, "factory-prefix"),
|
|
range(bindingStart, binding, "export-binding"),
|
|
range(suffixStart, suffix, "factory-suffix"),
|
|
],
|
|
output_format: "commonjs-factory",
|
|
source_map: null,
|
|
});
|
|
return { bytes, manifest };
|
|
};
|
|
|
|
const range = (
|
|
start: number,
|
|
bytes: Uint8Array,
|
|
purpose: "factory-prefix" | "export-binding" | "factory-suffix",
|
|
) => ({
|
|
byte_start: start,
|
|
byte_end: start + bytes.byteLength,
|
|
inserted_sha256: sha256(bytes),
|
|
purpose,
|
|
});
|
|
|
|
const encode = (value: string): Uint8Array => new TextEncoder().encode(value);
|
|
|
|
const decodeUtf8 = (value: Uint8Array): string =>
|
|
new TextDecoder("utf-8", { fatal: true }).decode(value);
|
|
|
|
const concatenate = (values: readonly Uint8Array[]): Uint8Array => {
|
|
const output = new Uint8Array(
|
|
values.reduce((total, value) => total + value.byteLength, 0),
|
|
);
|
|
let offset = 0;
|
|
for (const value of values) {
|
|
output.set(value, offset);
|
|
offset += value.byteLength;
|
|
}
|
|
return output;
|
|
};
|
|
|
|
const sha256 = (value: Uint8Array): string =>
|
|
createHash("sha256").update(value).digest("hex");
|