import { fnv1a64 } from "./replay.mjs"; function buildId(core) { if (typeof core._sim_checkpoint_build_id !== "function") throw new Error("native_checkpoint_build_id_missing"); const pointer = core._sim_checkpoint_build_id(); const id = new TextDecoder().decode(core.HEAPU8.subarray(pointer, pointer + 64)); if (!/^[a-f0-9]{64}$/.test(id)) throw new Error("native_checkpoint_unversioned_build"); return id; } // Copy opaque native state with zero-filled blocks omitted. No arithmetic or // state reconstruction occurs here; each C adapter owns its layout/validation. export function exportNativeCheckpoint(core, prefix, owner = null) { const length = core[`_${prefix}_checkpoint_size`](); const pointer = core._malloc(length); if (!pointer) throw new Error(`${prefix}_checkpoint_allocation`); try { const args = owner === null ? [pointer, length] : [owner, pointer, length]; const code = core[`_${prefix}_checkpoint_export`](...args); if (code !== 0) throw new Error(`${prefix}_checkpoint_export_${code}`); const bytes = core.HEAPU8.subarray(pointer, pointer + length), blocks = []; for (let offset = 0; offset < length; offset += 256) { const block = bytes.subarray(offset, Math.min(offset + 256, length)); if (block.some((value) => value !== 0)) blocks.push([offset, Array.from(block, (value) => value.toString(16).padStart(2, "0")).join("")]); } const value = { schema: "native-checkpoint.v1", component: prefix, build_id: buildId(core), byte_length: length, blocks }; return { ...value, state_hash: fnv1a64(value) }; } finally { core._free(pointer); } } export function validateNativeCheckpoint(core, prefix, value) { if (value?.build_id !== buildId(core)) throw new Error(`${prefix}_checkpoint_build_mismatch`); const length = core[`_${prefix}_checkpoint_size`](); if (!value || value.schema !== "native-checkpoint.v1" || value.component !== prefix || value.byte_length !== length || !Array.isArray(value.blocks) || fnv1a64(Object.fromEntries(Object.entries(value).filter(([key]) => key !== "state_hash"))) !== value.state_hash) throw new Error(`${prefix}_checkpoint_invalid`); const bytes = new Uint8Array(length); let end = 0; for (const [offset, hex] of value.blocks) { if (!Number.isSafeInteger(offset) || offset < end || typeof hex !== "string" || !/^(?:[0-9a-f]{2})+$/.test(hex) || offset + hex.length / 2 > length) throw new Error(`${prefix}_checkpoint_blocks`); for (let index = 0; index < hex.length; index += 2) bytes[offset + index / 2] = Number.parseInt(hex.slice(index, index + 2), 16); end = offset + hex.length / 2; } return bytes; } export function restoreNativeCheckpoint(core, prefix, value, owner = null) { const bytes = validateNativeCheckpoint(core, prefix, value); const length = bytes.length; const pointer = core._malloc(length); if (!pointer) throw new Error(`${prefix}_checkpoint_allocation`); try { core.HEAPU8.set(bytes, pointer); const args = owner === null ? [pointer, length] : [owner, pointer, length]; const code = core[`_${prefix}_checkpoint_restore`](...args); if (code !== 0) throw new Error(`${prefix}_checkpoint_restore_${code}`); } finally { core._free(pointer); } }