mirror of
https://github.com/Xahau/xahaud.git
synced 2026-09-04 00:08:44 +00:00
Replace configure-time header/embed generation with a static QuickJSProviderProfile.h and one generated QuickJSProviderValues.cpp. CMake only EXISTS-checks the three lock files. A refresh target plus fingerprint stamp sees wasm appear or change without reconfigure. The snapshot importer accepts python/jshookz or packages/jshookz.
286 lines
10 KiB
Python
286 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""Project the sealed QuickJS bundle into one C++ translation unit."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
CHUNK = 64
|
|
CMAKE_SET = re.compile(r'^set\(XAHAU_QUICKJS_([A-Z0-9_]+) "([^"]*)"\)\s*$')
|
|
|
|
|
|
def quote(value: str) -> str:
|
|
if not isinstance(value, str) or any(c in value for c in "\n\r"):
|
|
raise ValueError(f"invalid QuickJS policy string: {value!r}")
|
|
return json.dumps(value)
|
|
|
|
|
|
def parse_cmake(path: Path) -> dict[str, str]:
|
|
values: dict[str, str] = {}
|
|
for line in path.read_text().splitlines():
|
|
match = CMAKE_SET.match(line)
|
|
if match:
|
|
values[match.group(1)] = match.group(2)
|
|
return values
|
|
|
|
|
|
def hex_bytes(value: str) -> str:
|
|
if len(value) != 64 or any(c not in "0123456789abcdefABCDEF" for c in value):
|
|
raise ValueError(f"QuickJS identity must be 32 hexadecimal bytes: {value!r}")
|
|
return ", ".join(f"0x{value[i:i + 2]}" for i in range(0, 64, 2))
|
|
|
|
|
|
def require(cmake: dict[str, str], key: str) -> str:
|
|
if key not in cmake:
|
|
raise ValueError(f"QuickJS cmake projection missing {key}")
|
|
return cmake[key]
|
|
|
|
|
|
def embed_body(data: bytes | None) -> str:
|
|
if data is None:
|
|
return (
|
|
"std::span<std::uint8_t const>\n"
|
|
"embeddedQuickJSProvider()\n"
|
|
"{\n"
|
|
" return {};\n"
|
|
"}\n"
|
|
)
|
|
lines = []
|
|
for offset in range(0, len(data), CHUNK):
|
|
chunk = data[offset : offset + CHUNK]
|
|
lines.append(' "' + "".join(f"\\x{b:02x}" for b in chunk) + '"')
|
|
return (
|
|
"namespace {\n"
|
|
"\n"
|
|
"constexpr char const sealedProvider[] =\n"
|
|
+ "\n".join(lines)
|
|
+ ";\n"
|
|
"\n"
|
|
"static_assert(\n"
|
|
f" sizeof(sealedProvider) - 1 == {len(data)},\n"
|
|
' "embedded provider size disagrees with its manifest");\n'
|
|
"\n"
|
|
"} // namespace\n"
|
|
"\n"
|
|
"std::span<std::uint8_t const>\n"
|
|
"embeddedQuickJSProvider()\n"
|
|
"{\n"
|
|
" return {\n"
|
|
" reinterpret_cast<std::uint8_t const*>(sealedProvider),\n"
|
|
" sizeof(sealedProvider) - 1};\n"
|
|
"}\n"
|
|
)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--bundle-dir", type=Path, required=True)
|
|
parser.add_argument("--wasmtime-version", required=True)
|
|
parser.add_argument("--output", type=Path, required=True)
|
|
args = parser.parse_args()
|
|
|
|
bundle = args.bundle_dir
|
|
profile_path = bundle / "jshookz_provider.manifest.json"
|
|
cmake_path = bundle / "jshookz_provider.manifest.cmake"
|
|
native_path = bundle / "jshookz_provider.native-abi.json"
|
|
for path in (profile_path, cmake_path, native_path):
|
|
if not path.is_file():
|
|
print(f"missing sealed QuickJS bundle file: {path}", file=sys.stderr)
|
|
return 1
|
|
|
|
cmake = parse_cmake(cmake_path)
|
|
schema = require(cmake, "MANIFEST_SCHEMA")
|
|
if schema != "xahau.quickjs.runtime-profile-lock.v1":
|
|
print(f"unsupported QuickJS provider manifest schema: {schema}", file=sys.stderr)
|
|
return 1
|
|
|
|
actual_manifest = hashlib.sha256(profile_path.read_bytes()).hexdigest()
|
|
if actual_manifest != require(cmake, "MANIFEST_SHA256"):
|
|
print(
|
|
"QuickJS provider JSON manifest does not match its CMake projection",
|
|
file=sys.stderr,
|
|
)
|
|
return 1
|
|
|
|
actual_native = hashlib.sha256(native_path.read_bytes()).hexdigest()
|
|
if actual_native != require(cmake, "NATIVE_ABI_SHA256"):
|
|
print(
|
|
"QuickJS native ABI snapshot does not match its sealed digest",
|
|
file=sys.stderr,
|
|
)
|
|
return 1
|
|
|
|
pinned_wasmtime = require(cmake, "WASMTIME_VERSION")
|
|
if args.wasmtime_version != pinned_wasmtime:
|
|
print(
|
|
f"QuickJS provider requires Wasmtime {pinned_wasmtime}, "
|
|
f"but CMake resolved {args.wasmtime_version}",
|
|
file=sys.stderr,
|
|
)
|
|
return 1
|
|
|
|
profile = json.loads(profile_path.read_text())
|
|
native = json.loads(native_path.read_text())
|
|
expected_count = int(require(cmake, "PROVIDER_IMPORT_COUNT"))
|
|
provider_imports = profile["provider"]["imports"]
|
|
native_imports = native["selected"]
|
|
provider_names = [item["name"] for item in provider_imports]
|
|
native_names = [item["name"] for item in native_imports]
|
|
if (
|
|
len(provider_imports) != expected_count
|
|
or len(native_imports) != expected_count
|
|
or len(set(provider_names)) != expected_count
|
|
or len(set(native_names)) != expected_count
|
|
or set(provider_names) != set(native_names)
|
|
):
|
|
print("QuickJS provider/native ABI import sets differ", file=sys.stderr)
|
|
return 1
|
|
|
|
wasm_name = require(cmake, "PROVIDER_FILE")
|
|
wasm_path = bundle / wasm_name
|
|
expected_sha = require(cmake, "PROVIDER_SHA256").lower()
|
|
expected_size = int(require(cmake, "PROVIDER_SIZE"))
|
|
fingerprint = hashlib.sha256()
|
|
for path in (profile_path, cmake_path, native_path):
|
|
fingerprint.update(path.read_bytes())
|
|
fingerprint.update(args.wasmtime_version.encode())
|
|
wasm_bytes: bytes | None = None
|
|
if wasm_path.is_file():
|
|
wasm_bytes = wasm_path.read_bytes()
|
|
digest = hashlib.sha256(wasm_bytes).hexdigest()
|
|
if digest != expected_sha or len(wasm_bytes) != expected_size:
|
|
print(
|
|
"QuickJS provider WASM does not match its generated manifest",
|
|
file=sys.stderr,
|
|
)
|
|
return 1
|
|
fingerprint.update(b"wasm")
|
|
fingerprint.update(bytes.fromhex(digest))
|
|
provenance = f"{wasm_path} sha256={digest} size={len(wasm_bytes)}"
|
|
else:
|
|
fingerprint.update(b"absent")
|
|
provenance = "no provider binary was present in the bundle directory"
|
|
|
|
stamp_path = args.output.with_name(args.output.name + ".stamp")
|
|
stamp = fingerprint.hexdigest()
|
|
if (
|
|
args.output.is_file()
|
|
and stamp_path.is_file()
|
|
and stamp_path.read_text() == stamp
|
|
):
|
|
return 0
|
|
|
|
if wasm_bytes is None:
|
|
print(
|
|
f"No QuickJS provider WASM at {wasm_path}; nothing is embedded "
|
|
"and the daemon will not register a QuickJS runtime",
|
|
file=sys.stderr,
|
|
)
|
|
|
|
provider_rows = ",\n ".join(
|
|
"{"
|
|
+ ", ".join(
|
|
(
|
|
quote(item["module"]),
|
|
quote(item["name"]),
|
|
quote(",".join(item["params"])),
|
|
quote(",".join(item["results"])),
|
|
)
|
|
)
|
|
+ "}"
|
|
for item in provider_imports
|
|
)
|
|
native_rows = ",\n ".join(
|
|
"{"
|
|
+ ", ".join(
|
|
(
|
|
quote(item["name"]),
|
|
quote(item["return_type"]),
|
|
quote(",".join(item["param_types"])),
|
|
quote(item["amendment"] or ""),
|
|
)
|
|
)
|
|
+ "}"
|
|
for item in native_imports
|
|
)
|
|
source = native["source"]
|
|
if profile["provider"]["sha256"].lower() != expected_sha:
|
|
print("provider sha256 disagrees between JSON lock and cmake", file=sys.stderr)
|
|
return 1
|
|
if int(profile["provider"]["size"]) != expected_size:
|
|
print("provider size disagrees between JSON lock and cmake", file=sys.stderr)
|
|
return 1
|
|
|
|
text = f"""// Generated by cmake/GenerateQuickJSProviderBundle.py; do not edit.
|
|
// Source: {provenance}
|
|
#include <xrpld/app/hook/QuickJSHookRuntime.h>
|
|
#include <xrpld/app/hook/detail/QuickJSProviderProfile.h>
|
|
#include <array>
|
|
#include <cstdint>
|
|
#include <span>
|
|
#include <string_view>
|
|
|
|
namespace hook::artifact::generated {{
|
|
|
|
std::array<std::uint8_t, 32> const providerSHA256 = {{
|
|
{hex_bytes(expected_sha)}}};
|
|
std::array<std::uint8_t, 32> const bytecodeABI = {{
|
|
{hex_bytes(require(cmake, "BYTECODE_ABI_ID"))}}};
|
|
std::array<std::uint8_t, 32> const runtimeProfile = {{
|
|
{hex_bytes(require(cmake, "RUNTIME_PROFILE_ID"))}}};
|
|
std::size_t const providerSize = {expected_size};
|
|
std::uint16_t const hookApiVersion = {require(cmake, "HOOK_API_VERSION")};
|
|
std::uint64_t const initializationFuel = {require(cmake, "INITIALIZATION_FUEL")}ULL;
|
|
std::uint64_t const invocationFuel = {require(cmake, "INVOCATION_FUEL")}ULL;
|
|
std::string_view const hostWorkMeter = {quote(require(cmake, "HOST_WORK_METER"))};
|
|
std::uint64_t const hostWorkBudget = {require(cmake, "HOST_WORK_BUDGET")}ULL;
|
|
std::uint64_t const hostWorkBasePerCall = {require(cmake, "HOST_WORK_BASE_PER_CALL")}ULL;
|
|
std::uint64_t const hostWorkPerAddressedByte =
|
|
{require(cmake, "HOST_WORK_PER_ADDRESSED_BYTE")}ULL;
|
|
std::string_view const hostAdapterPolicy = {quote(require(cmake, "HOST_ADAPTER_POLICY"))};
|
|
std::uint32_t const heapBytes = {require(cmake, "HEAP_BYTES")}U;
|
|
std::uint32_t const stackBytes = {require(cmake, "STACK_BYTES")}U;
|
|
|
|
namespace {{
|
|
|
|
std::string_view const providerImportNames[] = {{
|
|
{", ".join(quote(name) for name in provider_names)}}};
|
|
ProviderImportSignature const providerImportSignatureData[] = {{
|
|
{provider_rows}}};
|
|
NativeImportSignature const nativeImportSignatureData[] = {{
|
|
{native_rows}}};
|
|
|
|
}} // namespace
|
|
|
|
std::span<std::string_view const> const providerImports{{providerImportNames}};
|
|
std::span<ProviderImportSignature const> const providerImportSignatures{{
|
|
providerImportSignatureData}};
|
|
std::span<NativeImportSignature const> const nativeImportSignatures{{
|
|
nativeImportSignatureData}};
|
|
std::string_view const nativeABISourceRepository = {quote(source["repository"])};
|
|
std::string_view const nativeABISourceCommit = {quote(source["commit"])};
|
|
std::string_view const nativeABISourcePath = {quote(source["path"])};
|
|
std::string_view const nativeABISHA256 = {quote(actual_native)};
|
|
std::size_t const nativeABICatalogueCount = {source["macro_function_count"]};
|
|
|
|
}} // namespace hook::artifact::generated
|
|
|
|
namespace hook {{
|
|
|
|
{embed_body(wasm_bytes)}
|
|
}} // namespace hook
|
|
"""
|
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
args.output.write_text(text)
|
|
stamp_path.write_text(stamp)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|