Files
rea/src/domain/javascriptExportInstrumentation.ts
T

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");