shim/trace: JSONL emitter (byte-exact port), tracer, snapshot/diff, Hook<Descriptor> template, self-test; ctest 7/7 + tracecmp exit codes
This commit is contained in:
parent
66d4bdaf9d
commit
23696875cc
22 changed files with 3269 additions and 14 deletions
|
|
@ -22,6 +22,18 @@ add_subdirectory(src/mars/parse) # brace-block + .effect readers (lib mars_p
|
|||
add_subdirectory(src/mars/text) # flat-kv, id-manifest, csv (lib mars_text)
|
||||
add_subdirectory(src/game/sim) # strategic formulas, pure (lib sots_game_sim)
|
||||
|
||||
# ---- shim trace/compare infrastructure (host-testable; linked into binkw32) ----
|
||||
add_library(shim_trace STATIC
|
||||
src/shim/trace/sha256.cpp
|
||||
src/shim/trace/emitter.cpp
|
||||
src/shim/trace/tracer.cpp
|
||||
src/shim/trace/selftest.cpp)
|
||||
target_include_directories(shim_trace PUBLIC ${CMAKE_SOURCE_DIR}/src)
|
||||
target_compile_options(shim_trace PRIVATE -Wall -Wextra -Werror)
|
||||
if(MINGW)
|
||||
target_compile_definitions(shim_trace PUBLIC __USE_MINGW_ANSI_STDIO=1) # C99 %lld/%.17g
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
# ---- shim: proxy binkw32.dll that the original game loads (Phase 2 frontend) ----
|
||||
add_library(minhook STATIC
|
||||
|
|
@ -32,7 +44,7 @@ if(WIN32)
|
|||
target_include_directories(minhook PUBLIC third_party/minhook/include)
|
||||
|
||||
add_library(binkw32 SHARED src/shim/main.cpp src/shim/binkw32.def)
|
||||
target_link_libraries(binkw32 PRIVATE minhook sots_addresses)
|
||||
target_link_libraries(binkw32 PRIVATE minhook sots_addresses shim_trace)
|
||||
target_compile_definitions(binkw32 PRIVATE
|
||||
SHIM_BUILD_ID="${SHIM_BUILD_ID}"
|
||||
SOTS_ADDR_PROVENANCE="${SOTS_ADDR_PROVENANCE}")
|
||||
|
|
@ -44,7 +56,7 @@ else()
|
|||
add_executable(addr_smoke tests/addr_smoke.cpp)
|
||||
target_link_libraries(addr_smoke PRIVATE sots_addresses)
|
||||
add_test(NAME addr_smoke COMMAND addr_smoke)
|
||||
foreach(_t mars_parse mars_text game_sim)
|
||||
foreach(_t mars_parse mars_text game_sim shim_trace)
|
||||
if(EXISTS ${CMAKE_SOURCE_DIR}/tests/${_t}/CMakeLists.txt)
|
||||
add_subdirectory(tests/${_t})
|
||||
endif()
|
||||
|
|
|
|||
193
docs/shim-trace.md
Normal file
193
docs/shim-trace.md
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
# Shim trace / compare infrastructure (`src/shim/trace/`)
|
||||
|
||||
Hooks installed by the proxy `binkw32.dll` can run in one of four **modes** and write a
|
||||
JSON-Lines log that the harness in `sots-re/verify/harness/compare/` (`tracecmp.py`) validates
|
||||
and diffs. The wire format is `TRACE_FORMAT.md` (v1) in that directory; the C++ emitter here is
|
||||
a byte-exact port of its reference emitter (`mkfixture.py`) and is tested against it.
|
||||
|
||||
| file | what |
|
||||
|---|---|
|
||||
| `platform.h` | mutex (CRITICAL_SECTION / pthread), monotonic µs, thread id, UTC stamp, `SHIM_CDECL`/`SHIM_STDCALL` |
|
||||
| `sha256.{h,cpp}` | FIPS 180-4 SHA-256, no allocation |
|
||||
| `emitter.{h,cpp}` | typed values (`Tv`), `Record`, `Meta`, `Buf`, `emit_record`/`emit_meta` (spec §3–5) |
|
||||
| `tracer.{h,cpp}` | `Config` (shim.cfg keys), `Tracer` (locked writer, call ids, depth), `Region`/`Snapshot`/`Scratch`, `diff_tv`/`diff_outputs` (spec §6) |
|
||||
| `hook.h` | `Hook<Descriptor>`: the mode dispatch around a hooked C function |
|
||||
| `selftest.{h,cpp}` | worked example hook over the shim's own `Fill()`; run once per launch |
|
||||
|
||||
Host tests: `tests/shim_trace/` (ctest `shim_trace_*`). They build the same sources with
|
||||
`-Wall -Wextra -Werror` on Linux; the shim cross-build links the same static library.
|
||||
|
||||
## Modes
|
||||
|
||||
| mode | what runs | what is logged | caller gets |
|
||||
|---|---|---|---|
|
||||
| `off` | original | nothing | original's result |
|
||||
| `trace` | original | `args`, `ret`, `side` (before/after of every declared region) | original's result |
|
||||
| `compare` | original, then **ours on a copy of the pre-call state** | trace fields + `ours{ret,side}`, `diverged`, `diff` | original's result |
|
||||
| `replace` | ours | nothing | ours' result |
|
||||
|
||||
`compare` never lets ours touch game memory: the declared regions are snapshotted *before* the
|
||||
original runs, those snapshots are copied into scratch buffers, the arguments are rebound onto
|
||||
the scratch buffers (`rebind`), and ours runs there. The diff compares the original's `after`
|
||||
with the scratch buffers' contents and both return values.
|
||||
|
||||
A throw from ours, or from any describe/regions/rebind callback, is caught inside the hook and
|
||||
becomes `err` on the record (which the harness counts as a divergence). The original is always
|
||||
invoked in `off`/`trace`/`compare`, whatever happened during capture. Nothing propagates across
|
||||
the hook boundary.
|
||||
|
||||
## Configuration (`shim.cfg`, next to the DLL)
|
||||
|
||||
```
|
||||
hooks=trace # default mode for every hook; off = install nothing at all
|
||||
hook.Game::CfgVar_RegisterKey=compare # per-hook override, keyed by the record's "hook" name
|
||||
hook.Shim::SelfTest::Fill=off
|
||||
trace.path=C:\SOTS\shim.trace.jsonl # default: shim.trace.jsonl beside the DLL; overwritten per run
|
||||
trace.inline_max=256 # regions <= this many bytes are logged as hex, else sha256 + 32-byte head
|
||||
trace.flush=always # always (default) | lazy (flush only on divergence / err / exit)
|
||||
```
|
||||
|
||||
Unusable values are logged to `shim.log` (`config: key=value rejected (...)`) and ignored.
|
||||
`hooks=off` keeps M0 behaviour: no MinHook, no trace file. If the trace file cannot be opened,
|
||||
every template hook reports mode `off` (the game runs un-instrumented, never half-instrumented).
|
||||
|
||||
The M0 `Mars::Application::Initialize` hook is an asm stub, not a template hook: it is installed
|
||||
whenever `hooks` is not `off`, and it only logs to `shim.log`. See "thiscall" below.
|
||||
|
||||
## Log
|
||||
|
||||
`shim.trace.jsonl`: line 1 is the `meta` record (`build`, `exe_sha256` of the running exe,
|
||||
`started`, `inline_max`, and a `hooks` policy map with one entry per registered template hook);
|
||||
every later line is one call record. `call_id` is process-global and taken *before* the original
|
||||
runs, so nested hooks get higher ids and `depth` says who is inside whom. `ts` is microseconds
|
||||
since the tracer opened. The file is ASCII, LF-terminated; a crash mid-run leaves at most one
|
||||
truncated last line, which the harness reports as one invalid record (`--skip-invalid` drops it).
|
||||
|
||||
`shim.log` gets one `trace:` line at startup (path, default mode) and one `selftest:` line
|
||||
(the self-test hook's mode and checksum), and `shutdown: trace records=N` on detach.
|
||||
|
||||
## Declaring a hook
|
||||
|
||||
A hook is a descriptor struct; `Hook<D>` generates the detour. Everything the template needs is
|
||||
static, so a descriptor is a header-only declaration plus a few small functions.
|
||||
|
||||
```cpp
|
||||
#include "shim/trace/hook.h"
|
||||
using namespace shim::trace;
|
||||
|
||||
struct CfgVarRegisterKeyHook {
|
||||
static constexpr const char* name = "Game::CfgVar_RegisterKey"; // the record's "hook"
|
||||
static constexpr CallConv conv = CallConv::Cdecl; // Cdecl | Stdcall
|
||||
using Ret = bool; // void is allowed
|
||||
using Args = std::tuple<CfgTable*, const char*, int>; // the parameter list
|
||||
|
||||
// inputs, in declaration order (never compared by the harness; use .named("x"))
|
||||
static void describe_args(std::vector<Tv>& out, CfgTable* t, const char* key, int value) {
|
||||
out.push_back(tv::ptr(t).named("table"));
|
||||
out.push_back(tv::str(key).named("key")); // raw cp1252 bytes; nullptr -> ptr 0x0
|
||||
out.push_back(tv::i32(value).named("value"));
|
||||
}
|
||||
static Tv describe_ret(bool r) { return tv::boolean(r); } // omit when Ret is void
|
||||
|
||||
// declared side-effect regions: name, address, size, optional structured describer
|
||||
static void regions(std::vector<Region>& out, CfgTable* t, const char*, int) {
|
||||
out.push_back(Region{"cfg_table", t, sizeof(CfgTable), &describe_table});
|
||||
}
|
||||
static Tv describe_table(const void* p, std::size_t, unsigned /*inline_max*/) {
|
||||
const CfgTable* t = static_cast<const CfgTable*>(p); // a *copy* of the region
|
||||
Tv s = tv::struct_();
|
||||
s.add("count", tv::u32(t->count));
|
||||
return s; // diffs then name the field
|
||||
}
|
||||
|
||||
// the same call, aimed at the scratch copies (regions in declaration order)
|
||||
static Args rebind(Scratch& s, CfgTable*, const char* key, int value) {
|
||||
return Args(s.as<CfgTable>(0), key, value);
|
||||
}
|
||||
|
||||
static bool ours(CfgTable* t, const char* key, int value); // the reimplementation
|
||||
static HookPolicy policy() { return HookPolicy{}; } // ftol / ftol_kind / ptr_exact / unordered
|
||||
};
|
||||
```
|
||||
|
||||
Install (in `InstallHooks`, after `Tracer::open`):
|
||||
|
||||
```cpp
|
||||
using H = Hook<CfgVarRegisterKeyHook>;
|
||||
H::register_policy(tracer); // BEFORE tracer.open(): lands in meta.hooks
|
||||
...
|
||||
H::configure(tracer); // reads hook.<name> / hooks from the config
|
||||
MH_CreateHook(target, reinterpret_cast<void*>(H::detour()), reinterpret_cast<void**>(&H::original));
|
||||
MH_EnableHook(target);
|
||||
```
|
||||
|
||||
Rules of the road:
|
||||
|
||||
- **Regions are copies.** `describe` callbacks receive the snapshot buffer, not live memory; a
|
||||
region that contains pointers into other memory is only compared by the pointer values
|
||||
(ignored by default policy) unless you declare the pointed-to memory as another region.
|
||||
- **Region sizes must be known at call time.** A region whose length is only known after the
|
||||
call cannot be snapshotted "before"; declare an upper bound or split the hook.
|
||||
- **Prefer `struct` describers** over raw `bytes` for anything with fields: the harness then
|
||||
points at `side.cfg_table.after.v.count` instead of a byte offset.
|
||||
- **Floats**: `tv::f32` stores at float32 width and prints `%.9g`; the diff rounds both sides
|
||||
to float32 first (spec §6). Never describe a `float` as `f64`.
|
||||
- **Big ints**: `i64`/`u64` at or beyond 2^53 in magnitude are emitted as decimal strings.
|
||||
- **`json` values** are your own canonical text (spec §7); the shim compares them as text.
|
||||
- **Policies** only affect the shim's advisory `diff`/`diverged`; `tracecmp.py` recomputes from
|
||||
the meta policy plus CLI flags and warns when the shim's verdict disagrees.
|
||||
- **Re-entrancy**: a hook calling into another hooked function is fine (depth, ids). A hook
|
||||
calling *itself* through ours in compare mode would recurse into the tracer; do not do that.
|
||||
- **Cost**: trace/compare allocate (Tv trees, snapshots). Fine for M1-scale hooks; do not put
|
||||
a per-frame hot path in `trace` without thinking about `trace.flush=lazy`.
|
||||
|
||||
### thiscall / unverified prototypes
|
||||
|
||||
The template's detour is a real C++ function with a fixed prototype. That is only safe when the
|
||||
prototype is known. M0 proved that a C++ wrapper around an `[unverified]` `thiscall` corrupts
|
||||
the game (`docs/M0.md`, gotcha 1). So:
|
||||
|
||||
- `__thiscall` hooks and any hook whose entry in `sots_addresses.h` is `[unverified]` stay
|
||||
**asm stubs**: `pushfl/pushal`, call a C logger with ECX, `popal/popfl`, `jmp` trampoline.
|
||||
They are trace-only and cannot capture the return value. Their logger may still build a
|
||||
`Record` and hand it to `Tracer::write` (mode `trace`, `ret` null, regions snapshotted by hand).
|
||||
- Once a prototype is verified, `thiscall` can be adapted to the template by declaring the
|
||||
detour as a `Stdcall` hook that takes `this` as its first explicit parameter behind a tiny asm
|
||||
thunk (`push ecx` then jump) — not provided here; do it only with a verified prototype.
|
||||
- `cdecl`/`stdcall` with verified prototypes use the template directly (`CallConv::Cdecl`,
|
||||
`CallConv::Stdcall`).
|
||||
|
||||
## Running a report
|
||||
|
||||
```
|
||||
# on the game VM: play; then copy C:\SOTS\shim.trace.jsonl to verify/traces/<run-tag>.jsonl
|
||||
python3 verify/harness/compare/tracecmp.py verify/traces/m1-cfgvar-20260907.jsonl \
|
||||
--json-out verify/results/compare/m1-cfgvar-20260907.json
|
||||
# exit 0 clean, 1 divergences, 2 invalid record(s) (--skip-invalid tolerates a truncated tail)
|
||||
# --hook NAME one hook only
|
||||
# --tolerance NAME=abs:1e-6 / rel:1e-5 / ulp:2 float policy (overrides meta.hooks)
|
||||
# --ptr exact compare pointer values too
|
||||
# --replay IMPL.jsonl offline: a golden trace vs a host implementation's {call_id, ret, side} lines
|
||||
```
|
||||
|
||||
## Host tests
|
||||
|
||||
```
|
||||
cmake -S . -B build-host -G Ninja -DCMAKE_BUILD_TYPE=Debug # or: cmake --preset host
|
||||
cmake --build build-host && ctest --test-dir build-host
|
||||
```
|
||||
|
||||
- `shim_trace_sha256` — FIPS vectors, chunked updates, padding edges.
|
||||
- `shim_trace_emitter` — golden strings for §4 escaping, number forms, bytes inline/head, records,
|
||||
meta; writes `out/emitter_oracle.jsonl`, then `tests/shim_trace/oracle_emit.py` rebuilds the
|
||||
same records with `mkfixture.emit_record` and compares byte for byte, and `tracecmp.py` must
|
||||
read it back (exit 1: it carries one injected divergence).
|
||||
- `shim_trace_diff` — every §6 rule (type, ptr, nan/inf, f32 rounding, abs/rel/ulp, bytes
|
||||
offset, set vs list, unordered policy, struct missing/extra, side names), snapshots, config.
|
||||
- `shim_trace_hook` — the self-test hook through every mode: clean log → `tracecmp` exit 0,
|
||||
wrong/throwing ours → exit 1 (and exit 0 with `--hook` on the clean hook), truncated file →
|
||||
exit 2 / 0 with `--skip-invalid`, plus a second thread and the startup `run_once` path.
|
||||
|
||||
The Python steps use `/usr/bin/python3` and the harness dir from `-DSOTS_TRACECMP_DIR` (default
|
||||
`$HOME/sots-re/verify/harness/compare`) or the `SOTS_TRACECMP_DIR` environment variable; when
|
||||
`tracecmp.py` is not there they print `SKIP` and the test still passes.
|
||||
|
|
@ -11,9 +11,13 @@
|
|||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#include "MinHook.h"
|
||||
#include "generated/sots_addresses.h"
|
||||
#include "shim/trace/hook.h"
|
||||
#include "shim/trace/selftest.h"
|
||||
#include "shim/trace/tracer.h"
|
||||
|
||||
namespace {
|
||||
|
||||
|
|
@ -37,21 +41,27 @@ void Log(const char* fmt, ...) {
|
|||
}
|
||||
|
||||
// ---- config (shim.cfg next to the DLL; "key=value" per line) ------------------------------
|
||||
//
|
||||
// `hooks=`, `hook.<Name>=`, `trace.*` are owned by the tracer (src/shim/trace/tracer.h);
|
||||
// `hooks=off` additionally means "install nothing" (the M0 asm-stub hook included).
|
||||
|
||||
struct Config {
|
||||
bool hooks = true; // hooks=trace (default) | off
|
||||
bool hooks = true; // hooks != off
|
||||
shim::trace::Config trace;
|
||||
};
|
||||
|
||||
Config ReadConfig() {
|
||||
Config cfg;
|
||||
char path[MAX_PATH];
|
||||
std::snprintf(path, sizeof path, "%s\\shim.trace.jsonl", g_dir);
|
||||
cfg.trace.path = path;
|
||||
std::snprintf(path, sizeof path, "%s\\shim.cfg", g_dir);
|
||||
FILE* f = std::fopen(path, "r");
|
||||
if (!f) {
|
||||
Log("config: %s not found, using defaults", path);
|
||||
return cfg;
|
||||
}
|
||||
char line[256];
|
||||
char line[512];
|
||||
while (std::fgets(line, sizeof line, f)) {
|
||||
char* p = line;
|
||||
while (*p == ' ' || *p == '\t') ++p;
|
||||
|
|
@ -61,14 +71,16 @@ Config ReadConfig() {
|
|||
*eq = '\0';
|
||||
char* val = eq + 1;
|
||||
val[std::strcspn(val, "\r\n")] = '\0';
|
||||
if (std::strcmp(p, "hooks") == 0) {
|
||||
cfg.hooks = std::strcmp(val, "off") != 0;
|
||||
Log("config: hooks=%s", val);
|
||||
std::string err;
|
||||
if (cfg.trace.apply(p, val, &err)) {
|
||||
if (!err.empty()) Log("config: %s=%s rejected (%s)", p, val, err.c_str());
|
||||
else Log("config: %s=%s", p, val);
|
||||
} else {
|
||||
Log("config: ignoring unknown key '%s'", p);
|
||||
}
|
||||
}
|
||||
std::fclose(f);
|
||||
cfg.hooks = cfg.trace.default_mode != shim::trace::Mode::Off;
|
||||
return cfg;
|
||||
}
|
||||
|
||||
|
|
@ -151,14 +163,46 @@ void Shim_Init(HMODULE self) {
|
|||
Log("addresses: %s", SOTS_ADDR_PROVENANCE);
|
||||
|
||||
const Config cfg = ReadConfig();
|
||||
if (cfg.hooks) {
|
||||
InstallHooks();
|
||||
} else {
|
||||
if (!cfg.hooks) {
|
||||
Log("hook: disabled by config");
|
||||
return;
|
||||
}
|
||||
|
||||
// Trace log: register every template hook's policy (goes into the meta line), then open.
|
||||
// Hooks read their mode from the tracer after open(); a tracer that failed to open reports
|
||||
// Off for everything, so the game runs un-instrumented rather than half-instrumented.
|
||||
shim::trace::Tracer& tracer = shim::trace::Tracer::instance();
|
||||
tracer.configure(cfg.trace);
|
||||
shim::trace::Hook<shim::selftest::FillHook>::register_policy(tracer);
|
||||
char exeSha[65] = {};
|
||||
if (!shim::trace::sha256_file(exePath, exeSha)) Log("trace: could not hash %s", exePath);
|
||||
if (tracer.open(SHIM_BUILD_ID, exeSha)) {
|
||||
Log("trace: %s (default mode %s, inline_max %u, flush %s)", cfg.trace.path.c_str(),
|
||||
shim::trace::mode_name(cfg.trace.default_mode), cfg.trace.inline_max,
|
||||
cfg.trace.flush_always ? "always" : "lazy");
|
||||
} else {
|
||||
Log("trace: cannot open %s; template hooks forced off", cfg.trace.path.c_str());
|
||||
}
|
||||
|
||||
InstallHooks();
|
||||
|
||||
// Self-test through the hook template (no MinHook involved): one record per launch proves
|
||||
// the emitter/tracer inside the game process. `hook.Shim::SelfTest::Fill=off` silences it.
|
||||
{
|
||||
using H = shim::trace::Hook<shim::selftest::FillHook>;
|
||||
H::configure(tracer);
|
||||
const unsigned sum = shim::selftest::run_once(H::mode);
|
||||
Log("selftest: %s mode=%s checksum=%08x records=%lu", shim::selftest::FillHook::name,
|
||||
shim::trace::mode_name(H::mode), sum, static_cast<unsigned long>(tracer.records_written()));
|
||||
}
|
||||
}
|
||||
|
||||
void Shim_Shutdown() {
|
||||
shim::trace::Tracer& tracer = shim::trace::Tracer::instance();
|
||||
if (tracer.is_open()) {
|
||||
Log("shutdown: trace records=%lu", static_cast<unsigned long>(tracer.records_written()));
|
||||
tracer.close();
|
||||
}
|
||||
if (g_minhookUp) {
|
||||
MH_STATUS st = MH_Uninitialize();
|
||||
Log("shutdown: MH_Uninitialize -> %s", MH_StatusToString(st));
|
||||
|
|
|
|||
|
|
@ -1,7 +1,21 @@
|
|||
# sots-engine shim configuration. Read once when binkw32.dll (the proxy) loads.
|
||||
# One key=value per line; '#' starts a comment.
|
||||
# One key=value per line; '#' starts a comment. See docs/shim-trace.md.
|
||||
#
|
||||
# hooks = trace | off
|
||||
# trace install the hooks and log them to shim.log (default)
|
||||
# off forward Bink calls only; touch nothing in the exe
|
||||
# hooks = off | trace | compare | replace
|
||||
# default mode for every hook
|
||||
# off forward Bink calls only; touch nothing in the exe (nothing is installed)
|
||||
# trace run the original, log args/ret/side effects to shim.trace.jsonl
|
||||
# compare run the original AND our reimplementation on the same inputs, log both + diff
|
||||
# replace run our reimplementation instead of the original (nothing logged)
|
||||
hooks=trace
|
||||
|
||||
# hook.<Name> = off | trace | compare | replace
|
||||
# per-hook override; <Name> is the hook's record name (the "hook" field in the log)
|
||||
#hook.Shim::SelfTest::Fill=off
|
||||
|
||||
# trace.path = <file> default: shim.trace.jsonl next to the DLL (overwritten each run)
|
||||
# trace.inline_max = <bytes> regions up to this size are logged as hex (default 256)
|
||||
# trace.flush = always | lazy fflush every record (default) or only on divergence/err/exit
|
||||
#trace.path=C:\SOTS\shim.trace.jsonl
|
||||
#trace.inline_max=256
|
||||
#trace.flush=always
|
||||
|
|
|
|||
482
src/shim/trace/emitter.cpp
Normal file
482
src/shim/trace/emitter.cpp
Normal file
|
|
@ -0,0 +1,482 @@
|
|||
#include "shim/trace/emitter.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#include "shim/trace/sha256.h"
|
||||
|
||||
namespace shim::trace {
|
||||
|
||||
// ---- modes ----------------------------------------------------------------------------------
|
||||
|
||||
const char* mode_name(Mode m) {
|
||||
switch (m) {
|
||||
case Mode::Off: return "off";
|
||||
case Mode::Trace: return "trace";
|
||||
case Mode::Compare: return "compare";
|
||||
case Mode::Replace: return "replace";
|
||||
}
|
||||
return "off";
|
||||
}
|
||||
|
||||
bool parse_mode(const char* text, Mode& out) {
|
||||
if (!text) return false;
|
||||
if (std::strcmp(text, "off") == 0) { out = Mode::Off; return true; }
|
||||
if (std::strcmp(text, "trace") == 0) { out = Mode::Trace; return true; }
|
||||
if (std::strcmp(text, "compare") == 0) { out = Mode::Compare; return true; }
|
||||
if (std::strcmp(text, "replace") == 0) { out = Mode::Replace; return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- typed values -----------------------------------------------------------------------------
|
||||
|
||||
const char* Tv::type_name() const {
|
||||
switch (kind) {
|
||||
case Bool: return "bool";
|
||||
case Int: return itype;
|
||||
case Float: return f32 ? "f32" : "f64";
|
||||
case Str: return "str";
|
||||
case WStr: return "wstr";
|
||||
case Ptr: return "ptr";
|
||||
case Enum: return "enum";
|
||||
case Null: return "null";
|
||||
case Bytes: return "bytes";
|
||||
case List: return "list";
|
||||
case Set: return "set";
|
||||
case Struct: return "struct";
|
||||
case Json: return "json";
|
||||
}
|
||||
return "null";
|
||||
}
|
||||
|
||||
Tv& Tv::add(const char* key, Tv v) {
|
||||
keys.emplace_back(key);
|
||||
items.push_back(std::move(v));
|
||||
return *this;
|
||||
}
|
||||
|
||||
namespace tv {
|
||||
|
||||
namespace {
|
||||
Tv make_int(const char* t, long long v) {
|
||||
Tv r;
|
||||
r.kind = Tv::Int;
|
||||
r.itype = t;
|
||||
r.i = v;
|
||||
return r;
|
||||
}
|
||||
Tv make_uint(const char* t, unsigned long long v) {
|
||||
Tv r;
|
||||
r.kind = Tv::Int;
|
||||
r.itype = t;
|
||||
r.is_unsigned = true;
|
||||
r.u = v;
|
||||
return r;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Tv boolean(bool v) { Tv r; r.kind = Tv::Bool; r.b = v; return r; }
|
||||
Tv i8(std::int8_t v) { return make_int("i8", v); }
|
||||
Tv i16(std::int16_t v) { return make_int("i16", v); }
|
||||
Tv i32(std::int32_t v) { return make_int("i32", v); }
|
||||
Tv i64(std::int64_t v) { return make_int("i64", v); }
|
||||
Tv u8(std::uint8_t v) { return make_uint("u8", v); }
|
||||
Tv u16(std::uint16_t v) { return make_uint("u16", v); }
|
||||
Tv u32(std::uint32_t v) { return make_uint("u32", v); }
|
||||
Tv u64(std::uint64_t v) { return make_uint("u64", v); }
|
||||
Tv f32(float v) { Tv r; r.kind = Tv::Float; r.f32 = true; r.f = v; return r; }
|
||||
Tv f64(double v) { Tv r; r.kind = Tv::Float; r.f = v; return r; }
|
||||
|
||||
Tv str(const char* s) {
|
||||
if (!s) return ptr(static_cast<std::uintptr_t>(0));
|
||||
return str(s, std::strlen(s));
|
||||
}
|
||||
Tv str(const void* bytes, std::size_t n) {
|
||||
Tv r;
|
||||
r.kind = Tv::Str;
|
||||
r.s.assign(static_cast<const char*>(bytes), n);
|
||||
return r;
|
||||
}
|
||||
Tv wstr(const std::uint16_t* units, std::size_t n) {
|
||||
Tv r;
|
||||
r.kind = Tv::WStr;
|
||||
r.w.assign(units, units + n);
|
||||
return r;
|
||||
}
|
||||
Tv wstr(const wchar_t* s) {
|
||||
if (!s) return ptr(static_cast<std::uintptr_t>(0));
|
||||
Tv r;
|
||||
r.kind = Tv::WStr;
|
||||
for (; *s; ++s) r.w.push_back(static_cast<std::uint16_t>(*s));
|
||||
return r;
|
||||
}
|
||||
Tv ptr(const void* p) { return ptr(reinterpret_cast<std::uintptr_t>(p)); }
|
||||
Tv ptr(std::uintptr_t p) { Tv r; r.kind = Tv::Ptr; r.p = p; return r; }
|
||||
Tv enum_(long long v, const char* symbolic) {
|
||||
Tv r;
|
||||
r.kind = Tv::Enum;
|
||||
r.i = v;
|
||||
if (symbolic) r.s = symbolic;
|
||||
return r;
|
||||
}
|
||||
Tv null() { return Tv(); }
|
||||
|
||||
Tv bytes(const void* data, std::size_t n, std::size_t inline_max) {
|
||||
Tv r;
|
||||
r.kind = Tv::Bytes;
|
||||
r.blen = n;
|
||||
Sha256::digest_hex(data, n, r.sha);
|
||||
const std::uint8_t* p = static_cast<const std::uint8_t*>(data);
|
||||
if (n <= inline_max) {
|
||||
r.inline_full = true;
|
||||
r.data.assign(p, p + n);
|
||||
} else {
|
||||
r.data.assign(p, p + 32);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
Tv list(std::vector<Tv> items) { Tv r; r.kind = Tv::List; r.items = std::move(items); return r; }
|
||||
Tv set(std::vector<Tv> items) { Tv r; r.kind = Tv::Set; r.items = std::move(items); return r; }
|
||||
Tv struct_() { Tv r; r.kind = Tv::Struct; return r; }
|
||||
Tv json(const char* canonical_text) { Tv r; r.kind = Tv::Json; r.s = canonical_text; return r; }
|
||||
|
||||
} // namespace tv
|
||||
|
||||
// ---- output buffer --------------------------------------------------------------------------------
|
||||
|
||||
Buf::Buf() { grow(4096); }
|
||||
Buf::~Buf() { std::free(data_); }
|
||||
|
||||
bool Buf::grow(std::size_t need) {
|
||||
if (need <= cap_) return true;
|
||||
std::size_t ncap = cap_ ? cap_ : 4096;
|
||||
while (ncap < need) ncap *= 2;
|
||||
char* nd = static_cast<char*>(std::realloc(data_, ncap));
|
||||
if (!nd) {
|
||||
overflow_ = true;
|
||||
return false;
|
||||
}
|
||||
data_ = nd;
|
||||
cap_ = ncap;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Buf::put(char c) {
|
||||
if (len_ + 1 > cap_ && !grow(len_ + 1)) return;
|
||||
data_[len_++] = c;
|
||||
}
|
||||
|
||||
void Buf::put(const void* bytes, std::size_t n) {
|
||||
if (!n) return;
|
||||
if (len_ + n > cap_ && !grow(len_ + n)) return;
|
||||
std::memcpy(data_ + len_, bytes, n);
|
||||
len_ += n;
|
||||
}
|
||||
|
||||
void Buf::puts(const char* s) { put(s, std::strlen(s)); }
|
||||
|
||||
void Buf::printf(const char* fmt, ...) {
|
||||
char tmp[128];
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
const int n = std::vsnprintf(tmp, sizeof tmp, fmt, ap);
|
||||
va_end(ap);
|
||||
if (n < 0) {
|
||||
overflow_ = true;
|
||||
return;
|
||||
}
|
||||
if (static_cast<std::size_t>(n) < sizeof tmp) {
|
||||
put(tmp, static_cast<std::size_t>(n));
|
||||
return;
|
||||
}
|
||||
if (!grow(len_ + static_cast<std::size_t>(n) + 1)) return;
|
||||
va_start(ap, fmt);
|
||||
std::vsnprintf(data_ + len_, static_cast<std::size_t>(n) + 1, fmt, ap);
|
||||
va_end(ap);
|
||||
len_ += static_cast<std::size_t>(n);
|
||||
}
|
||||
|
||||
void Buf::clear() {
|
||||
len_ = 0;
|
||||
overflow_ = false;
|
||||
}
|
||||
|
||||
// ---- escaping / numbers -------------------------------------------------------------------------
|
||||
|
||||
namespace {
|
||||
|
||||
const char* const kHex = "0123456789abcdef";
|
||||
|
||||
inline void put_u_escape(Buf& out, unsigned unit) {
|
||||
char e[6] = {'\\', 'u', kHex[(unit >> 12) & 15], kHex[(unit >> 8) & 15], kHex[(unit >> 4) & 15], kHex[unit & 15]};
|
||||
out.put(e, 6);
|
||||
}
|
||||
|
||||
// One unit of either string flavour (bytes for str, UTF-16 units for wstr): mkfixture.esc().
|
||||
inline void put_unit(Buf& out, unsigned unit) {
|
||||
switch (unit) {
|
||||
case '"': out.put("\\\"", 2); return;
|
||||
case '\\': out.put("\\\\", 2); return;
|
||||
case '\n': out.put("\\n", 2); return;
|
||||
case '\r': out.put("\\r", 2); return;
|
||||
case '\t': out.put("\\t", 2); return;
|
||||
default:
|
||||
if (unit < 0x20 || unit >= 0x7f) put_u_escape(out, unit);
|
||||
else out.put(static_cast<char>(unit));
|
||||
}
|
||||
}
|
||||
|
||||
void put_hex(Buf& out, const std::uint8_t* p, std::size_t n) {
|
||||
for (std::size_t i = 0; i < n; ++i) {
|
||||
out.put(kHex[p[i] >> 4]);
|
||||
out.put(kHex[p[i] & 15]);
|
||||
}
|
||||
}
|
||||
|
||||
const long long kTwo53 = 1LL << 53;
|
||||
|
||||
} // namespace
|
||||
|
||||
void emit_esc(Buf& out, const char* bytes, std::size_t n) {
|
||||
out.put('"');
|
||||
for (std::size_t i = 0; i < n; ++i) put_unit(out, static_cast<unsigned char>(bytes[i]));
|
||||
out.put('"');
|
||||
}
|
||||
|
||||
void emit_esc(Buf& out, const std::string& s) { emit_esc(out, s.data(), s.size()); }
|
||||
|
||||
void emit_esc_w(Buf& out, const std::uint16_t* units, std::size_t n) {
|
||||
out.put('"');
|
||||
for (std::size_t i = 0; i < n; ++i) put_unit(out, units[i]);
|
||||
out.put('"');
|
||||
}
|
||||
|
||||
// Integers above 2^53 in magnitude go out as decimal strings (section 3): mkfixture.u64().
|
||||
void emit_int(Buf& out, long long v) {
|
||||
const bool quote = v >= kTwo53 || v <= -kTwo53;
|
||||
if (quote) out.put('"');
|
||||
out.printf("%lld", v);
|
||||
if (quote) out.put('"');
|
||||
}
|
||||
|
||||
void emit_uint(Buf& out, unsigned long long v) {
|
||||
const bool quote = v >= static_cast<unsigned long long>(kTwo53);
|
||||
if (quote) out.put('"');
|
||||
out.printf("%llu", v);
|
||||
if (quote) out.put('"');
|
||||
}
|
||||
|
||||
// mkfixture.fmt_num() for floats.
|
||||
void emit_float(Buf& out, double v, bool f32) {
|
||||
if (std::isnan(v)) { out.puts("\"nan\""); return; }
|
||||
if (std::isinf(v)) { out.puts(v > 0 ? "\"inf\"" : "\"-inf\""); return; }
|
||||
out.printf(f32 ? "%.9g" : "%.17g", v);
|
||||
}
|
||||
|
||||
// ---- typed values ----------------------------------------------------------------------------------
|
||||
|
||||
// mkfixture.emit_tv(): {"t":..., ("n","sha256","hex"|"head") | "v":... [,"name":...]} [,"n":...]
|
||||
void emit_tv(Buf& out, const Tv& v) {
|
||||
out.puts("{\"t\":");
|
||||
emit_esc(out, v.type_name(), std::strlen(v.type_name()));
|
||||
if (v.kind == Tv::Bytes) {
|
||||
out.printf(",\"n\":%llu,\"sha256\":\"", static_cast<unsigned long long>(v.blen));
|
||||
out.puts(v.sha);
|
||||
out.puts(v.inline_full ? "\",\"hex\":\"" : "\",\"head\":\"");
|
||||
put_hex(out, v.data.data(), v.data.size());
|
||||
out.puts("\"}");
|
||||
return;
|
||||
}
|
||||
out.puts(",\"v\":");
|
||||
switch (v.kind) {
|
||||
case Tv::Bool: out.puts(v.b ? "true" : "false"); break;
|
||||
case Tv::Int:
|
||||
if (v.is_unsigned) emit_uint(out, v.u);
|
||||
else emit_int(out, v.i);
|
||||
break;
|
||||
case Tv::Float: emit_float(out, v.f, v.f32); break;
|
||||
case Tv::Str: emit_esc(out, v.s); break;
|
||||
case Tv::WStr: emit_esc_w(out, v.w.data(), v.w.size()); break;
|
||||
case Tv::Ptr: out.printf("\"0x%08llx\"", static_cast<unsigned long long>(v.p)); break;
|
||||
case Tv::Enum: out.printf("%lld", v.i); break;
|
||||
case Tv::Null: out.puts("null"); break;
|
||||
case Tv::List:
|
||||
case Tv::Set:
|
||||
out.put('[');
|
||||
for (std::size_t i = 0; i < v.items.size(); ++i) {
|
||||
if (i) out.put(',');
|
||||
emit_tv(out, v.items[i]);
|
||||
}
|
||||
out.put(']');
|
||||
break;
|
||||
case Tv::Struct:
|
||||
out.put('{');
|
||||
for (std::size_t i = 0; i < v.items.size(); ++i) {
|
||||
if (i) out.put(',');
|
||||
emit_esc(out, v.keys[i]);
|
||||
out.put(':');
|
||||
emit_tv(out, v.items[i]);
|
||||
}
|
||||
out.put('}');
|
||||
break;
|
||||
case Tv::Json: out.puts(v.s.c_str()); break;
|
||||
case Tv::Bytes: break; // handled above
|
||||
}
|
||||
if (v.kind == Tv::Enum && !v.s.empty()) {
|
||||
out.puts(",\"name\":");
|
||||
emit_esc(out, v.s);
|
||||
}
|
||||
if (!v.name.empty()) {
|
||||
out.puts(",\"n\":");
|
||||
emit_esc(out, v.name);
|
||||
}
|
||||
out.put('}');
|
||||
}
|
||||
|
||||
std::string canonical(const Tv& v) {
|
||||
Buf b;
|
||||
emit_tv(b, v);
|
||||
return b.str();
|
||||
}
|
||||
|
||||
// ---- records ----------------------------------------------------------------------------------------
|
||||
|
||||
namespace {
|
||||
|
||||
void emit_side(Buf& out, const std::vector<SideEntry>& side) {
|
||||
out.put('{');
|
||||
for (std::size_t i = 0; i < side.size(); ++i) {
|
||||
const SideEntry& e = side[i];
|
||||
if (i) out.put(',');
|
||||
emit_esc(out, e.name);
|
||||
out.puts(":{");
|
||||
if (e.before_kind == SideEntry::IsNull) {
|
||||
out.puts("\"before\":null,");
|
||||
} else if (e.before_kind == SideEntry::Value) {
|
||||
out.puts("\"before\":");
|
||||
emit_tv(out, e.before);
|
||||
out.put(',');
|
||||
}
|
||||
out.puts("\"after\":");
|
||||
emit_tv(out, e.after);
|
||||
out.put('}');
|
||||
}
|
||||
out.put('}');
|
||||
}
|
||||
|
||||
void emit_ours_side(Buf& out, const std::vector<OursSide>& side) {
|
||||
out.put('{');
|
||||
for (std::size_t i = 0; i < side.size(); ++i) {
|
||||
if (i) out.put(',');
|
||||
emit_esc(out, side[i].name);
|
||||
out.puts(":{\"after\":");
|
||||
emit_tv(out, side[i].after);
|
||||
out.put('}');
|
||||
}
|
||||
out.put('}');
|
||||
}
|
||||
|
||||
void emit_opt_tv(Buf& out, const std::optional<Tv>& v) {
|
||||
if (v) emit_tv(out, *v);
|
||||
else out.puts("null");
|
||||
}
|
||||
|
||||
void emit_diff(Buf& out, const std::vector<DiffEntry>& diff) {
|
||||
out.put('[');
|
||||
for (std::size_t i = 0; i < diff.size(); ++i) {
|
||||
const DiffEntry& d = diff[i];
|
||||
if (i) out.put(',');
|
||||
out.puts("{\"path\":");
|
||||
emit_esc(out, d.path);
|
||||
out.puts(",\"why\":");
|
||||
emit_esc(out, d.why, std::strlen(d.why));
|
||||
out.puts(",\"orig\":");
|
||||
if (!d.orig_raw.empty()) out.puts(d.orig_raw.c_str());
|
||||
else emit_opt_tv(out, d.orig);
|
||||
out.puts(",\"ours\":");
|
||||
if (!d.ours_raw.empty()) out.puts(d.ours_raw.c_str());
|
||||
else emit_opt_tv(out, d.ours);
|
||||
if (d.first_diff_offset >= 0) out.printf(",\"first_diff_offset\":%lld", d.first_diff_offset);
|
||||
if (!d.note.empty()) {
|
||||
out.puts(",\"note\":");
|
||||
emit_esc(out, d.note);
|
||||
}
|
||||
out.put('}');
|
||||
}
|
||||
out.put(']');
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// mkfixture.emit_record(): fixed KEY_ORDER, optional keys omitted.
|
||||
void emit_record(Buf& out, const Record& r) {
|
||||
out.printf("{\"ts\":%lld,\"hook\":", static_cast<long long>(r.ts));
|
||||
emit_esc(out, r.hook);
|
||||
out.printf(",\"mode\":\"%s\",\"call_id\":%llu,\"thread\":%llu", mode_name(r.mode),
|
||||
static_cast<unsigned long long>(r.call_id), static_cast<unsigned long long>(r.thread));
|
||||
if (r.depth >= 0) out.printf(",\"depth\":%d", r.depth);
|
||||
out.puts(",\"args\":[");
|
||||
for (std::size_t i = 0; i < r.args.size(); ++i) {
|
||||
if (i) out.put(',');
|
||||
emit_tv(out, r.args[i]);
|
||||
}
|
||||
out.puts("],\"ret\":");
|
||||
emit_opt_tv(out, r.ret);
|
||||
out.puts(",\"side\":");
|
||||
emit_side(out, r.side);
|
||||
if (r.has_ours) {
|
||||
out.puts(",\"ours\":{\"ret\":");
|
||||
emit_opt_tv(out, r.ours_ret);
|
||||
out.puts(",\"side\":");
|
||||
emit_ours_side(out, r.ours_side);
|
||||
out.put('}');
|
||||
}
|
||||
if (r.diverged >= 0) out.puts(r.diverged ? ",\"diverged\":true" : ",\"diverged\":false");
|
||||
if (r.has_diff) {
|
||||
out.puts(",\"diff\":");
|
||||
emit_diff(out, r.diff);
|
||||
}
|
||||
if (r.err) {
|
||||
out.puts(",\"err\":");
|
||||
emit_esc(out, *r.err);
|
||||
}
|
||||
if (r.note) {
|
||||
out.puts(",\"note\":");
|
||||
emit_esc(out, *r.note);
|
||||
}
|
||||
out.puts("}\n");
|
||||
}
|
||||
|
||||
// mkfixture.emit_meta() over the dict shape mkfixture.meta() builds.
|
||||
void emit_meta(Buf& out, const Meta& m) {
|
||||
out.puts("{\"meta\":{\"format\":1,\"build\":");
|
||||
emit_esc(out, m.build);
|
||||
out.puts(",\"exe_sha256\":");
|
||||
emit_esc(out, m.exe_sha256);
|
||||
out.puts(",\"started\":");
|
||||
emit_esc(out, m.started);
|
||||
out.printf(",\"inline_max\":%u,\"hooks\":{", m.inline_max);
|
||||
for (std::size_t i = 0; i < m.hooks.size(); ++i) {
|
||||
const HookPolicy& p = m.hooks[i].second;
|
||||
if (i) out.put(',');
|
||||
emit_esc(out, m.hooks[i].first);
|
||||
out.puts(":{\"ftol\":");
|
||||
emit_float(out, p.ftol, false);
|
||||
out.printf(",\"ftol_kind\":\"%s\",\"ptr\":\"%s\"", p.ftol_kind, p.ptr_exact ? "exact" : "ignore");
|
||||
if (!p.unordered.empty()) {
|
||||
out.puts(",\"unordered\":[");
|
||||
for (std::size_t j = 0; j < p.unordered.size(); ++j) {
|
||||
if (j) out.put(',');
|
||||
emit_esc(out, p.unordered[j]);
|
||||
}
|
||||
out.put(']');
|
||||
}
|
||||
out.put('}');
|
||||
}
|
||||
out.puts("}}}\n");
|
||||
}
|
||||
|
||||
} // namespace shim::trace
|
||||
196
src/shim/trace/emitter.h
Normal file
196
src/shim/trace/emitter.h
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
// JSONL emitter for the trace/compare log (sots-re/verify/harness/compare/TRACE_FORMAT.md, v1).
|
||||
//
|
||||
// A faithful port of the reference emitter in mkfixture.py (esc / fmt_num / emit_tv /
|
||||
// emit_record / emit_meta): fixed key order, ASCII-only escaping, %.9g / %.17g floats,
|
||||
// big integers as decimal strings, bytes as n + sha256 (+ hex inline or a 32-byte head).
|
||||
//
|
||||
// Typed values (`Tv`) are a small tree; building one allocates (std::string / std::vector),
|
||||
// so callers on a hook boundary wrap construction in try/catch (hook.h does). The output
|
||||
// buffer (`Buf`) is malloc-based and never throws: on allocation failure it flags overflow
|
||||
// and drops bytes, and the tracer refuses to write a record whose buffer is not ok().
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace shim::trace {
|
||||
|
||||
// ---- modes ----------------------------------------------------------------------------------
|
||||
|
||||
enum class Mode : std::uint8_t { Off, Trace, Compare, Replace };
|
||||
|
||||
const char* mode_name(Mode m); // "off" | "trace" | "compare" | "replace"
|
||||
bool parse_mode(const char* text, Mode& out); // accepts the same four words
|
||||
|
||||
// ---- typed values -----------------------------------------------------------------------------
|
||||
|
||||
struct Tv {
|
||||
enum Kind : std::uint8_t { Bool, Int, Float, Str, WStr, Ptr, Enum, Null, Bytes, List, Set, Struct, Json };
|
||||
|
||||
Kind kind = Null;
|
||||
const char* itype = "i32"; // Int: "i8".."u64"
|
||||
bool is_unsigned = false; // Int: which of i/u is live
|
||||
bool b = false; // Bool
|
||||
long long i = 0; // Int (signed), Enum
|
||||
unsigned long long u = 0; // Int (unsigned)
|
||||
double f = 0.0; // Float (already rounded to float32 when f32)
|
||||
bool f32 = false; // Float width
|
||||
std::uintptr_t p = 0; // Ptr
|
||||
std::string s; // Str raw bytes | Enum symbolic name ("" = none) | Json canonical text
|
||||
std::vector<std::uint16_t> w; // WStr UTF-16 units
|
||||
// Bytes: length, sha256 hex, and either the full data (inline_full) or the first 32 bytes.
|
||||
std::size_t blen = 0;
|
||||
char sha[65] = {};
|
||||
bool inline_full = false;
|
||||
std::vector<std::uint8_t> data;
|
||||
// List / Set: items. Struct: keys[i] names items[i] (insertion order is emission order).
|
||||
std::vector<Tv> items;
|
||||
std::vector<std::string> keys;
|
||||
std::string name; // optional "n" (never emitted for Bytes)
|
||||
|
||||
const char* type_name() const;
|
||||
Tv&& named(const char* n) && { name = n; return std::move(*this); }
|
||||
Tv& named(const char* n) & { name = n; return *this; }
|
||||
// Struct helper.
|
||||
Tv& add(const char* key, Tv v);
|
||||
};
|
||||
|
||||
namespace tv {
|
||||
Tv boolean(bool v);
|
||||
Tv i8(std::int8_t v);
|
||||
Tv i16(std::int16_t v);
|
||||
Tv i32(std::int32_t v);
|
||||
Tv i64(std::int64_t v);
|
||||
Tv u8(std::uint8_t v);
|
||||
Tv u16(std::uint16_t v);
|
||||
Tv u32(std::uint32_t v);
|
||||
Tv u64(std::uint64_t v);
|
||||
Tv f32(float v);
|
||||
Tv f64(double v);
|
||||
Tv str(const char* s); // NUL-terminated raw bytes (nullptr -> ptr 0x0, see below)
|
||||
Tv str(const void* bytes, std::size_t n); // raw bytes, embedded NULs preserved
|
||||
Tv wstr(const std::uint16_t* units, std::size_t n);
|
||||
Tv wstr(const wchar_t* s); // NUL-terminated; units truncated to 16 bits
|
||||
Tv ptr(const void* p);
|
||||
Tv ptr(std::uintptr_t p);
|
||||
Tv enum_(long long v, const char* symbolic = nullptr);
|
||||
Tv null();
|
||||
Tv bytes(const void* data, std::size_t n, std::size_t inline_max);
|
||||
Tv list(std::vector<Tv> items);
|
||||
Tv set(std::vector<Tv> items);
|
||||
Tv struct_();
|
||||
Tv json(const char* canonical_text); // caller guarantees section-7 canonical JSON
|
||||
} // namespace tv
|
||||
|
||||
// ---- records ------------------------------------------------------------------------------------
|
||||
|
||||
struct SideEntry {
|
||||
std::string name;
|
||||
enum Before : std::uint8_t { Absent, IsNull, Value } before_kind = Absent;
|
||||
Tv before;
|
||||
Tv after;
|
||||
};
|
||||
|
||||
struct OursSide {
|
||||
std::string name;
|
||||
Tv after;
|
||||
};
|
||||
|
||||
struct DiffEntry {
|
||||
std::string path;
|
||||
const char* why = "exact"; // exact | ftol | type | len | missing | extra | hash | err
|
||||
std::optional<Tv> orig;
|
||||
std::optional<Tv> ours;
|
||||
std::string orig_raw; // when non-empty: raw JSON text emitted instead of `orig` (e.g. a length)
|
||||
std::string ours_raw; // same for `ours` (e.g. the quoted err text)
|
||||
long long first_diff_offset = -1; // bytes only, when both inline
|
||||
std::string note; // free text, "" = absent
|
||||
};
|
||||
|
||||
struct Record {
|
||||
std::int64_t ts = 0;
|
||||
std::string hook;
|
||||
Mode mode = Mode::Trace;
|
||||
std::uint32_t call_id = 0;
|
||||
std::uint32_t thread = 0;
|
||||
int depth = -1; // <0 = omit
|
||||
std::vector<Tv> args;
|
||||
std::optional<Tv> ret; // nullopt = void -> null
|
||||
std::vector<SideEntry> side;
|
||||
bool has_ours = false;
|
||||
std::optional<Tv> ours_ret;
|
||||
std::vector<OursSide> ours_side;
|
||||
int diverged = -1; // <0 omit, 0 false, 1 true
|
||||
bool has_diff = false;
|
||||
std::vector<DiffEntry> diff;
|
||||
std::optional<std::string> err;
|
||||
std::optional<std::string> note;
|
||||
};
|
||||
|
||||
// Per-hook policy, recorded in meta.hooks (the keys tracecmp.py --tolerance accepts).
|
||||
struct HookPolicy {
|
||||
double ftol = 0.0;
|
||||
const char* ftol_kind = "abs"; // abs | rel | ulp
|
||||
bool ptr_exact = false;
|
||||
std::vector<std::string> unordered; // list paths to treat as sets
|
||||
};
|
||||
|
||||
struct Meta {
|
||||
std::string build;
|
||||
std::string exe_sha256;
|
||||
std::string started;
|
||||
unsigned inline_max = 256;
|
||||
std::vector<std::pair<std::string, HookPolicy>> hooks;
|
||||
};
|
||||
|
||||
// ---- output buffer --------------------------------------------------------------------------------
|
||||
|
||||
class Buf {
|
||||
public:
|
||||
Buf();
|
||||
~Buf();
|
||||
Buf(const Buf&) = delete;
|
||||
Buf& operator=(const Buf&) = delete;
|
||||
|
||||
void put(char c);
|
||||
void put(const void* bytes, std::size_t n);
|
||||
void puts(const char* s);
|
||||
#if defined(__MINGW32__)
|
||||
void printf(const char* fmt, ...) __attribute__((format(gnu_printf, 2, 3)));
|
||||
#else
|
||||
void printf(const char* fmt, ...) __attribute__((format(printf, 2, 3)));
|
||||
#endif
|
||||
void clear();
|
||||
|
||||
const char* data() const { return data_ ? data_ : ""; }
|
||||
std::size_t size() const { return len_; }
|
||||
bool ok() const { return !overflow_; }
|
||||
std::string str() const { return std::string(data(), len_); }
|
||||
|
||||
private:
|
||||
bool grow(std::size_t need);
|
||||
char* data_ = nullptr;
|
||||
std::size_t len_ = 0, cap_ = 0;
|
||||
bool overflow_ = false;
|
||||
};
|
||||
|
||||
// ---- emit ---------------------------------------------------------------------------------------------
|
||||
|
||||
void emit_esc(Buf& out, const char* bytes, std::size_t n); // section 4 rule 2 (str)
|
||||
void emit_esc(Buf& out, const std::string& s);
|
||||
void emit_esc_w(Buf& out, const std::uint16_t* units, std::size_t n); // section 4 rule 2 (wstr)
|
||||
void emit_int(Buf& out, long long v);
|
||||
void emit_uint(Buf& out, unsigned long long v);
|
||||
void emit_float(Buf& out, double v, bool f32);
|
||||
void emit_tv(Buf& out, const Tv& v);
|
||||
void emit_record(Buf& out, const Record& r); // one line, ends in "}\n"
|
||||
void emit_meta(Buf& out, const Meta& m); // one line, ends in "}\n"
|
||||
|
||||
// Canonical text of a typed value: emit_tv() output. Used as the sort key for sets.
|
||||
std::string canonical(const Tv& v);
|
||||
|
||||
} // namespace shim::trace
|
||||
222
src/shim/trace/hook.h
Normal file
222
src/shim/trace/hook.h
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
// Hook template: mode dispatch (off / trace / compare / replace) around a hooked C function.
|
||||
//
|
||||
// A hook is a descriptor struct D (see docs/shim-trace.md, "Declaring a hook"):
|
||||
//
|
||||
// struct MyHook {
|
||||
// static constexpr const char* name = "Game::Foo"; // record `hook` name
|
||||
// static constexpr CallConv conv = CallConv::Cdecl; // Cdecl | Stdcall
|
||||
// using Ret = int; // void allowed
|
||||
// using Args = std::tuple<Table*, const char*, int>; // parameter list
|
||||
// static void describe_args(std::vector<Tv>& out, Table* t, const char* k, int v);
|
||||
// static Tv describe_ret(int r); // omit when Ret is void
|
||||
// static void regions(std::vector<Region>& out, Table* t, const char* k, int v);
|
||||
// static Args rebind(Scratch& s, Table* t, const char* k, int v); // args for `ours`
|
||||
// static int ours(Table* t, const char* k, int v); // the reimplementation
|
||||
// static HookPolicy policy(); // meta.hooks entry
|
||||
// };
|
||||
//
|
||||
// Hook<MyHook>::detour() is the function to install (MinHook target -> detour) and
|
||||
// Hook<MyHook>::original the trampoline (or the real function in host tests).
|
||||
//
|
||||
// Mode semantics:
|
||||
// Off call original.
|
||||
// Trace snapshot regions, call original, snapshot again, emit {args, ret, side}.
|
||||
// Compare snapshot, call original, snapshot; copy the *before* snapshots into scratch
|
||||
// memory, rebind the args onto the copies, call ours, snapshot the copies, diff,
|
||||
// emit {.., ours, diverged, diff}. The original's result is what the caller gets.
|
||||
// Replace call ours only; nothing is emitted (nothing to diff against).
|
||||
//
|
||||
// Nothing escapes the hook boundary: every capture step is wrapped, a throw from ours or
|
||||
// from a describe/regions/rebind callback becomes `err` in the record, and the original is
|
||||
// always invoked in Off/Trace/Compare regardless of capture failures.
|
||||
//
|
||||
// thiscall (and any hook whose prototype is [unverified] in sots_addresses.h) must NOT use
|
||||
// this template: see docs/M0.md gotcha 1. Those stay register-transparent asm stubs that
|
||||
// only log an entry and tail-jump to the trampoline (trace-only, no return capture).
|
||||
#pragma once
|
||||
|
||||
#include <exception>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "shim/trace/emitter.h"
|
||||
#include "shim/trace/platform.h"
|
||||
#include "shim/trace/tracer.h"
|
||||
|
||||
namespace shim::trace {
|
||||
|
||||
enum class CallConv { Cdecl, Stdcall };
|
||||
|
||||
template <CallConv CC, class R, class... A>
|
||||
struct FnPtr;
|
||||
template <class R, class... A>
|
||||
struct FnPtr<CallConv::Cdecl, R, A...> {
|
||||
using type = R(SHIM_CDECL*)(A...);
|
||||
};
|
||||
template <class R, class... A>
|
||||
struct FnPtr<CallConv::Stdcall, R, A...> {
|
||||
using type = R(SHIM_STDCALL*)(A...);
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
|
||||
// Holds a return value, or nothing for void, so one code path serves both.
|
||||
template <class R>
|
||||
struct RetSlot {
|
||||
R v{};
|
||||
template <class F, class... X>
|
||||
void invoke(F f, X... x) { v = f(x...); }
|
||||
template <class F, class T>
|
||||
void apply(F f, T& t) { v = std::apply(f, t); }
|
||||
R get() const { return v; }
|
||||
template <class D>
|
||||
std::optional<Tv> describe() const { return D::describe_ret(v); }
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RetSlot<void> {
|
||||
template <class F, class... X>
|
||||
void invoke(F f, X... x) { f(x...); }
|
||||
template <class F, class T>
|
||||
void apply(F f, T& t) { std::apply(f, t); }
|
||||
void get() const {}
|
||||
template <class D>
|
||||
std::optional<Tv> describe() const { return std::nullopt; }
|
||||
};
|
||||
|
||||
inline std::string what(const char* stage) {
|
||||
std::string s = stage;
|
||||
try {
|
||||
throw;
|
||||
} catch (const std::exception& e) {
|
||||
s += ": ";
|
||||
s += e.what();
|
||||
} catch (...) {
|
||||
s += ": non-std exception";
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
template <class D, class ArgsTuple = typename D::Args>
|
||||
class Hook;
|
||||
|
||||
template <class D, class... A>
|
||||
class Hook<D, std::tuple<A...>> {
|
||||
public:
|
||||
using Ret = typename D::Ret;
|
||||
using Fn = typename FnPtr<D::conv, Ret, A...>::type;
|
||||
|
||||
// Trampoline to the original (MinHook fills it) — or, in host tests, the real function.
|
||||
inline static Fn original = nullptr;
|
||||
inline static Mode mode = Mode::Off;
|
||||
|
||||
// Reads the mode for D::name from the tracer (Off when the tracer is not open).
|
||||
static void configure(Tracer& t) { mode = t.mode_for(D::name); }
|
||||
static void register_policy(Tracer& t) { t.register_hook(D::name, D::policy()); }
|
||||
|
||||
static Fn detour() {
|
||||
if constexpr (D::conv == CallConv::Cdecl) return &detour_cdecl;
|
||||
else return &detour_stdcall;
|
||||
}
|
||||
|
||||
static Ret dispatch(A... a) {
|
||||
switch (mode) {
|
||||
case Mode::Off: return original(a...);
|
||||
case Mode::Replace: return D::ours(a...);
|
||||
case Mode::Trace: return run<false>(a...);
|
||||
case Mode::Compare: return run<true>(a...);
|
||||
}
|
||||
return original(a...);
|
||||
}
|
||||
|
||||
private:
|
||||
static Ret SHIM_CDECL detour_cdecl(A... a) { return dispatch(a...); }
|
||||
static Ret SHIM_STDCALL detour_stdcall(A... a) { return dispatch(a...); }
|
||||
|
||||
template <bool kCompare>
|
||||
static Ret run(A... a) {
|
||||
Tracer& tr = Tracer::instance();
|
||||
const unsigned inline_max = tr.inline_max();
|
||||
Record rec;
|
||||
std::vector<Region> regions;
|
||||
std::vector<Snapshot> before;
|
||||
bool captured = false;
|
||||
rec.mode = kCompare ? Mode::Compare : Mode::Trace;
|
||||
rec.call_id = tr.next_call_id(); // before the original: nested hooks get later ids
|
||||
rec.depth = tr.enter();
|
||||
try {
|
||||
rec.hook = D::name;
|
||||
D::describe_args(rec.args, a...);
|
||||
D::regions(regions, a...);
|
||||
for (const Region& r : regions) before.push_back(Snapshot::capture(r));
|
||||
captured = true;
|
||||
} catch (...) {
|
||||
rec.err = detail::what("capture before original");
|
||||
}
|
||||
|
||||
detail::RetSlot<Ret> orig_ret;
|
||||
orig_ret.invoke(original, a...); // always; the game must see the original's behavior
|
||||
|
||||
try {
|
||||
if (captured) {
|
||||
rec.ret = orig_ret.template describe<D>();
|
||||
for (std::size_t i = 0; i < regions.size(); ++i) {
|
||||
SideEntry e;
|
||||
e.name = before[i].name;
|
||||
e.before_kind = SideEntry::Value;
|
||||
e.before = before[i].to_tv(inline_max);
|
||||
e.after = before[i].recapture(regions[i]).to_tv(inline_max);
|
||||
rec.side.push_back(std::move(e));
|
||||
}
|
||||
if (kCompare) compare(rec, before, inline_max, a...);
|
||||
}
|
||||
} catch (...) {
|
||||
rec.err = detail::what("capture after original");
|
||||
}
|
||||
try {
|
||||
tr.write(rec);
|
||||
} catch (...) {
|
||||
}
|
||||
tr.leave();
|
||||
return orig_ret.get();
|
||||
}
|
||||
|
||||
static void compare(Record& rec, const std::vector<Snapshot>& before, unsigned inline_max, A... a) {
|
||||
Scratch scratch(before);
|
||||
detail::RetSlot<Ret> ours_ret;
|
||||
try {
|
||||
std::tuple<A...> args2 = D::rebind(scratch, a...);
|
||||
ours_ret.apply(&D::ours, args2);
|
||||
} catch (...) {
|
||||
rec.err = detail::what("ours");
|
||||
rec.diverged = 1;
|
||||
rec.has_diff = true;
|
||||
DiffEntry d;
|
||||
d.path = "call";
|
||||
d.why = "err";
|
||||
Buf b;
|
||||
emit_esc(b, *rec.err);
|
||||
d.ours_raw = b.str();
|
||||
rec.diff.push_back(std::move(d));
|
||||
return;
|
||||
}
|
||||
rec.has_ours = true;
|
||||
rec.ours_ret = ours_ret.template describe<D>();
|
||||
for (std::size_t i = 0; i < scratch.count(); ++i) {
|
||||
OursSide s;
|
||||
s.name = scratch.current(i).name;
|
||||
s.after = scratch.current(i).to_tv(inline_max);
|
||||
rec.ours_side.push_back(std::move(s));
|
||||
}
|
||||
rec.has_diff = true;
|
||||
rec.diverged = diff_outputs(rec.ret, rec.side, rec.ours_ret, rec.ours_side, D::policy(), rec.diff) ? 1 : 0;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace shim::trace
|
||||
122
src/shim/trace/platform.h
Normal file
122
src/shim/trace/platform.h
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
// Tiny platform layer so the trace/compare infrastructure compiles both inside the shim
|
||||
// (i686 MinGW, Win32 API) and on the host (Linux, pthread) for ctest.
|
||||
//
|
||||
// Nothing here throws. Nothing here allocates.
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <pthread.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#if defined(_WIN32) && (defined(__i386__) || defined(_M_IX86))
|
||||
#define SHIM_X86_WIN 1
|
||||
#define SHIM_CDECL __attribute__((cdecl))
|
||||
#define SHIM_STDCALL __attribute__((stdcall))
|
||||
#else
|
||||
#define SHIM_X86_WIN 0
|
||||
#define SHIM_CDECL
|
||||
#define SHIM_STDCALL
|
||||
#endif
|
||||
|
||||
namespace shim {
|
||||
|
||||
// Non-recursive mutex. CRITICAL_SECTION on Windows (recursive there, which is fine: a hook
|
||||
// that re-enters the tracer from inside `write` would be a bug either way).
|
||||
class Mutex {
|
||||
public:
|
||||
Mutex();
|
||||
~Mutex();
|
||||
Mutex(const Mutex&) = delete;
|
||||
Mutex& operator=(const Mutex&) = delete;
|
||||
void lock();
|
||||
void unlock();
|
||||
|
||||
private:
|
||||
#if defined(_WIN32)
|
||||
CRITICAL_SECTION cs_;
|
||||
#else
|
||||
pthread_mutex_t m_;
|
||||
#endif
|
||||
};
|
||||
|
||||
class LockGuard {
|
||||
public:
|
||||
explicit LockGuard(Mutex& m) : m_(m) { m_.lock(); }
|
||||
~LockGuard() { m_.unlock(); }
|
||||
LockGuard(const LockGuard&) = delete;
|
||||
LockGuard& operator=(const LockGuard&) = delete;
|
||||
|
||||
private:
|
||||
Mutex& m_;
|
||||
};
|
||||
|
||||
// Monotonic microseconds (QPC on Windows, CLOCK_MONOTONIC elsewhere). Absolute origin is
|
||||
// arbitrary; the tracer subtracts its own start time.
|
||||
std::int64_t monotonic_us();
|
||||
|
||||
// OS thread id (GetCurrentThreadId / gettid).
|
||||
std::uint32_t thread_id();
|
||||
|
||||
// "YYYY-MM-DDTHH:MM:SSZ" of now, UTC. `out` must hold at least 21 bytes.
|
||||
void utc_timestamp(char* out, unsigned cap);
|
||||
|
||||
// ---- inline definitions ---------------------------------------------------------------------
|
||||
|
||||
#if defined(_WIN32)
|
||||
inline Mutex::Mutex() { InitializeCriticalSection(&cs_); }
|
||||
inline Mutex::~Mutex() { DeleteCriticalSection(&cs_); }
|
||||
inline void Mutex::lock() { EnterCriticalSection(&cs_); }
|
||||
inline void Mutex::unlock() { LeaveCriticalSection(&cs_); }
|
||||
|
||||
inline std::int64_t monotonic_us() {
|
||||
LARGE_INTEGER f, c;
|
||||
QueryPerformanceFrequency(&f);
|
||||
QueryPerformanceCounter(&c);
|
||||
// split to avoid overflowing 64 bits for long uptimes
|
||||
const long long secs = c.QuadPart / f.QuadPart;
|
||||
const long long rem = c.QuadPart % f.QuadPart;
|
||||
return static_cast<std::int64_t>(secs * 1000000LL + rem * 1000000LL / f.QuadPart);
|
||||
}
|
||||
|
||||
inline std::uint32_t thread_id() { return static_cast<std::uint32_t>(GetCurrentThreadId()); }
|
||||
|
||||
inline void utc_timestamp(char* out, unsigned cap) {
|
||||
SYSTEMTIME st;
|
||||
GetSystemTime(&st);
|
||||
if (cap < 21) { if (cap) out[0] = '\0'; return; }
|
||||
std::snprintf(out, cap, "%04u-%02u-%02uT%02u:%02u:%02uZ", unsigned(st.wYear), unsigned(st.wMonth),
|
||||
unsigned(st.wDay), unsigned(st.wHour), unsigned(st.wMinute), unsigned(st.wSecond));
|
||||
}
|
||||
#else
|
||||
inline Mutex::Mutex() { pthread_mutex_init(&m_, nullptr); }
|
||||
inline Mutex::~Mutex() { pthread_mutex_destroy(&m_); }
|
||||
inline void Mutex::lock() { pthread_mutex_lock(&m_); }
|
||||
inline void Mutex::unlock() { pthread_mutex_unlock(&m_); }
|
||||
|
||||
inline std::int64_t monotonic_us() {
|
||||
timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return static_cast<std::int64_t>(ts.tv_sec) * 1000000LL + ts.tv_nsec / 1000;
|
||||
}
|
||||
|
||||
inline std::uint32_t thread_id() { return static_cast<std::uint32_t>(syscall(SYS_gettid)); }
|
||||
|
||||
inline void utc_timestamp(char* out, unsigned cap) {
|
||||
time_t now = time(nullptr);
|
||||
tm t;
|
||||
gmtime_r(&now, &t);
|
||||
if (cap < 21) { if (cap) out[0] = '\0'; return; }
|
||||
strftime(out, cap, "%Y-%m-%dT%H:%M:%SZ", &t);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace shim
|
||||
69
src/shim/trace/selftest.cpp
Normal file
69
src/shim/trace/selftest.cpp
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
#include "shim/trace/selftest.h"
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
namespace shim::selftest {
|
||||
|
||||
using trace::Tv;
|
||||
|
||||
std::uint32_t SHIM_CDECL Fill(std::uint8_t* buf, std::uint32_t n, std::uint32_t seed) {
|
||||
std::uint32_t x = seed, sum = 0;
|
||||
for (std::uint32_t i = 0; i < n; ++i) {
|
||||
x = x * 1664525u + 1013904223u;
|
||||
buf[i] = static_cast<std::uint8_t>(x >> 24);
|
||||
sum = sum * 31u + buf[i];
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
// Same contract, written differently (rolling checksum accumulated after the fill).
|
||||
std::uint32_t SHIM_CDECL FillOurs(std::uint8_t* buf, std::uint32_t n, std::uint32_t seed) {
|
||||
std::uint32_t x = seed;
|
||||
for (std::uint32_t i = 0; i < n; ++i) {
|
||||
x = 1664525u * x + 1013904223u;
|
||||
buf[i] = static_cast<std::uint8_t>((x >> 24) & 0xffu);
|
||||
}
|
||||
std::uint32_t sum = 0;
|
||||
for (std::uint32_t i = 0; i < n; ++i) sum = sum * 31u + buf[i];
|
||||
return sum;
|
||||
}
|
||||
|
||||
std::uint32_t SHIM_CDECL FillWrong(std::uint8_t* buf, std::uint32_t n, std::uint32_t seed) {
|
||||
const std::uint32_t sum = FillOurs(buf, n, seed);
|
||||
if (n) buf[n - 1] = static_cast<std::uint8_t>(buf[n - 1] + 1);
|
||||
return sum; // checksum still reports the original's answer: only the region diverges
|
||||
}
|
||||
|
||||
std::uint32_t SHIM_CDECL FillThrows(std::uint8_t*, std::uint32_t, std::uint32_t) {
|
||||
throw std::runtime_error("selftest: deliberate throw");
|
||||
}
|
||||
|
||||
void FillHook::describe_args(std::vector<Tv>& out, std::uint8_t* buf, std::uint32_t n, std::uint32_t seed) {
|
||||
out.push_back(trace::tv::ptr(buf).named("buf"));
|
||||
out.push_back(trace::tv::u32(n).named("n"));
|
||||
out.push_back(trace::tv::u32(seed).named("seed"));
|
||||
}
|
||||
|
||||
Tv FillHook::describe_ret(std::uint32_t r) { return trace::tv::u32(r); }
|
||||
|
||||
void FillHook::regions(std::vector<trace::Region>& out, std::uint8_t* buf, std::uint32_t n, std::uint32_t) {
|
||||
trace::Region r;
|
||||
r.name = "buf";
|
||||
r.ptr = buf;
|
||||
r.size = n;
|
||||
out.push_back(r);
|
||||
}
|
||||
|
||||
FillHook::Args FillHook::rebind(trace::Scratch& s, std::uint8_t*, std::uint32_t n, std::uint32_t seed) {
|
||||
return Args(s.as<std::uint8_t>(0), n, seed);
|
||||
}
|
||||
|
||||
std::uint32_t run_once(trace::Mode mode) {
|
||||
using H = trace::Hook<FillHook>;
|
||||
H::original = &Fill;
|
||||
H::mode = mode;
|
||||
std::uint8_t buf[64] = {};
|
||||
return H::detour()(buf, sizeof buf, 0x5eed);
|
||||
}
|
||||
|
||||
} // namespace shim::selftest
|
||||
53
src/shim/trace/selftest.h
Normal file
53
src/shim/trace/selftest.h
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
// Worked example hook that needs no game knowledge: the shim's own `Fill` function.
|
||||
//
|
||||
// Fill(buf, n, seed): writes n bytes of an LCG stream into buf, returns a 32-bit checksum.
|
||||
//
|
||||
// FillHook wraps it with the hook template (args: ptr/u32/u32, ret: u32, one side region
|
||||
// "buf"), with `ours` = an independent reimplementation. FillWrongHook is the same hook with
|
||||
// a deliberately wrong `ours` (last byte off by one) so the compare path's divergence
|
||||
// reporting can be exercised. The shim runs FillHook once at startup (see main.cpp) as a
|
||||
// smoke record; the host test drives all modes and validates the log with tracecmp.py.
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
#include "shim/trace/hook.h"
|
||||
|
||||
namespace shim::selftest {
|
||||
|
||||
std::uint32_t SHIM_CDECL Fill(std::uint8_t* buf, std::uint32_t n, std::uint32_t seed);
|
||||
std::uint32_t SHIM_CDECL FillOurs(std::uint8_t* buf, std::uint32_t n, std::uint32_t seed);
|
||||
std::uint32_t SHIM_CDECL FillWrong(std::uint8_t* buf, std::uint32_t n, std::uint32_t seed);
|
||||
std::uint32_t SHIM_CDECL FillThrows(std::uint8_t* buf, std::uint32_t n, std::uint32_t seed);
|
||||
|
||||
struct FillHook {
|
||||
static constexpr const char* name = "Shim::SelfTest::Fill";
|
||||
static constexpr trace::CallConv conv = trace::CallConv::Cdecl;
|
||||
using Ret = std::uint32_t;
|
||||
using Args = std::tuple<std::uint8_t*, std::uint32_t, std::uint32_t>;
|
||||
|
||||
static void describe_args(std::vector<trace::Tv>& out, std::uint8_t* buf, std::uint32_t n, std::uint32_t seed);
|
||||
static trace::Tv describe_ret(std::uint32_t r);
|
||||
static void regions(std::vector<trace::Region>& out, std::uint8_t* buf, std::uint32_t n, std::uint32_t seed);
|
||||
static Args rebind(trace::Scratch& s, std::uint8_t* buf, std::uint32_t n, std::uint32_t seed);
|
||||
static std::uint32_t ours(std::uint8_t* buf, std::uint32_t n, std::uint32_t seed) { return FillOurs(buf, n, seed); }
|
||||
static trace::HookPolicy policy() { return trace::HookPolicy{}; }
|
||||
};
|
||||
|
||||
struct FillWrongHook : FillHook {
|
||||
static constexpr const char* name = "Shim::SelfTest::FillWrong";
|
||||
static std::uint32_t ours(std::uint8_t* buf, std::uint32_t n, std::uint32_t seed) { return FillWrong(buf, n, seed); }
|
||||
};
|
||||
|
||||
struct FillThrowsHook : FillHook {
|
||||
static constexpr const char* name = "Shim::SelfTest::FillThrows";
|
||||
static std::uint32_t ours(std::uint8_t* buf, std::uint32_t n, std::uint32_t seed) { return FillThrows(buf, n, seed); }
|
||||
};
|
||||
|
||||
// Runs FillHook once through its detour (original = Fill) with a 64-byte buffer.
|
||||
// Returns the checksum. `mode` is applied to the hook first.
|
||||
std::uint32_t run_once(trace::Mode mode);
|
||||
|
||||
} // namespace shim::selftest
|
||||
116
src/shim/trace/sha256.cpp
Normal file
116
src/shim/trace/sha256.cpp
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
#include "shim/trace/sha256.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace shim {
|
||||
|
||||
namespace {
|
||||
|
||||
const std::uint32_t K[64] = {
|
||||
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
||||
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
||||
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2};
|
||||
|
||||
inline std::uint32_t rotr(std::uint32_t x, unsigned n) { return (x >> n) | (x << (32 - n)); }
|
||||
|
||||
} // namespace
|
||||
|
||||
Sha256::Sha256() : buflen_(0), total_(0) {
|
||||
static const std::uint32_t init[8] = {0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
|
||||
0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19};
|
||||
std::memcpy(h_, init, sizeof h_);
|
||||
}
|
||||
|
||||
void Sha256::block(const std::uint8_t* p) {
|
||||
std::uint32_t w[64];
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
w[i] = (std::uint32_t(p[4 * i]) << 24) | (std::uint32_t(p[4 * i + 1]) << 16) |
|
||||
(std::uint32_t(p[4 * i + 2]) << 8) | std::uint32_t(p[4 * i + 3]);
|
||||
}
|
||||
for (int i = 16; i < 64; ++i) {
|
||||
const std::uint32_t s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >> 3);
|
||||
const std::uint32_t s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >> 10);
|
||||
w[i] = w[i - 16] + s0 + w[i - 7] + s1;
|
||||
}
|
||||
std::uint32_t a = h_[0], b = h_[1], c = h_[2], d = h_[3], e = h_[4], f = h_[5], g = h_[6], h = h_[7];
|
||||
for (int i = 0; i < 64; ++i) {
|
||||
const std::uint32_t S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
||||
const std::uint32_t ch = (e & f) ^ (~e & g);
|
||||
const std::uint32_t t1 = h + S1 + ch + K[i] + w[i];
|
||||
const std::uint32_t S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
||||
const std::uint32_t maj = (a & b) ^ (a & c) ^ (b & c);
|
||||
const std::uint32_t t2 = S0 + maj;
|
||||
h = g; g = f; f = e; e = d + t1;
|
||||
d = c; c = b; b = a; a = t1 + t2;
|
||||
}
|
||||
h_[0] += a; h_[1] += b; h_[2] += c; h_[3] += d;
|
||||
h_[4] += e; h_[5] += f; h_[6] += g; h_[7] += h;
|
||||
}
|
||||
|
||||
void Sha256::update(const void* data, std::size_t len) {
|
||||
const std::uint8_t* p = static_cast<const std::uint8_t*>(data);
|
||||
total_ += len;
|
||||
if (buflen_) {
|
||||
const std::size_t take = (64 - buflen_ < len) ? 64 - buflen_ : len;
|
||||
std::memcpy(buf_ + buflen_, p, take);
|
||||
buflen_ += take;
|
||||
p += take;
|
||||
len -= take;
|
||||
if (buflen_ < 64) return;
|
||||
block(buf_);
|
||||
buflen_ = 0;
|
||||
}
|
||||
while (len >= 64) {
|
||||
block(p);
|
||||
p += 64;
|
||||
len -= 64;
|
||||
}
|
||||
if (len) {
|
||||
std::memcpy(buf_, p, len);
|
||||
buflen_ = len;
|
||||
}
|
||||
}
|
||||
|
||||
void Sha256::finish(std::uint8_t out[32]) {
|
||||
const std::uint64_t bits = total_ * 8;
|
||||
std::uint8_t pad[72];
|
||||
std::size_t padlen = (buflen_ < 56) ? 56 - buflen_ : 120 - buflen_;
|
||||
std::memset(pad, 0, sizeof pad);
|
||||
pad[0] = 0x80;
|
||||
for (int i = 0; i < 8; ++i) pad[padlen + i] = static_cast<std::uint8_t>(bits >> (56 - 8 * i));
|
||||
update(pad, padlen + 8);
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
out[4 * i] = static_cast<std::uint8_t>(h_[i] >> 24);
|
||||
out[4 * i + 1] = static_cast<std::uint8_t>(h_[i] >> 16);
|
||||
out[4 * i + 2] = static_cast<std::uint8_t>(h_[i] >> 8);
|
||||
out[4 * i + 3] = static_cast<std::uint8_t>(h_[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void Sha256::digest(const void* data, std::size_t len, std::uint8_t out[32]) {
|
||||
Sha256 s;
|
||||
s.update(data, len);
|
||||
s.finish(out);
|
||||
}
|
||||
|
||||
void Sha256::to_hex(const std::uint8_t d[32], char hex[65]) {
|
||||
static const char* digits = "0123456789abcdef";
|
||||
for (int i = 0; i < 32; ++i) {
|
||||
hex[2 * i] = digits[d[i] >> 4];
|
||||
hex[2 * i + 1] = digits[d[i] & 15];
|
||||
}
|
||||
hex[64] = '\0';
|
||||
}
|
||||
|
||||
void Sha256::digest_hex(const void* data, std::size_t len, char hex[65]) {
|
||||
std::uint8_t d[32];
|
||||
digest(data, len, d);
|
||||
to_hex(d, hex);
|
||||
}
|
||||
|
||||
} // namespace shim
|
||||
30
src/shim/trace/sha256.h
Normal file
30
src/shim/trace/sha256.h
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// Minimal SHA-256 (FIPS 180-4), written from the standard. No allocation, no exceptions.
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace shim {
|
||||
|
||||
class Sha256 {
|
||||
public:
|
||||
Sha256();
|
||||
void update(const void* data, std::size_t len);
|
||||
// Finalizes into `out[32]`. The object must not be updated afterwards.
|
||||
void finish(std::uint8_t out[32]);
|
||||
|
||||
// One-shot helpers.
|
||||
static void digest(const void* data, std::size_t len, std::uint8_t out[32]);
|
||||
// Lowercase hex of the digest into `hex[65]` (NUL-terminated).
|
||||
static void digest_hex(const void* data, std::size_t len, char hex[65]);
|
||||
static void to_hex(const std::uint8_t d[32], char hex[65]);
|
||||
|
||||
private:
|
||||
void block(const std::uint8_t* p);
|
||||
std::uint32_t h_[8];
|
||||
std::uint8_t buf_[64];
|
||||
std::size_t buflen_;
|
||||
std::uint64_t total_;
|
||||
};
|
||||
|
||||
} // namespace shim
|
||||
401
src/shim/trace/tracer.cpp
Normal file
401
src/shim/trace/tracer.cpp
Normal file
|
|
@ -0,0 +1,401 @@
|
|||
#include "shim/trace/tracer.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#include "shim/trace/sha256.h"
|
||||
|
||||
namespace shim::trace {
|
||||
|
||||
// ---- configuration --------------------------------------------------------------------------
|
||||
|
||||
bool Config::apply(const char* key, const char* value, std::string* err) {
|
||||
auto fail = [&](const char* msg) {
|
||||
if (err) *err = msg;
|
||||
return true;
|
||||
};
|
||||
if (std::strcmp(key, "hooks") == 0) {
|
||||
Mode m;
|
||||
if (!parse_mode(value, m)) return fail("hooks: expected off|trace|compare|replace");
|
||||
default_mode = m;
|
||||
return true;
|
||||
}
|
||||
if (std::strncmp(key, "hook.", 5) == 0) {
|
||||
const char* name = key + 5;
|
||||
if (!*name) return fail("hook.<Name>: empty name");
|
||||
Mode m;
|
||||
if (!parse_mode(value, m)) return fail("hook.<Name>: expected off|trace|compare|replace");
|
||||
for (auto& hm : hook_modes) {
|
||||
if (hm.first == name) {
|
||||
hm.second = m;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
hook_modes.emplace_back(name, m);
|
||||
return true;
|
||||
}
|
||||
if (std::strcmp(key, "trace.path") == 0) {
|
||||
if (!*value) return fail("trace.path: empty");
|
||||
path = value;
|
||||
return true;
|
||||
}
|
||||
if (std::strcmp(key, "trace.inline_max") == 0) {
|
||||
char* end = nullptr;
|
||||
const unsigned long n = std::strtoul(value, &end, 10);
|
||||
if (!end || *end || end == value) return fail("trace.inline_max: expected a number");
|
||||
inline_max = static_cast<unsigned>(n);
|
||||
return true;
|
||||
}
|
||||
if (std::strcmp(key, "trace.flush") == 0) {
|
||||
if (std::strcmp(value, "always") == 0) flush_always = true;
|
||||
else if (std::strcmp(value, "lazy") == 0) flush_always = false;
|
||||
else return fail("trace.flush: expected always|lazy");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Mode Config::mode_for(const char* hook) const {
|
||||
for (const auto& hm : hook_modes)
|
||||
if (hm.first == hook) return hm.second;
|
||||
return default_mode;
|
||||
}
|
||||
|
||||
// ---- snapshots ---------------------------------------------------------------------------------
|
||||
|
||||
Snapshot Snapshot::capture(const Region& r) {
|
||||
Snapshot s;
|
||||
s.name = r.name;
|
||||
s.describe = r.describe;
|
||||
const std::uint8_t* p = static_cast<const std::uint8_t*>(r.ptr);
|
||||
if (p && r.size) s.data.assign(p, p + r.size);
|
||||
return s;
|
||||
}
|
||||
|
||||
Snapshot Snapshot::recapture(const Region& r) const {
|
||||
Snapshot s = capture(r);
|
||||
s.name = name;
|
||||
s.describe = describe;
|
||||
return s;
|
||||
}
|
||||
|
||||
Tv Snapshot::to_tv(unsigned inline_max) const {
|
||||
if (describe) return describe(data.data(), data.size(), inline_max);
|
||||
return tv::bytes(data.data(), data.size(), inline_max);
|
||||
}
|
||||
|
||||
Scratch::Scratch(const std::vector<Snapshot>& before) : copies_(before) {}
|
||||
|
||||
// ---- diff ----------------------------------------------------------------------------------------------
|
||||
|
||||
namespace {
|
||||
|
||||
DiffEntry entry(const std::string& path, const char* why, const Tv* a, const Tv* b) {
|
||||
DiffEntry d;
|
||||
d.path = path;
|
||||
d.why = why;
|
||||
if (a) d.orig = *a;
|
||||
if (b) d.ours = *b;
|
||||
return d;
|
||||
}
|
||||
|
||||
float to_f32(double x) { return static_cast<float>(x); }
|
||||
|
||||
long long ulp_int(double x, bool f32) {
|
||||
if (f32) {
|
||||
const float f = to_f32(x);
|
||||
std::int32_t i;
|
||||
std::memcpy(&i, &f, sizeof i);
|
||||
return i >= 0 ? i : -static_cast<long long>(i & 0x7fffffff);
|
||||
}
|
||||
std::int64_t i;
|
||||
std::memcpy(&i, &x, sizeof i);
|
||||
return i >= 0 ? i : -static_cast<long long>(i & 0x7fffffffffffffffLL);
|
||||
}
|
||||
|
||||
// tracecmp.floats_equal with the f32 rounding rule applied by the caller.
|
||||
bool floats_equal(double a, double b, const HookPolicy& pol, bool f32) {
|
||||
if (std::isnan(a) || std::isnan(b)) return std::isnan(a) && std::isnan(b);
|
||||
if (std::isinf(a) || std::isinf(b)) return a == b;
|
||||
if (a == b) return true;
|
||||
if (pol.ftol <= 0) return false;
|
||||
if (std::strcmp(pol.ftol_kind, "abs") == 0) return std::fabs(a - b) <= pol.ftol;
|
||||
if (std::strcmp(pol.ftol_kind, "rel") == 0)
|
||||
return std::fabs(a - b) <= pol.ftol * std::max(std::fabs(a), std::fabs(b));
|
||||
if (std::strcmp(pol.ftol_kind, "ulp") == 0) {
|
||||
const long long d = ulp_int(a, f32) - ulp_int(b, f32);
|
||||
return static_cast<double>(d < 0 ? -d : d) <= pol.ftol;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool is_unordered(const HookPolicy& pol, const std::string& path) {
|
||||
for (const auto& p : pol.unordered)
|
||||
if (p == path) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<const Tv*> sorted_by_canonical(const std::vector<Tv>& items) {
|
||||
std::vector<std::pair<std::string, const Tv*>> keyed;
|
||||
keyed.reserve(items.size());
|
||||
for (const Tv& t : items) keyed.emplace_back(canonical(t), &t);
|
||||
std::sort(keyed.begin(), keyed.end(),
|
||||
[](const auto& x, const auto& y) { return x.first < y.first; });
|
||||
std::vector<const Tv*> out;
|
||||
out.reserve(keyed.size());
|
||||
for (const auto& k : keyed) out.push_back(k.second);
|
||||
return out;
|
||||
}
|
||||
|
||||
const Tv* struct_field(const Tv& s, const std::string& key) {
|
||||
for (std::size_t i = 0; i < s.keys.size(); ++i)
|
||||
if (s.keys[i] == key) return &s.items[i];
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string len_text(std::size_t n) { return std::to_string(n); }
|
||||
|
||||
} // namespace
|
||||
|
||||
bool diff_tv(const Tv& a, const Tv& b, const HookPolicy& pol, const std::string& path,
|
||||
std::vector<DiffEntry>& out) {
|
||||
const std::size_t start = out.size();
|
||||
if (a.kind != b.kind || (a.kind == Tv::Int && std::strcmp(a.itype, b.itype) != 0) ||
|
||||
(a.kind == Tv::Float && a.f32 != b.f32)) {
|
||||
out.push_back(entry(path, "type", &a, &b));
|
||||
return true;
|
||||
}
|
||||
switch (a.kind) {
|
||||
case Tv::Ptr: {
|
||||
const bool za = a.p == 0, zb = b.p == 0;
|
||||
if (za != zb || (pol.ptr_exact && a.p != b.p)) out.push_back(entry(path, "exact", &a, &b));
|
||||
break;
|
||||
}
|
||||
case Tv::Null:
|
||||
break;
|
||||
case Tv::Bool:
|
||||
if (a.b != b.b) out.push_back(entry(path, "exact", &a, &b));
|
||||
break;
|
||||
case Tv::Enum:
|
||||
if (a.i != b.i) out.push_back(entry(path, "exact", &a, &b));
|
||||
break;
|
||||
case Tv::Str:
|
||||
case Tv::Json:
|
||||
if (a.s != b.s) out.push_back(entry(path, "exact", &a, &b));
|
||||
break;
|
||||
case Tv::WStr:
|
||||
if (a.w != b.w) out.push_back(entry(path, "exact", &a, &b));
|
||||
break;
|
||||
case Tv::Int:
|
||||
if (a.is_unsigned ? (a.u != b.u) : (a.i != b.i)) out.push_back(entry(path, "exact", &a, &b));
|
||||
break;
|
||||
case Tv::Float: {
|
||||
// %.9g round-trips the float32, not the widened double: compare at float32 width.
|
||||
const double x = a.f32 ? static_cast<double>(to_f32(a.f)) : a.f;
|
||||
const double y = b.f32 ? static_cast<double>(to_f32(b.f)) : b.f;
|
||||
if (!floats_equal(x, y, pol, a.f32)) out.push_back(entry(path, pol.ftol > 0 ? "ftol" : "exact", &a, &b));
|
||||
break;
|
||||
}
|
||||
case Tv::Bytes: {
|
||||
if (a.blen != b.blen) {
|
||||
out.push_back(entry(path, "len", &a, &b));
|
||||
break;
|
||||
}
|
||||
if (std::strcmp(a.sha, b.sha) != 0) {
|
||||
DiffEntry d = entry(path, "hash", &a, &b);
|
||||
if (a.inline_full && b.inline_full) {
|
||||
const std::size_t n = std::min(a.data.size(), b.data.size());
|
||||
for (std::size_t i = 0; i < n; ++i) {
|
||||
if (a.data[i] != b.data[i]) {
|
||||
d.first_diff_offset = static_cast<long long>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
out.push_back(std::move(d));
|
||||
} else if (a.inline_full && b.inline_full && a.data != b.data) {
|
||||
DiffEntry d = entry(path, "hash", &a, &b);
|
||||
d.note = "same sha256, different hex: corrupt log";
|
||||
out.push_back(std::move(d));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Tv::List:
|
||||
case Tv::Set: {
|
||||
std::vector<const Tv*> va, vb;
|
||||
if (a.kind == Tv::Set || is_unordered(pol, path)) {
|
||||
va = sorted_by_canonical(a.items);
|
||||
vb = sorted_by_canonical(b.items);
|
||||
} else {
|
||||
for (const Tv& t : a.items) va.push_back(&t);
|
||||
for (const Tv& t : b.items) vb.push_back(&t);
|
||||
}
|
||||
if (va.size() != vb.size()) {
|
||||
DiffEntry d = entry(path, "len", nullptr, nullptr);
|
||||
d.orig_raw = len_text(va.size());
|
||||
d.ours_raw = len_text(vb.size());
|
||||
out.push_back(std::move(d));
|
||||
break;
|
||||
}
|
||||
for (std::size_t i = 0; i < va.size(); ++i)
|
||||
diff_tv(*va[i], *vb[i], pol, path + ".v[" + std::to_string(i) + "]", out);
|
||||
break;
|
||||
}
|
||||
case Tv::Struct: {
|
||||
std::vector<std::string> ka = a.keys, kb = b.keys;
|
||||
std::sort(ka.begin(), ka.end());
|
||||
std::sort(kb.begin(), kb.end());
|
||||
for (const std::string& k : ka)
|
||||
if (!struct_field(b, k)) out.push_back(entry(path + ".v." + k, "missing", struct_field(a, k), nullptr));
|
||||
for (const std::string& k : kb)
|
||||
if (!struct_field(a, k)) out.push_back(entry(path + ".v." + k, "extra", nullptr, struct_field(b, k)));
|
||||
for (const std::string& k : ka)
|
||||
if (const Tv* fb = struct_field(b, k)) diff_tv(*struct_field(a, k), *fb, pol, path + ".v." + k, out);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return out.size() > start;
|
||||
}
|
||||
|
||||
bool diff_outputs(const std::optional<Tv>& ret_a, const std::vector<SideEntry>& side_a,
|
||||
const std::optional<Tv>& ret_b, const std::vector<OursSide>& side_b, const HookPolicy& pol,
|
||||
std::vector<DiffEntry>& out) {
|
||||
const std::size_t start = out.size();
|
||||
if (ret_a.has_value() != ret_b.has_value()) {
|
||||
out.push_back(entry("ret", ret_b ? "extra" : "missing", ret_a ? &*ret_a : nullptr, ret_b ? &*ret_b : nullptr));
|
||||
} else if (ret_a) {
|
||||
diff_tv(*ret_a, *ret_b, pol, "ret", out);
|
||||
}
|
||||
auto find_a = [&](const std::string& n) -> const SideEntry* {
|
||||
for (const auto& e : side_a) if (e.name == n) return &e;
|
||||
return nullptr;
|
||||
};
|
||||
auto find_b = [&](const std::string& n) -> const OursSide* {
|
||||
for (const auto& e : side_b) if (e.name == n) return &e;
|
||||
return nullptr;
|
||||
};
|
||||
std::vector<std::string> na, nb;
|
||||
for (const auto& e : side_a) na.push_back(e.name);
|
||||
for (const auto& e : side_b) nb.push_back(e.name);
|
||||
std::sort(na.begin(), na.end());
|
||||
std::sort(nb.begin(), nb.end());
|
||||
for (const std::string& n : na)
|
||||
if (!find_b(n)) out.push_back(entry("side." + n + ".after", "missing", &find_a(n)->after, nullptr));
|
||||
for (const std::string& n : nb)
|
||||
if (!find_a(n)) out.push_back(entry("side." + n + ".after", "extra", nullptr, &find_b(n)->after));
|
||||
for (const std::string& n : na)
|
||||
if (const OursSide* b = find_b(n)) diff_tv(find_a(n)->after, b->after, pol, "side." + n + ".after", out);
|
||||
return out.size() > start;
|
||||
}
|
||||
|
||||
// ---- tracer ------------------------------------------------------------------------------------------------
|
||||
|
||||
namespace {
|
||||
thread_local int t_depth = 0;
|
||||
std::atomic<std::uint32_t> g_next_call_id{0};
|
||||
} // namespace
|
||||
|
||||
Tracer& Tracer::instance() {
|
||||
static Tracer t;
|
||||
return t;
|
||||
}
|
||||
|
||||
void Tracer::configure(const Config& cfg) { cfg_ = cfg; }
|
||||
|
||||
void Tracer::register_hook(const char* name, const HookPolicy& policy) {
|
||||
for (auto& h : hooks_) {
|
||||
if (h.first == name) {
|
||||
h.second = policy;
|
||||
return;
|
||||
}
|
||||
}
|
||||
hooks_.emplace_back(name, policy);
|
||||
}
|
||||
|
||||
bool Tracer::open(const char* build_id, const char* exe_sha256) {
|
||||
if (file_) return true;
|
||||
if (cfg_.path.empty()) return false;
|
||||
std::FILE* f = std::fopen(cfg_.path.c_str(), "wb");
|
||||
if (!f) return false;
|
||||
Meta m;
|
||||
m.build = build_id ? build_id : "";
|
||||
m.exe_sha256 = exe_sha256 ? exe_sha256 : "";
|
||||
char ts[32];
|
||||
utc_timestamp(ts, sizeof ts);
|
||||
m.started = ts;
|
||||
m.inline_max = cfg_.inline_max;
|
||||
m.hooks = hooks_;
|
||||
Buf b;
|
||||
emit_meta(b, m);
|
||||
if (!b.ok()) {
|
||||
std::fclose(f);
|
||||
return false;
|
||||
}
|
||||
std::fwrite(b.data(), 1, b.size(), f);
|
||||
std::fflush(f);
|
||||
LockGuard g(mu_);
|
||||
file_ = f;
|
||||
t0_ = monotonic_us();
|
||||
written_ = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Tracer::close() {
|
||||
LockGuard g(mu_);
|
||||
if (!file_) return;
|
||||
std::fflush(file_);
|
||||
std::fclose(file_);
|
||||
file_ = nullptr;
|
||||
}
|
||||
|
||||
Mode Tracer::mode_for(const char* hook) const {
|
||||
if (!file_) return Mode::Off;
|
||||
return cfg_.mode_for(hook);
|
||||
}
|
||||
|
||||
std::uint32_t Tracer::next_call_id() { return g_next_call_id.fetch_add(1, std::memory_order_relaxed); }
|
||||
|
||||
int Tracer::enter() { return t_depth++; }
|
||||
void Tracer::leave() { if (t_depth > 0) --t_depth; }
|
||||
|
||||
std::int64_t Tracer::now_us() const { return monotonic_us() - t0_; }
|
||||
|
||||
bool Tracer::write(Record& rec) {
|
||||
if (!file_) return false;
|
||||
rec.ts = now_us();
|
||||
rec.thread = thread_id();
|
||||
Buf b;
|
||||
emit_record(b, rec);
|
||||
if (!b.ok()) return false;
|
||||
const bool urgent = cfg_.flush_always || rec.diverged > 0 || rec.err.has_value();
|
||||
LockGuard g(mu_);
|
||||
if (!file_) return false;
|
||||
std::fwrite(b.data(), 1, b.size(), file_);
|
||||
if (urgent) std::fflush(file_);
|
||||
++written_;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Tracer::flush() {
|
||||
LockGuard g(mu_);
|
||||
if (file_) std::fflush(file_);
|
||||
}
|
||||
|
||||
bool sha256_file(const char* path, char hex[65]) {
|
||||
std::FILE* f = std::fopen(path, "rb");
|
||||
if (!f) return false;
|
||||
Sha256 h;
|
||||
std::uint8_t buf[65536];
|
||||
std::size_t n;
|
||||
while ((n = std::fread(buf, 1, sizeof buf, f)) > 0) h.update(buf, n);
|
||||
std::fclose(f);
|
||||
std::uint8_t d[32];
|
||||
h.finish(d);
|
||||
Sha256::to_hex(d, hex);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace shim::trace
|
||||
133
src/shim/trace/tracer.h
Normal file
133
src/shim/trace/tracer.h
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
// Tracer: per-hook modes from shim.cfg, the locked JSONL writer, call ids / depth, side-effect
|
||||
// snapshots + scratch copies, and the shim-side diff (TRACE_FORMAT.md section 6).
|
||||
//
|
||||
// Host-testable: only platform.h touches the OS.
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "shim/trace/emitter.h"
|
||||
#include "shim/trace/platform.h"
|
||||
|
||||
namespace shim::trace {
|
||||
|
||||
// ---- configuration --------------------------------------------------------------------------
|
||||
//
|
||||
// shim.cfg keys handled here (main.cpp forwards every key it does not own):
|
||||
// hooks=off|trace|compare|replace default mode for every hook ("off" = nothing installed)
|
||||
// hook.<Name>=off|trace|compare|replace per-hook override (<Name> is the hook's record name)
|
||||
// trace.path=<file> log file (default: <shim dir>\shim.trace.jsonl)
|
||||
// trace.inline_max=<n> bytes inlined as hex (default 256)
|
||||
// trace.flush=always|lazy fflush after every record (default) or only on
|
||||
// divergence / err / close
|
||||
struct Config {
|
||||
Mode default_mode = Mode::Trace;
|
||||
std::vector<std::pair<std::string, Mode>> hook_modes;
|
||||
std::string path;
|
||||
unsigned inline_max = 256;
|
||||
bool flush_always = true;
|
||||
|
||||
// Returns true when `key` is a trace key. Sets *err (if given) and returns true when the
|
||||
// key is ours but the value is unusable (the key is then left unchanged).
|
||||
bool apply(const char* key, const char* value, std::string* err = nullptr);
|
||||
Mode mode_for(const char* hook) const;
|
||||
};
|
||||
|
||||
// ---- side-effect regions ------------------------------------------------------------------------
|
||||
|
||||
// A declared region of memory the hooked function may write. `describe` turns the captured
|
||||
// bytes into a structured Tv (so diffs name a field); nullptr = plain `bytes`.
|
||||
struct Region {
|
||||
const char* name = "";
|
||||
const void* ptr = nullptr;
|
||||
std::size_t size = 0;
|
||||
Tv (*describe)(const void* data, std::size_t size, unsigned inline_max) = nullptr;
|
||||
};
|
||||
|
||||
struct Snapshot {
|
||||
std::string name;
|
||||
std::vector<std::uint8_t> data;
|
||||
Tv (*describe)(const void*, std::size_t, unsigned) = nullptr;
|
||||
|
||||
static Snapshot capture(const Region& r);
|
||||
// Re-read the same region (for "after"); the result shares name/describe.
|
||||
Snapshot recapture(const Region& r) const;
|
||||
Tv to_tv(unsigned inline_max) const;
|
||||
};
|
||||
|
||||
// Writable copies of a set of snapshots: the state `ours` runs on in compare mode.
|
||||
class Scratch {
|
||||
public:
|
||||
explicit Scratch(const std::vector<Snapshot>& before);
|
||||
std::size_t count() const { return copies_.size(); }
|
||||
void* ptr(std::size_t i) { return copies_[i].data.data(); }
|
||||
template <class T>
|
||||
T* as(std::size_t i) { return static_cast<T*>(ptr(i)); }
|
||||
std::size_t size(std::size_t i) const { return copies_[i].data.size(); }
|
||||
const Snapshot& current(std::size_t i) const { return copies_[i]; } // reflects writes by ours
|
||||
|
||||
private:
|
||||
std::vector<Snapshot> copies_;
|
||||
};
|
||||
|
||||
// ---- diff ------------------------------------------------------------------------------------------
|
||||
|
||||
// Appends diff entries for `a` (original) vs `b` (ours) at `path`; returns true if any.
|
||||
bool diff_tv(const Tv& a, const Tv& b, const HookPolicy& pol, const std::string& path,
|
||||
std::vector<DiffEntry>& out);
|
||||
|
||||
// Section 6 over a whole record: ret, then side names (missing/extra), then side.<n>.after.
|
||||
bool diff_outputs(const std::optional<Tv>& ret_a, const std::vector<SideEntry>& side_a,
|
||||
const std::optional<Tv>& ret_b, const std::vector<OursSide>& side_b, const HookPolicy& pol,
|
||||
std::vector<DiffEntry>& out);
|
||||
|
||||
// ---- tracer ------------------------------------------------------------------------------------------
|
||||
|
||||
class Tracer {
|
||||
public:
|
||||
static Tracer& instance();
|
||||
|
||||
// All three before open(): policy goes into the meta record.
|
||||
void configure(const Config& cfg);
|
||||
void register_hook(const char* name, const HookPolicy& policy);
|
||||
const Config& config() const { return cfg_; }
|
||||
|
||||
// Opens cfg.path ("w"), writes the meta line. false = could not open (tracing stays off).
|
||||
bool open(const char* build_id, const char* exe_sha256);
|
||||
void close();
|
||||
bool is_open() const { return file_ != nullptr; }
|
||||
|
||||
Mode mode_for(const char* hook) const; // Off when not open
|
||||
unsigned inline_max() const { return cfg_.inline_max; }
|
||||
|
||||
std::uint32_t next_call_id(); // atomic; take it BEFORE calling the original
|
||||
int enter(); // ++ thread-local depth; returns the depth of this call
|
||||
void leave();
|
||||
std::int64_t now_us() const; // microseconds since open()
|
||||
|
||||
// Fills ts/thread, serializes, appends under the lock. false = not open / buffer overflow.
|
||||
bool write(Record& rec);
|
||||
void flush();
|
||||
std::uint32_t records_written() const { return written_; }
|
||||
|
||||
private:
|
||||
Tracer() = default;
|
||||
Config cfg_;
|
||||
std::vector<std::pair<std::string, HookPolicy>> hooks_;
|
||||
std::FILE* file_ = nullptr;
|
||||
std::int64_t t0_ = 0;
|
||||
Mutex mu_;
|
||||
std::uint32_t next_id_ = 0;
|
||||
std::uint32_t written_ = 0;
|
||||
};
|
||||
|
||||
// SHA-256 of a whole file, lowercase hex. false if unreadable.
|
||||
bool sha256_file(const char* path, char hex[65]);
|
||||
|
||||
} // namespace shim::trace
|
||||
22
tests/shim_trace/CMakeLists.txt
Normal file
22
tests/shim_trace/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Host tests for the shim's trace/compare infrastructure (src/shim/trace). The Python
|
||||
# oracle (sots-re/verify/harness/compare) is used when SOTS_TRACECMP_DIR points at it;
|
||||
# the tests skip those steps cleanly otherwise.
|
||||
set(SOTS_TRACECMP_DIR "$ENV{HOME}/sots-re/verify/harness/compare" CACHE PATH
|
||||
"Directory holding tracecmp.py + mkfixture.py (python oracle for the trace tests)")
|
||||
|
||||
set(_out ${CMAKE_CURRENT_BINARY_DIR}/out)
|
||||
file(MAKE_DIRECTORY ${_out})
|
||||
|
||||
foreach(_t sha256 emitter diff hook)
|
||||
add_executable(shim_trace_test_${_t} test_${_t}.cpp)
|
||||
target_link_libraries(shim_trace_test_${_t} PRIVATE shim_trace)
|
||||
target_compile_options(shim_trace_test_${_t} PRIVATE -Wall -Wextra -Werror)
|
||||
target_compile_definitions(shim_trace_test_${_t} PRIVATE SOTS_TRACECMP_DIR="${SOTS_TRACECMP_DIR}")
|
||||
endforeach()
|
||||
target_link_libraries(shim_trace_test_hook PRIVATE pthread)
|
||||
|
||||
add_test(NAME shim_trace_sha256 COMMAND shim_trace_test_sha256)
|
||||
add_test(NAME shim_trace_emitter COMMAND shim_trace_test_emitter ${_out}/emitter_oracle.jsonl
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/oracle_emit.py)
|
||||
add_test(NAME shim_trace_diff COMMAND shim_trace_test_diff)
|
||||
add_test(NAME shim_trace_hook COMMAND shim_trace_test_hook ${_out})
|
||||
37
tests/shim_trace/check.h
Normal file
37
tests/shim_trace/check.h
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
// Minimal test helpers for the shim trace tests (no dependency on other modules).
|
||||
#pragma once
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
namespace tracetest {
|
||||
|
||||
inline int& failures() { static int n = 0; return n; }
|
||||
inline int& checks() { static int n = 0; return n; }
|
||||
|
||||
inline void report(bool ok, const char* expr, const char* file, int line, const std::string& detail) {
|
||||
++checks();
|
||||
if (ok) return;
|
||||
++failures();
|
||||
std::fprintf(stderr, "FAIL %s:%d %s%s%s\n", file, line, expr, detail.empty() ? "" : " -- ", detail.c_str());
|
||||
}
|
||||
|
||||
inline void check_str(const std::string& got, const std::string& want, const char* expr, const char* file, int line) {
|
||||
report(got == want, expr, file, line, got == want ? "" : "\n got: " + got + "\n want: " + want);
|
||||
}
|
||||
|
||||
template <class A, class B>
|
||||
inline void check_eq(const A& a, const B& b, const char* expr, const char* file, int line) {
|
||||
report(a == b, expr, file, line, a == b ? "" : "got " + std::to_string(a) + ", expected " + std::to_string(b));
|
||||
}
|
||||
|
||||
inline int finish(const char* name) {
|
||||
std::printf("%s: %d checks, %d failures\n", name, checks(), failures());
|
||||
return failures() == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
} // namespace tracetest
|
||||
|
||||
#define CHECK(expr) ::tracetest::report((expr), #expr, __FILE__, __LINE__, "")
|
||||
#define CHECK_EQ(a, b) ::tracetest::check_eq((a), (b), #a " == " #b, __FILE__, __LINE__)
|
||||
#define CHECK_STR(got, want) ::tracetest::check_str((got), (want), #got, __FILE__, __LINE__)
|
||||
62
tests/shim_trace/harness.h
Normal file
62
tests/shim_trace/harness.h
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
// Running the Python oracle (sots-re/verify/harness/compare) from a host test.
|
||||
//
|
||||
// The harness directory comes from -DSOTS_TRACECMP_DIR (CMake) or the SOTS_TRACECMP_DIR
|
||||
// environment variable; when neither points at a directory holding tracecmp.py the
|
||||
// python steps are skipped (returning kSkipped) and the test still passes.
|
||||
#pragma once
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
namespace tracetest {
|
||||
|
||||
constexpr int kSkipped = -1;
|
||||
|
||||
inline std::string harness_dir() {
|
||||
if (const char* e = std::getenv("SOTS_TRACECMP_DIR"); e && *e) return e;
|
||||
#ifdef SOTS_TRACECMP_DIR
|
||||
return SOTS_TRACECMP_DIR;
|
||||
#else
|
||||
return "";
|
||||
#endif
|
||||
}
|
||||
|
||||
inline bool file_exists(const std::string& p) {
|
||||
if (std::FILE* f = std::fopen(p.c_str(), "rb")) {
|
||||
std::fclose(f);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
inline bool harness_present() { return file_exists(harness_dir() + "/tracecmp.py"); }
|
||||
|
||||
// Exit status of `python3 <script> <args>` (kSkipped when unavailable, 127 if it failed to run).
|
||||
inline int run_python(const std::string& script, const std::string& args) {
|
||||
if (!file_exists(script)) {
|
||||
std::printf(" SKIP (no %s)\n", script.c_str());
|
||||
return kSkipped;
|
||||
}
|
||||
const std::string cmd = "/usr/bin/python3 " + script + " " + args;
|
||||
std::printf(" $ %s\n", cmd.c_str());
|
||||
std::fflush(stdout);
|
||||
const int rc = std::system(cmd.c_str());
|
||||
if (rc == -1) return 127;
|
||||
#if defined(_WIN32)
|
||||
return rc;
|
||||
#else
|
||||
return WIFEXITED(rc) ? WEXITSTATUS(rc) : 127;
|
||||
#endif
|
||||
}
|
||||
|
||||
// tracecmp.py LOG [flags]: 0 clean, 1 divergence, 2 invalid.
|
||||
inline int run_tracecmp(const std::string& log, const std::string& flags = "") {
|
||||
if (!harness_present()) {
|
||||
std::printf(" SKIP tracecmp (SOTS_TRACECMP_DIR not set / harness absent)\n");
|
||||
return kSkipped;
|
||||
}
|
||||
return run_python(harness_dir() + "/tracecmp.py", log + " --quiet " + flags);
|
||||
}
|
||||
|
||||
} // namespace tracetest
|
||||
96
tests/shim_trace/oracle_emit.py
Normal file
96
tests/shim_trace/oracle_emit.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
#!/usr/bin/env python3
|
||||
"""oracle_emit.py HARNESS_DIR CPP_FIXTURE.jsonl
|
||||
|
||||
Rebuilds the records test_emitter.cpp::write_oracle_fixture() writes, using the
|
||||
reference emitter in HARNESS_DIR/mkfixture.py (emit_meta / emit_record), and compares
|
||||
the two files byte for byte. Exit 0 = identical, 1 = differ, 2 = usage.
|
||||
Stdlib only.
|
||||
"""
|
||||
import sys
|
||||
|
||||
|
||||
def build(mk) -> str:
|
||||
inf, nan = float("inf"), float("nan")
|
||||
meta = {"format": 1, "build": "shim-trace test", "exe_sha256": "0" * 64,
|
||||
"started": "2026-09-07T00:00:00Z", "inline_max": 256,
|
||||
"hooks": {"Hook::A": {"ftol": 0.0, "ftol_kind": "abs", "ptr": "ignore"},
|
||||
"Mars::ParseBlock": {"ftol": 1e-6, "ftol_kind": "rel", "ptr": "exact",
|
||||
"unordered": ["ret.v.items"]}}}
|
||||
fwd = bytes(range(16))
|
||||
recs = [
|
||||
{"ts": 1000, "hook": "CfgVar_RegisterKey", "mode": "trace", "call_id": 0, "thread": 4120, "depth": 0,
|
||||
"args": [mk.s("Résumé", "key"), mk.i32(-3, "value"), mk.ptr(0x00A36FD0, "table")],
|
||||
"ret": mk.boolean(True),
|
||||
"side": {"cfg_table": {"before": mk.by(fwd), "after": mk.by(fwd[::-1])}}},
|
||||
{"ts": 1037, "hook": "Manifest_Load", "mode": "trace", "call_id": 1, "thread": 4120,
|
||||
"args": [mk.s("Weapons/_weapons.txt", "path")],
|
||||
"ret": mk.i32(2, "count"),
|
||||
"side": {"registry": {"after": mk.struct_({
|
||||
"entries": mk.lst(mk.struct_({"id": mk.u32(i), "name": mk.s("w%d.weapon" % i)}) for i in (7, 9)),
|
||||
"deleted": mk.sset(mk.u32(i) for i in (501, 550, 599)),
|
||||
"flags": mk.enum(2, "MF_0"),
|
||||
"big": mk.u64(2**60 + 1),
|
||||
"small": mk.u64(5),
|
||||
"edge": mk.u64(2**53),
|
||||
"neg": mk._tv("i64", str(-(2**60)), None), # |v| >= 2^53 -> decimal string (section 3)
|
||||
"nothing": mk.null(),
|
||||
"w": mk._tv("wstr", "A€\x00", None)})}}},
|
||||
{"ts": 1074, "hook": "Mars::ParseBlock", "mode": "trace", "call_id": 2, "thread": 4124, "depth": 1,
|
||||
"args": [mk.s('Tab\tKey Quote"d Back\\slash nl\nx cr\rx \x01\x7f\xff €', "text"), mk.u32(2, "len")],
|
||||
"ret": mk.jsonv({"weapon": {"count": 3, "damage": 1.5, "name": "Laser\xe9", "ok": True, "range": 2.25,
|
||||
"tags": ["a", "b"]}}),
|
||||
"side": {"scratch": {"after": mk.lst(mk.f32(v) for v in (1.5, 0.1, -2.5e-7, 1e30))},
|
||||
"nanbox": {"after": mk.f32(nan)},
|
||||
"infs": {"after": mk.lst([mk._tv("f64", "inf", None), mk._tv("f64", "-inf", None),
|
||||
mk._tv("f64", 0.1, None), mk._tv("f64", 1e300, None),
|
||||
mk._tv("f64", 0.0, None)])},
|
||||
"blob": {"before": None, "after": mk.by(bytes(range(256)) * 2)}}},
|
||||
{"ts": 1111, "hook": "CfgVar_RegisterKey", "mode": "compare", "call_id": 3, "thread": 4120,
|
||||
"args": [mk._tv("i8", -128, None), mk._tv("i16", 32767, None), mk._tv("u8", 255, None),
|
||||
mk._tv("u16", 65535, None), mk.u64(2**53 - 1)],
|
||||
"ret": mk.null(), "side": {},
|
||||
"ours": {"ret": mk.null(), "side": {}}, "diverged": False, "diff": []},
|
||||
{"ts": 1148, "hook": "Manifest_Load", "mode": "compare", "call_id": 4, "thread": 1,
|
||||
"args": [], "ret": mk.i32(1), "side": {"r": {"after": mk.by(b"\x01\x02")}},
|
||||
"ours": {"ret": mk.i32(2), "side": {"r": {"after": mk.by(b"\x01\x03")}}},
|
||||
"diverged": True,
|
||||
"diff": [{"path": "ret", "why": "exact", "orig": mk.i32(1), "ours": mk.i32(2)},
|
||||
{"path": "side.r.after", "why": "hash", "orig": mk.by(b"\x01\x02"), "ours": mk.by(b"\x01\x03"),
|
||||
"first_diff_offset": 1}],
|
||||
"note": "hello"},
|
||||
{"ts": 1185, "hook": "X", "mode": "compare", "call_id": 5, "thread": 1,
|
||||
"args": [], "ret": None, "side": {},
|
||||
"diverged": True, "diff": [{"path": "call", "why": "err", "orig": None, "ours": "ours threw"}],
|
||||
"err": "ours threw"},
|
||||
{"ts": 1222, "hook": "Y", "mode": "replace", "call_id": 6, "thread": 2,
|
||||
"args": [mk.boolean(False, "flag")], "ret": mk.ptr(0), "side": {}},
|
||||
]
|
||||
return mk.emit_meta(meta) + "".join(mk.emit_record(r) for r in recs)
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) != 3:
|
||||
print(__doc__)
|
||||
return 2
|
||||
sys.path.insert(0, argv[1])
|
||||
import mkfixture as mk # noqa: E402
|
||||
want = build(mk)
|
||||
with open(argv[2], "r", encoding="utf-8", newline="") as f:
|
||||
got = f.read()
|
||||
if got == want:
|
||||
print("oracle_emit: identical (%d bytes, %d lines)" % (len(want), want.count("\n")))
|
||||
return 0
|
||||
gl, wl = got.split("\n"), want.split("\n")
|
||||
for i, (g, w) in enumerate(zip(gl, wl), 1):
|
||||
if g != w:
|
||||
j = next((k for k in range(min(len(g), len(w))) if g[k] != w[k]), min(len(g), len(w)))
|
||||
print("oracle_emit: line %d differs at column %d" % (i, j + 1))
|
||||
print(" cpp: ...%s" % g[max(0, j - 40):j + 60])
|
||||
print(" py: ...%s" % w[max(0, j - 40):j + 60])
|
||||
return 1
|
||||
print("oracle_emit: line count differs (cpp %d, py %d)" % (len(gl), len(wl)))
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
256
tests/shim_trace/test_diff.cpp
Normal file
256
tests/shim_trace/test_diff.cpp
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
// Snapshot / Scratch / diff rules (TRACE_FORMAT.md section 6), mirrored on tracecmp.compare_tv.
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "check.h"
|
||||
#include "shim/trace/tracer.h"
|
||||
|
||||
using namespace shim::trace;
|
||||
|
||||
static std::vector<DiffEntry> diff(const Tv& a, const Tv& b, HookPolicy pol = HookPolicy{}) {
|
||||
std::vector<DiffEntry> out;
|
||||
diff_tv(a, b, pol, "ret", out);
|
||||
return out;
|
||||
}
|
||||
|
||||
static std::string why(const std::vector<DiffEntry>& d, std::size_t i = 0) { return i < d.size() ? d[i].why : "<none>"; }
|
||||
static std::string path(const std::vector<DiffEntry>& d, std::size_t i = 0) { return i < d.size() ? d[i].path : "<none>"; }
|
||||
|
||||
static void scalars() {
|
||||
CHECK(diff(tv::boolean(true), tv::boolean(true)).empty());
|
||||
CHECK_STR(why(diff(tv::boolean(true), tv::boolean(false))), "exact");
|
||||
CHECK_STR(why(diff(tv::i32(1), tv::u32(1))), "type"); // i32 vs u32 is a type divergence
|
||||
CHECK_STR(why(diff(tv::i32(1), tv::i32(2))), "exact");
|
||||
CHECK(diff(tv::u64(1ull << 60), tv::u64(1ull << 60)).empty());
|
||||
CHECK_STR(why(diff(tv::str("a"), tv::str("b"))), "exact");
|
||||
CHECK(diff(tv::str("R\xe9"), tv::str("R\xe9")).empty());
|
||||
const std::uint16_t w1[] = {1, 2}, w2[] = {1, 3};
|
||||
CHECK_STR(why(diff(tv::wstr(w1, 2), tv::wstr(w2, 2))), "exact");
|
||||
CHECK_STR(why(diff(tv::str("a"), tv::wstr(w1, 1))), "type");
|
||||
CHECK(diff(tv::null(), tv::null()).empty());
|
||||
CHECK_STR(why(diff(tv::null(), tv::i32(0))), "type");
|
||||
CHECK(diff(tv::enum_(2, "A"), tv::enum_(2, "B")).empty()); // symbolic name is not compared
|
||||
CHECK_STR(why(diff(tv::enum_(2), tv::enum_(3))), "exact");
|
||||
CHECK(diff(tv::json("{\"a\":1}"), tv::json("{\"a\":1}")).empty());
|
||||
CHECK_STR(why(diff(tv::json("{\"a\":1}"), tv::json("{\"a\":2}"))), "exact");
|
||||
}
|
||||
|
||||
static void pointers() {
|
||||
// ignored by default; null vs non-null always diverges; exact when the policy says so
|
||||
CHECK(diff(tv::ptr(static_cast<std::uintptr_t>(0x10)), tv::ptr(static_cast<std::uintptr_t>(0x20))).empty());
|
||||
CHECK_STR(why(diff(tv::ptr(static_cast<std::uintptr_t>(0)), tv::ptr(static_cast<std::uintptr_t>(0x20)))), "exact");
|
||||
CHECK_STR(why(diff(tv::ptr(static_cast<std::uintptr_t>(0x20)), tv::ptr(static_cast<std::uintptr_t>(0)))), "exact");
|
||||
CHECK(diff(tv::ptr(static_cast<std::uintptr_t>(0)), tv::ptr(static_cast<std::uintptr_t>(0))).empty());
|
||||
HookPolicy exact;
|
||||
exact.ptr_exact = true;
|
||||
CHECK_STR(why(diff(tv::ptr(static_cast<std::uintptr_t>(0x10)), tv::ptr(static_cast<std::uintptr_t>(0x20)), exact)), "exact");
|
||||
CHECK(diff(tv::ptr(static_cast<std::uintptr_t>(0x10)), tv::ptr(static_cast<std::uintptr_t>(0x10)), exact).empty());
|
||||
}
|
||||
|
||||
static void floats() {
|
||||
const float nanf = std::numeric_limits<float>::quiet_NaN();
|
||||
const double inf = std::numeric_limits<double>::infinity();
|
||||
CHECK(diff(tv::f32(nanf), tv::f32(nanf)).empty()); // nan == nan
|
||||
CHECK_STR(why(diff(tv::f32(nanf), tv::f32(0.f))), "exact");
|
||||
CHECK(diff(tv::f64(inf), tv::f64(inf)).empty());
|
||||
CHECK_STR(why(diff(tv::f64(inf), tv::f64(-inf))), "exact");
|
||||
CHECK_STR(why(diff(tv::f32(1.f), tv::f64(1.0))), "type"); // never mix widths
|
||||
// the f32 rounding rule: a value stored as f32 compares at float32 precision
|
||||
CHECK(diff(tv::f32(0.1f), tv::f32(static_cast<float>(0.1))).empty());
|
||||
CHECK_STR(why(diff(tv::f32(1.0f), tv::f32(1.0f + 1e-5f))), "exact");
|
||||
HookPolicy abs;
|
||||
abs.ftol = 1e-3;
|
||||
CHECK(diff(tv::f32(1.0f), tv::f32(1.0f + 1e-5f), abs).empty()); // mkfixture case #7 passes abs 1e-3
|
||||
CHECK_STR(why(diff(tv::f32(1.0f), tv::f32(1.5f), abs)), "ftol"); // and reports "ftol" when a tolerance is set
|
||||
CHECK_STR(why(diff(tv::f32(nanf), tv::f32(1.f), abs)), "ftol"); // nan only equals nan, whatever the tolerance
|
||||
HookPolicy rel;
|
||||
rel.ftol = 1e-6;
|
||||
rel.ftol_kind = "rel";
|
||||
CHECK(diff(tv::f64(1e6), tv::f64(1e6 + 0.5), rel).empty());
|
||||
CHECK_STR(why(diff(tv::f64(1.0), tv::f64(1.0 + 1e-5), rel)), "ftol");
|
||||
HookPolicy ulp;
|
||||
ulp.ftol = 2;
|
||||
ulp.ftol_kind = "ulp";
|
||||
CHECK(diff(tv::f32(1.0f), tv::f32(std::nextafter(1.0f, 2.0f)), ulp).empty());
|
||||
CHECK(diff(tv::f32(1.0f), tv::f32(std::nextafter(std::nextafter(1.0f, 2.0f), 2.0f)), ulp).empty());
|
||||
CHECK_STR(why(diff(tv::f32(1.0f), tv::f32(std::nextafter(std::nextafter(std::nextafter(1.0f, 2.0f), 2.0f), 2.0f)), ulp)), "ftol");
|
||||
CHECK(diff(tv::f32(-0.0f), tv::f32(0.0f), ulp).empty()); // -0 == 0
|
||||
CHECK(diff(tv::f64(1.0), tv::f64(std::nextafter(1.0, 2.0)), ulp).empty());
|
||||
}
|
||||
|
||||
static void bytes() {
|
||||
const std::uint8_t a[4] = {1, 2, 3, 4}, b[4] = {1, 2, 9, 4}, c[3] = {1, 2, 3};
|
||||
CHECK(diff(tv::bytes(a, 4, 256), tv::bytes(a, 4, 256)).empty());
|
||||
auto d = diff(tv::bytes(a, 4, 256), tv::bytes(b, 4, 256));
|
||||
CHECK_STR(why(d), "hash");
|
||||
CHECK_EQ(d.empty() ? -1 : d[0].first_diff_offset, 2); // first differing byte offset (inline)
|
||||
d = diff(tv::bytes(a, 4, 256), tv::bytes(c, 3, 256));
|
||||
CHECK_STR(why(d), "len");
|
||||
d = diff(tv::bytes(a, 4, 2), tv::bytes(b, 4, 2)); // hashed (n > inline_max): no offset
|
||||
CHECK_STR(why(d), "hash");
|
||||
CHECK_EQ(d.empty() ? 0 : d[0].first_diff_offset, -1);
|
||||
// same sha256 but different hex = corrupt log
|
||||
Tv x = tv::bytes(a, 4, 256), y = tv::bytes(a, 4, 256);
|
||||
y.data[0] = 7;
|
||||
d = diff(x, y);
|
||||
CHECK_STR(why(d), "hash");
|
||||
CHECK(!d.empty() && d[0].note.find("corrupt") != std::string::npos);
|
||||
}
|
||||
|
||||
static void containers() {
|
||||
auto L = [](std::vector<Tv> v) { return tv::list(std::move(v)); };
|
||||
auto S = [](std::vector<Tv> v) { return tv::set(std::move(v)); };
|
||||
CHECK(diff(L({tv::i32(1), tv::i32(2)}), L({tv::i32(1), tv::i32(2)})).empty());
|
||||
auto d = diff(L({tv::i32(1), tv::i32(2)}), L({tv::i32(2), tv::i32(1)}));
|
||||
CHECK_STR(why(d), "exact");
|
||||
CHECK_STR(path(d), "ret.v[0]");
|
||||
CHECK(diff(S({tv::i32(1), tv::i32(2)}), S({tv::i32(2), tv::i32(1)})).empty()); // set order irrelevant
|
||||
CHECK_STR(why(diff(S({tv::i32(1), tv::i32(1)}), S({tv::i32(1), tv::i32(2)}))), "exact"); // multiset
|
||||
d = diff(L({tv::i32(1)}), L({tv::i32(1), tv::i32(2)}));
|
||||
CHECK_STR(why(d), "len");
|
||||
CHECK_STR(d.empty() ? "" : d[0].orig_raw, "1");
|
||||
CHECK_STR(d.empty() ? "" : d[0].ours_raw, "2");
|
||||
HookPolicy un;
|
||||
un.unordered = {"ret"};
|
||||
CHECK(diff(L({tv::i32(1), tv::i32(2)}), L({tv::i32(2), tv::i32(1)}), un).empty()); // policy makes a list a set
|
||||
|
||||
Tv sa = tv::struct_(), sb = tv::struct_();
|
||||
sa.add("id", tv::u32(1)).add("big", tv::u64(5)).add("name", tv::str("x"));
|
||||
sb.add("name", tv::str("x")).add("id", tv::u32(1)).add("big", tv::u64(5)); // key order irrelevant
|
||||
CHECK(diff(sa, sb).empty());
|
||||
Tv sc = tv::struct_();
|
||||
sc.add("id", tv::u32(1)).add("name", tv::str("y")).add("zz", tv::i8(0));
|
||||
d = diff(sa, sc);
|
||||
// sorted: missing "big", extra "zz", then common keys in order -> name differs
|
||||
CHECK_EQ(d.size(), static_cast<std::size_t>(3));
|
||||
CHECK_STR(why(d, 0), "missing");
|
||||
CHECK_STR(path(d, 0), "ret.v.big");
|
||||
CHECK_STR(why(d, 1), "extra");
|
||||
CHECK_STR(path(d, 1), "ret.v.zz");
|
||||
CHECK_STR(why(d, 2), "exact");
|
||||
CHECK_STR(path(d, 2), "ret.v.name");
|
||||
// nested path: struct in list
|
||||
Tv e1 = tv::struct_(), e2 = tv::struct_();
|
||||
e1.add("id", tv::u32(1));
|
||||
e2.add("id", tv::u32(2));
|
||||
d = diff(L({e1}), L({e2}));
|
||||
CHECK_STR(path(d), "ret.v[0].v.id");
|
||||
}
|
||||
|
||||
static void outputs() {
|
||||
std::vector<SideEntry> sa;
|
||||
std::vector<OursSide> sb;
|
||||
SideEntry e;
|
||||
e.name = "cfg";
|
||||
e.after = tv::i32(1);
|
||||
sa.push_back(e);
|
||||
e.name = "only_orig";
|
||||
sa.push_back(e);
|
||||
sb.push_back({"cfg", tv::i32(2)});
|
||||
sb.push_back({"only_ours", tv::i32(0)});
|
||||
std::vector<DiffEntry> d;
|
||||
CHECK(diff_outputs(tv::boolean(true), sa, tv::boolean(true), sb, HookPolicy{}, d));
|
||||
CHECK_EQ(d.size(), static_cast<std::size_t>(3));
|
||||
CHECK_STR(path(d, 0), "side.only_orig.after");
|
||||
CHECK_STR(why(d, 0), "missing");
|
||||
CHECK_STR(path(d, 1), "side.only_ours.after");
|
||||
CHECK_STR(why(d, 1), "extra");
|
||||
CHECK_STR(path(d, 2), "side.cfg.after");
|
||||
CHECK_STR(why(d, 2), "exact");
|
||||
d.clear();
|
||||
CHECK(!diff_outputs(std::nullopt, {}, std::nullopt, {}, HookPolicy{}, d)); // void == void
|
||||
CHECK(diff_outputs(std::nullopt, {}, tv::i32(0), {}, HookPolicy{}, d)); // void vs value
|
||||
CHECK_STR(why(d), "extra");
|
||||
}
|
||||
|
||||
static Tv describe_pair(const void* p, std::size_t n, unsigned) {
|
||||
Tv s = tv::struct_();
|
||||
const std::uint8_t* b = static_cast<const std::uint8_t*>(p);
|
||||
s.add("lo", tv::u8(n > 0 ? b[0] : 0));
|
||||
s.add("hi", tv::u8(n > 1 ? b[1] : 0));
|
||||
return s;
|
||||
}
|
||||
|
||||
static void snapshots() {
|
||||
std::uint8_t mem[4] = {1, 2, 3, 4};
|
||||
Region r;
|
||||
r.name = "mem";
|
||||
r.ptr = mem;
|
||||
r.size = sizeof mem;
|
||||
Snapshot before = Snapshot::capture(r);
|
||||
CHECK_STR(before.name, "mem");
|
||||
CHECK_EQ(before.data.size(), static_cast<std::size_t>(4));
|
||||
mem[2] = 9;
|
||||
Snapshot after = before.recapture(r);
|
||||
CHECK_EQ(static_cast<int>(before.data[2]), 3); // the snapshot is a copy
|
||||
CHECK_EQ(static_cast<int>(after.data[2]), 9);
|
||||
// Scratch is a writable copy of `before`, independent of the live memory
|
||||
Scratch s({before});
|
||||
CHECK_EQ(s.count(), static_cast<std::size_t>(1));
|
||||
CHECK_EQ(s.size(0), static_cast<std::size_t>(4));
|
||||
s.as<std::uint8_t>(0)[0] = 42;
|
||||
CHECK_EQ(static_cast<int>(mem[0]), 1);
|
||||
CHECK_EQ(static_cast<int>(before.data[0]), 1);
|
||||
CHECK_EQ(static_cast<int>(s.current(0).data[0]), 42);
|
||||
// default describe = bytes; custom describe = struct
|
||||
CHECK_STR(before.to_tv(256).type_name(), "bytes");
|
||||
r.describe = &describe_pair;
|
||||
Snapshot st = Snapshot::capture(r);
|
||||
Tv t = st.to_tv(256);
|
||||
CHECK_STR(t.type_name(), "struct");
|
||||
CHECK_STR(canonical(t), "{\"t\":\"struct\",\"v\":{\"lo\":{\"t\":\"u8\",\"v\":1},\"hi\":{\"t\":\"u8\",\"v\":2}}}");
|
||||
// empty / null regions are fine
|
||||
Region z;
|
||||
z.name = "z";
|
||||
Snapshot zs = Snapshot::capture(z);
|
||||
CHECK(zs.data.empty());
|
||||
CHECK_STR(canonical(zs.to_tv(256)),
|
||||
"{\"t\":\"bytes\",\"n\":0,\"sha256\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\"hex\":\"\"}");
|
||||
}
|
||||
|
||||
static void config() {
|
||||
Config c;
|
||||
std::string err;
|
||||
CHECK(c.apply("hooks", "compare", &err));
|
||||
CHECK(c.default_mode == Mode::Compare);
|
||||
CHECK(c.apply("hook.Game::Foo", "replace", &err));
|
||||
CHECK(c.apply("hook.Game::Bar", "off", &err));
|
||||
CHECK(c.apply("hook.Game::Bar", "trace", &err)); // later line wins
|
||||
CHECK(c.mode_for("Game::Foo") == Mode::Replace);
|
||||
CHECK(c.mode_for("Game::Bar") == Mode::Trace);
|
||||
CHECK(c.mode_for("Other") == Mode::Compare);
|
||||
CHECK(c.apply("trace.path", "C:\\SOTS\\x.jsonl", &err));
|
||||
CHECK_STR(c.path, "C:\\SOTS\\x.jsonl");
|
||||
CHECK(c.apply("trace.inline_max", "64", &err));
|
||||
CHECK_EQ(c.inline_max, 64u);
|
||||
CHECK(c.apply("trace.flush", "lazy", &err));
|
||||
CHECK(!c.flush_always);
|
||||
CHECK(!c.apply("unrelated", "x", &err)); // not ours
|
||||
err.clear();
|
||||
CHECK(c.apply("hooks", "sometimes", &err)); // ours, but bad: reported, unchanged
|
||||
CHECK(!err.empty());
|
||||
CHECK(c.default_mode == Mode::Compare);
|
||||
err.clear();
|
||||
CHECK(c.apply("trace.inline_max", "lots", &err));
|
||||
CHECK(!err.empty());
|
||||
CHECK_EQ(c.inline_max, 64u);
|
||||
Mode m;
|
||||
CHECK(parse_mode("off", m) && m == Mode::Off);
|
||||
CHECK(!parse_mode("OFF", m));
|
||||
CHECK_STR(mode_name(Mode::Replace), "replace");
|
||||
}
|
||||
|
||||
int main() {
|
||||
scalars();
|
||||
pointers();
|
||||
floats();
|
||||
bytes();
|
||||
containers();
|
||||
outputs();
|
||||
snapshots();
|
||||
config();
|
||||
return tracetest::finish("shim_trace_diff");
|
||||
}
|
||||
425
tests/shim_trace/test_emitter.cpp
Normal file
425
tests/shim_trace/test_emitter.cpp
Normal file
|
|
@ -0,0 +1,425 @@
|
|||
// Emitter golden tests (byte-exact strings derived from TRACE_FORMAT.md section 4 and the
|
||||
// mkfixture.py reference emitter) + a fixture file that oracle_emit.py regenerates with
|
||||
// mkfixture's own emit_record() and compares byte for byte.
|
||||
#include "shim/trace/emitter.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "check.h"
|
||||
#include "harness.h"
|
||||
|
||||
using namespace shim::trace;
|
||||
|
||||
static std::string esc(const std::string& s) {
|
||||
Buf b;
|
||||
emit_esc(b, s);
|
||||
return b.str();
|
||||
}
|
||||
static std::string escw(std::vector<std::uint16_t> u) {
|
||||
Buf b;
|
||||
emit_esc_w(b, u.data(), u.size());
|
||||
return b.str();
|
||||
}
|
||||
static std::string tvs(const Tv& v) { return canonical(v); }
|
||||
|
||||
static const char* kSha512 = "110009dcee21620b166f3abfecb5eff7a873be729d1c2d53822e7acc5f34eb9b";
|
||||
|
||||
static void golden_strings() {
|
||||
// section 4 rule 2; the same inputs test_tracecmp.py feeds mkfixture.esc()
|
||||
CHECK_STR(esc("plain"), "\"plain\"");
|
||||
CHECK_STR(esc("R\xe9sum\xe9"), "\"R\\u00e9sum\\u00e9\"");
|
||||
CHECK_STR(esc("Tab\tKey"), "\"Tab\\tKey\"");
|
||||
CHECK_STR(esc("Quote\"d"), "\"Quote\\\"d\"");
|
||||
CHECK_STR(esc("Back\\slash"), "\"Back\\\\slash\"");
|
||||
CHECK_STR(esc("nl\nx"), "\"nl\\nx\"");
|
||||
CHECK_STR(esc("cr\rx"), "\"cr\\rx\"");
|
||||
CHECK_STR(esc("\x01\x7f\xff\x80"), "\"\\u0001\\u007f\\u00ff\\u0080\"");
|
||||
CHECK_STR(esc("\x08\x0c"), "\"\\u0008\\u000c\"");
|
||||
CHECK_STR(esc(std::string("a\0b", 3)), "\"a\\u0000b\"");
|
||||
CHECK_STR(escw({0x41, 0x20ac, 0x7e, 0x7f, 0xffff, 0x0a}), "\"A\\u20ac~\\u007f\\uffff\\n\"");
|
||||
}
|
||||
|
||||
static void golden_numbers() {
|
||||
CHECK_STR(tvs(tv::f32(1.5f)), "{\"t\":\"f32\",\"v\":1.5}");
|
||||
CHECK_STR(tvs(tv::f32(0.1f)), "{\"t\":\"f32\",\"v\":0.100000001}");
|
||||
CHECK_STR(tvs(tv::f32(-2.5e-7f)), "{\"t\":\"f32\",\"v\":-2.49999999e-07}");
|
||||
CHECK_STR(tvs(tv::f32(1e30f)), "{\"t\":\"f32\",\"v\":1.00000002e+30}");
|
||||
CHECK_STR(tvs(tv::f32(3.0f)), "{\"t\":\"f32\",\"v\":3}");
|
||||
CHECK_STR(tvs(tv::f32(123456789.0f)), "{\"t\":\"f32\",\"v\":123456792}");
|
||||
CHECK_STR(tvs(tv::f32(std::numeric_limits<float>::quiet_NaN())), "{\"t\":\"f32\",\"v\":\"nan\"}");
|
||||
CHECK_STR(tvs(tv::f32(-std::numeric_limits<float>::infinity())), "{\"t\":\"f32\",\"v\":\"-inf\"}");
|
||||
CHECK_STR(tvs(tv::f64(std::numeric_limits<double>::infinity())), "{\"t\":\"f64\",\"v\":\"inf\"}");
|
||||
CHECK_STR(tvs(tv::f64(0.1)), "{\"t\":\"f64\",\"v\":0.10000000000000001}");
|
||||
CHECK_STR(tvs(tv::f64(1e300)), "{\"t\":\"f64\",\"v\":1.0000000000000001e+300}");
|
||||
CHECK_STR(tvs(tv::f64(0.0)), "{\"t\":\"f64\",\"v\":0}");
|
||||
CHECK_STR(tvs(tv::f64(-0.0)), "{\"t\":\"f64\",\"v\":-0}");
|
||||
|
||||
CHECK_STR(tvs(tv::u64(18446744073709551615ull)), "{\"t\":\"u64\",\"v\":\"18446744073709551615\"}");
|
||||
CHECK_STR(tvs(tv::u64(9007199254740992ull)), "{\"t\":\"u64\",\"v\":\"9007199254740992\"}"); // 2^53 -> string
|
||||
CHECK_STR(tvs(tv::u64(9007199254740991ull)), "{\"t\":\"u64\",\"v\":9007199254740991}"); // 2^53-1 -> int
|
||||
CHECK_STR(tvs(tv::i64(-9007199254740992ll)), "{\"t\":\"i64\",\"v\":\"-9007199254740992\"}");
|
||||
CHECK_STR(tvs(tv::i64(-9007199254740991ll)), "{\"t\":\"i64\",\"v\":-9007199254740991}");
|
||||
CHECK_STR(tvs(tv::i8(-128)), "{\"t\":\"i8\",\"v\":-128}");
|
||||
CHECK_STR(tvs(tv::i16(32767)), "{\"t\":\"i16\",\"v\":32767}");
|
||||
CHECK_STR(tvs(tv::i32(-3).named("value")), "{\"t\":\"i32\",\"v\":-3,\"n\":\"value\"}");
|
||||
CHECK_STR(tvs(tv::u8(255)), "{\"t\":\"u8\",\"v\":255}");
|
||||
CHECK_STR(tvs(tv::u16(65535)), "{\"t\":\"u16\",\"v\":65535}");
|
||||
CHECK_STR(tvs(tv::u32(4294967295u)), "{\"t\":\"u32\",\"v\":4294967295}");
|
||||
CHECK_STR(tvs(tv::boolean(true)), "{\"t\":\"bool\",\"v\":true}");
|
||||
CHECK_STR(tvs(tv::null()), "{\"t\":\"null\",\"v\":null}");
|
||||
CHECK_STR(tvs(tv::ptr(static_cast<std::uintptr_t>(0xa36fd0)).named("table")), "{\"t\":\"ptr\",\"v\":\"0x00a36fd0\",\"n\":\"table\"}");
|
||||
CHECK_STR(tvs(tv::ptr(static_cast<std::uintptr_t>(0))), "{\"t\":\"ptr\",\"v\":\"0x00000000\"}");
|
||||
CHECK_STR(tvs(tv::enum_(2, "MF_0").named("f")), "{\"t\":\"enum\",\"v\":2,\"name\":\"MF_0\",\"n\":\"f\"}");
|
||||
CHECK_STR(tvs(tv::enum_(-1)), "{\"t\":\"enum\",\"v\":-1}");
|
||||
CHECK_STR(tvs(tv::str("R\xe9sum\xe9").named("key")), "{\"t\":\"str\",\"v\":\"R\\u00e9sum\\u00e9\",\"n\":\"key\"}");
|
||||
CHECK_STR(tvs(tv::str(nullptr)), "{\"t\":\"ptr\",\"v\":\"0x00000000\"}");
|
||||
const std::uint16_t w[] = {0x41, 0x20ac};
|
||||
CHECK_STR(tvs(tv::wstr(w, 2)), "{\"t\":\"wstr\",\"v\":\"A\\u20ac\"}");
|
||||
CHECK_STR(tvs(tv::json("{\"a\":[1,2.5,\"x\"]}")), "{\"t\":\"json\",\"v\":{\"a\":[1,2.5,\"x\"]}}");
|
||||
}
|
||||
|
||||
static void golden_bytes_and_containers() {
|
||||
CHECK_STR(tvs(tv::bytes("\x00\xff", 2, 256)),
|
||||
"{\"t\":\"bytes\",\"n\":2,\"sha256\":\"06eb7d6a69ee19e5fbdf749018d3d2abfa04bcbd1365db312eb86dc7169389b8\",\"hex\":\"00ff\"}");
|
||||
CHECK_STR(tvs(tv::bytes("", 0, 256)),
|
||||
"{\"t\":\"bytes\",\"n\":0,\"sha256\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\"hex\":\"\"}");
|
||||
std::vector<std::uint8_t> big;
|
||||
for (int k = 0; k < 2; ++k)
|
||||
for (int i = 0; i < 256; ++i) big.push_back(static_cast<std::uint8_t>(i));
|
||||
CHECK_STR(tvs(tv::bytes(big.data(), big.size(), 256)),
|
||||
std::string("{\"t\":\"bytes\",\"n\":512,\"sha256\":\"") + kSha512 +
|
||||
"\",\"head\":\"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f\"}");
|
||||
// exactly inline_max stays inline; inline_max+1 does not
|
||||
CHECK(tvs(tv::bytes(big.data(), 256, 256)).find("\"hex\"") != std::string::npos);
|
||||
CHECK(tvs(tv::bytes(big.data(), 257, 256)).find("\"head\"") != std::string::npos);
|
||||
// bytes never carry a name even if one is set
|
||||
CHECK_STR(tvs(tv::bytes("", 0, 256).named("x")),
|
||||
"{\"t\":\"bytes\",\"n\":0,\"sha256\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\"hex\":\"\"}");
|
||||
|
||||
std::vector<Tv> items;
|
||||
items.push_back(tv::i32(1));
|
||||
items.push_back(tv::str("a"));
|
||||
CHECK_STR(tvs(tv::list(items)), "{\"t\":\"list\",\"v\":[{\"t\":\"i32\",\"v\":1},{\"t\":\"str\",\"v\":\"a\"}]}");
|
||||
CHECK_STR(tvs(tv::set(items).named("s")), "{\"t\":\"set\",\"v\":[{\"t\":\"i32\",\"v\":1},{\"t\":\"str\",\"v\":\"a\"}],\"n\":\"s\"}");
|
||||
CHECK_STR(tvs(tv::list({})), "{\"t\":\"list\",\"v\":[]}");
|
||||
Tv st = tv::struct_();
|
||||
st.add("id", tv::u32(7)).add("na\"me", tv::str("w7"));
|
||||
CHECK_STR(tvs(st), "{\"t\":\"struct\",\"v\":{\"id\":{\"t\":\"u32\",\"v\":7},\"na\\\"me\":{\"t\":\"str\",\"v\":\"w7\"}}}");
|
||||
CHECK_STR(tvs(tv::struct_()), "{\"t\":\"struct\",\"v\":{}}");
|
||||
}
|
||||
|
||||
static void golden_records() {
|
||||
Record r;
|
||||
r.ts = 1;
|
||||
r.hook = "X";
|
||||
r.mode = Mode::Trace;
|
||||
r.call_id = 900;
|
||||
r.thread = 1;
|
||||
Buf b;
|
||||
emit_record(b, r);
|
||||
// == mkfixture.invalid_lines()[0], the harness's own minimal valid record
|
||||
CHECK_STR(b.str(), "{\"ts\":1,\"hook\":\"X\",\"mode\":\"trace\",\"call_id\":900,\"thread\":1,\"args\":[],\"ret\":null,\"side\":{}}\n");
|
||||
|
||||
Record c;
|
||||
c.ts = 1;
|
||||
c.hook = "X";
|
||||
c.mode = Mode::Compare;
|
||||
c.call_id = 4;
|
||||
c.thread = 1;
|
||||
c.depth = 0;
|
||||
c.args.push_back(tv::i32(1).named("a"));
|
||||
c.ret = tv::boolean(true);
|
||||
SideEntry se;
|
||||
se.name = "r";
|
||||
se.before_kind = SideEntry::Value;
|
||||
se.before = tv::null();
|
||||
se.after = tv::i32(2);
|
||||
c.side.push_back(se);
|
||||
SideEntry sn;
|
||||
sn.name = "q";
|
||||
sn.before_kind = SideEntry::IsNull;
|
||||
sn.after = tv::i32(0);
|
||||
c.side.push_back(sn);
|
||||
c.has_ours = true;
|
||||
c.ours_ret = tv::boolean(false);
|
||||
c.ours_side.push_back({"r", tv::i32(3)});
|
||||
c.ours_side.push_back({"q", tv::i32(0)});
|
||||
c.diverged = 1;
|
||||
c.has_diff = true;
|
||||
DiffEntry d;
|
||||
d.path = "ret";
|
||||
d.why = "exact";
|
||||
d.orig = tv::boolean(true);
|
||||
d.ours = tv::boolean(false);
|
||||
c.diff.push_back(d);
|
||||
DiffEntry d2;
|
||||
d2.path = "side.r.after";
|
||||
d2.why = "hash";
|
||||
d2.first_diff_offset = 1;
|
||||
d2.note = "x";
|
||||
c.diff.push_back(d2);
|
||||
DiffEntry d3;
|
||||
d3.path = "side.x.after";
|
||||
d3.why = "len";
|
||||
d3.orig_raw = "3";
|
||||
d3.ours_raw = "2";
|
||||
c.diff.push_back(d3);
|
||||
c.err = "e";
|
||||
c.note = "n\xe9";
|
||||
Buf cb;
|
||||
emit_record(cb, c);
|
||||
CHECK_STR(cb.str(),
|
||||
"{\"ts\":1,\"hook\":\"X\",\"mode\":\"compare\",\"call_id\":4,\"thread\":1,\"depth\":0,"
|
||||
"\"args\":[{\"t\":\"i32\",\"v\":1,\"n\":\"a\"}],\"ret\":{\"t\":\"bool\",\"v\":true},"
|
||||
"\"side\":{\"r\":{\"before\":{\"t\":\"null\",\"v\":null},\"after\":{\"t\":\"i32\",\"v\":2}},"
|
||||
"\"q\":{\"before\":null,\"after\":{\"t\":\"i32\",\"v\":0}}},"
|
||||
"\"ours\":{\"ret\":{\"t\":\"bool\",\"v\":false},\"side\":{\"r\":{\"after\":{\"t\":\"i32\",\"v\":3}},"
|
||||
"\"q\":{\"after\":{\"t\":\"i32\",\"v\":0}}}},\"diverged\":true,"
|
||||
"\"diff\":[{\"path\":\"ret\",\"why\":\"exact\",\"orig\":{\"t\":\"bool\",\"v\":true},\"ours\":{\"t\":\"bool\",\"v\":false}},"
|
||||
"{\"path\":\"side.r.after\",\"why\":\"hash\",\"orig\":null,\"ours\":null,\"first_diff_offset\":1,\"note\":\"x\"},"
|
||||
"{\"path\":\"side.x.after\",\"why\":\"len\",\"orig\":3,\"ours\":2}],"
|
||||
"\"err\":\"e\",\"note\":\"n\\u00e9\"}\n");
|
||||
|
||||
// replace record: no ours/diverged/diff; empty diff list still emits "diff":[] when flagged
|
||||
Record p;
|
||||
p.ts = 22;
|
||||
p.hook = "Mars::ParseBlock";
|
||||
p.mode = Mode::Replace;
|
||||
p.call_id = 7;
|
||||
p.thread = 3;
|
||||
p.has_diff = true;
|
||||
p.diverged = 0;
|
||||
Buf pb;
|
||||
emit_record(pb, p);
|
||||
CHECK_STR(pb.str(), "{\"ts\":22,\"hook\":\"Mars::ParseBlock\",\"mode\":\"replace\",\"call_id\":7,\"thread\":3,\"args\":[],\"ret\":null,\"side\":{},\"diverged\":false,\"diff\":[]}\n");
|
||||
|
||||
Meta m;
|
||||
m.build = "sots-engine test";
|
||||
m.exe_sha256 = std::string(64, '0');
|
||||
m.started = "2026-09-07T00:00:00Z";
|
||||
m.inline_max = 256;
|
||||
HookPolicy pa;
|
||||
HookPolicy pb2;
|
||||
pb2.ftol = 1e-6;
|
||||
pb2.ftol_kind = "rel";
|
||||
pb2.ptr_exact = true;
|
||||
pb2.unordered = {"ret.v.items"};
|
||||
m.hooks.emplace_back("Hook::A", pa);
|
||||
m.hooks.emplace_back("Mars::ParseBlock", pb2);
|
||||
Buf mb;
|
||||
emit_meta(mb, m);
|
||||
CHECK_STR(mb.str(),
|
||||
"{\"meta\":{\"format\":1,\"build\":\"sots-engine test\",\"exe_sha256\":\"" + std::string(64, '0') +
|
||||
"\",\"started\":\"2026-09-07T00:00:00Z\",\"inline_max\":256,\"hooks\":{"
|
||||
"\"Hook::A\":{\"ftol\":0,\"ftol_kind\":\"abs\",\"ptr\":\"ignore\"},"
|
||||
"\"Mars::ParseBlock\":{\"ftol\":9.9999999999999995e-07,\"ftol_kind\":\"rel\",\"ptr\":\"exact\",\"unordered\":[\"ret.v.items\"]}}}}\n");
|
||||
Meta m0;
|
||||
Buf m0b;
|
||||
emit_meta(m0b, m0);
|
||||
CHECK_STR(m0b.str(), "{\"meta\":{\"format\":1,\"build\":\"\",\"exe_sha256\":\"\",\"started\":\"\",\"inline_max\":256,\"hooks\":{}}}\n");
|
||||
}
|
||||
|
||||
static void buf_growth() {
|
||||
Buf b;
|
||||
std::string big(100000, 'z');
|
||||
b.put(big.data(), big.size());
|
||||
b.printf("%s", big.c_str()); // > 128 chars: takes the grow path
|
||||
CHECK_EQ(b.size(), static_cast<std::size_t>(200000));
|
||||
CHECK(b.ok());
|
||||
b.clear();
|
||||
CHECK_EQ(b.size(), static_cast<std::size_t>(0));
|
||||
// a 1 MiB bytes value inlines only when inline_max allows
|
||||
std::vector<std::uint8_t> mb(1 << 20, 7);
|
||||
Buf big1;
|
||||
emit_tv(big1, tv::bytes(mb.data(), mb.size(), 1 << 20));
|
||||
CHECK_EQ(big1.size(), static_cast<std::size_t>(2 * (1 << 20)) + std::string("{\"t\":\"bytes\",\"n\":1048576,\"sha256\":\"\",\"hex\":\"\"}").size() + 64);
|
||||
CHECK(big1.ok());
|
||||
}
|
||||
|
||||
// ---- oracle fixture: mirrored line for line in oracle_emit.py ----------------------------------
|
||||
|
||||
static std::vector<std::uint8_t> range_bytes(int n) {
|
||||
std::vector<std::uint8_t> v;
|
||||
for (int i = 0; i < n; ++i) v.push_back(static_cast<std::uint8_t>(i));
|
||||
return v;
|
||||
}
|
||||
|
||||
static void write_oracle_fixture(const std::string& path) {
|
||||
Buf out;
|
||||
Meta m;
|
||||
m.build = "shim-trace test";
|
||||
m.exe_sha256 = std::string(64, '0');
|
||||
m.started = "2026-09-07T00:00:00Z";
|
||||
m.inline_max = 256;
|
||||
HookPolicy pa;
|
||||
HookPolicy pb;
|
||||
pb.ftol = 1e-6;
|
||||
pb.ftol_kind = "rel";
|
||||
pb.ptr_exact = true;
|
||||
pb.unordered = {"ret.v.items"};
|
||||
m.hooks.emplace_back("Hook::A", pa);
|
||||
m.hooks.emplace_back("Mars::ParseBlock", pb);
|
||||
emit_meta(out, m);
|
||||
|
||||
{ // rec0: str/i32/ptr args, bool ret, inline bytes region before/after
|
||||
Record r;
|
||||
r.ts = 1000; r.hook = "CfgVar_RegisterKey"; r.mode = Mode::Trace; r.call_id = 0; r.thread = 4120; r.depth = 0;
|
||||
r.args.push_back(tv::str("R\xe9sum\xe9").named("key"));
|
||||
r.args.push_back(tv::i32(-3).named("value"));
|
||||
r.args.push_back(tv::ptr(static_cast<std::uintptr_t>(0x00a36fd0)).named("table"));
|
||||
r.ret = tv::boolean(true);
|
||||
auto fwd = range_bytes(16);
|
||||
auto rev = fwd;
|
||||
std::reverse(rev.begin(), rev.end());
|
||||
SideEntry s;
|
||||
s.name = "cfg_table"; s.before_kind = SideEntry::Value;
|
||||
s.before = tv::bytes(fwd.data(), fwd.size(), 256);
|
||||
s.after = tv::bytes(rev.data(), rev.size(), 256);
|
||||
r.side.push_back(s);
|
||||
emit_record(out, r);
|
||||
}
|
||||
{ // rec1: struct / list / set / enum / big ints / null / wstr
|
||||
Record r;
|
||||
r.ts = 1037; r.hook = "Manifest_Load"; r.mode = Mode::Trace; r.call_id = 1; r.thread = 4120;
|
||||
r.args.push_back(tv::str("Weapons/_weapons.txt").named("path"));
|
||||
r.ret = tv::i32(2).named("count");
|
||||
Tv reg = tv::struct_();
|
||||
std::vector<Tv> entries;
|
||||
for (unsigned id : {7u, 9u}) {
|
||||
Tv e = tv::struct_();
|
||||
e.add("id", tv::u32(id));
|
||||
e.add("name", tv::str(("w" + std::to_string(id) + ".weapon").c_str()));
|
||||
entries.push_back(e);
|
||||
}
|
||||
reg.add("entries", tv::list(entries));
|
||||
reg.add("deleted", tv::set({tv::u32(501), tv::u32(550), tv::u32(599)}));
|
||||
reg.add("flags", tv::enum_(2, "MF_0"));
|
||||
reg.add("big", tv::u64((1ull << 60) + 1));
|
||||
reg.add("small", tv::u64(5));
|
||||
reg.add("edge", tv::u64(1ull << 53));
|
||||
reg.add("neg", tv::i64(-(1ll << 60)));
|
||||
reg.add("nothing", tv::null());
|
||||
const std::uint16_t w[] = {0x41, 0x20ac, 0x0000};
|
||||
reg.add("w", tv::wstr(w, 3));
|
||||
SideEntry s;
|
||||
s.name = "registry";
|
||||
s.after = reg;
|
||||
r.side.push_back(s);
|
||||
emit_record(out, r);
|
||||
}
|
||||
{ // rec2: every escape, json ret, f32/f64 lists, nan, before:null, hashed region
|
||||
Record r;
|
||||
r.ts = 1074; r.hook = "Mars::ParseBlock"; r.mode = Mode::Trace; r.call_id = 2; r.thread = 4124; r.depth = 1;
|
||||
r.args.push_back(tv::str("Tab\tKey Quote\"d Back\\slash nl\nx cr\rx \x01\x7f\xff \x80").named("text"));
|
||||
r.args.push_back(tv::u32(2).named("len"));
|
||||
r.ret = tv::json("{\"weapon\":{\"count\":3,\"damage\":1.5,\"name\":\"Laser\\u00e9\",\"ok\":true,\"range\":2.25,\"tags\":[\"a\",\"b\"]}}");
|
||||
SideEntry sc;
|
||||
sc.name = "scratch";
|
||||
sc.after = tv::list({tv::f32(1.5f), tv::f32(0.1f), tv::f32(-2.5e-7f), tv::f32(1e30f)});
|
||||
r.side.push_back(sc);
|
||||
SideEntry nb;
|
||||
nb.name = "nanbox";
|
||||
nb.after = tv::f32(std::numeric_limits<float>::quiet_NaN());
|
||||
r.side.push_back(nb);
|
||||
SideEntry inf;
|
||||
inf.name = "infs";
|
||||
inf.after = tv::list({tv::f64(std::numeric_limits<double>::infinity()), tv::f64(-std::numeric_limits<double>::infinity()),
|
||||
tv::f64(0.1), tv::f64(1e300), tv::f64(0.0)});
|
||||
r.side.push_back(inf);
|
||||
auto big = range_bytes(256);
|
||||
big.insert(big.end(), big.begin(), big.end());
|
||||
SideEntry bl;
|
||||
bl.name = "blob"; bl.before_kind = SideEntry::IsNull;
|
||||
bl.after = tv::bytes(big.data(), big.size(), 256);
|
||||
r.side.push_back(bl);
|
||||
emit_record(out, r);
|
||||
}
|
||||
{ // rec3: compare, clean, small ints
|
||||
Record r;
|
||||
r.ts = 1111; r.hook = "CfgVar_RegisterKey"; r.mode = Mode::Compare; r.call_id = 3; r.thread = 4120;
|
||||
r.args = {tv::i8(-128), tv::i16(32767), tv::u8(255), tv::u16(65535), tv::u64((1ull << 53) - 1)};
|
||||
r.ret = tv::null();
|
||||
r.has_ours = true;
|
||||
r.ours_ret = tv::null();
|
||||
r.diverged = 0;
|
||||
r.has_diff = true;
|
||||
emit_record(out, r);
|
||||
}
|
||||
{ // rec4: compare with divergences + note
|
||||
Record r;
|
||||
r.ts = 1148; r.hook = "Manifest_Load"; r.mode = Mode::Compare; r.call_id = 4; r.thread = 1;
|
||||
r.ret = tv::i32(1);
|
||||
SideEntry s;
|
||||
s.name = "r";
|
||||
s.after = tv::bytes("\x01\x02", 2, 256);
|
||||
r.side.push_back(s);
|
||||
r.has_ours = true;
|
||||
r.ours_ret = tv::i32(2);
|
||||
r.ours_side.push_back({"r", tv::bytes("\x01\x03", 2, 256)});
|
||||
r.diverged = 1;
|
||||
r.has_diff = true;
|
||||
DiffEntry d1;
|
||||
d1.path = "ret"; d1.why = "exact"; d1.orig = tv::i32(1); d1.ours = tv::i32(2);
|
||||
r.diff.push_back(d1);
|
||||
DiffEntry d2;
|
||||
d2.path = "side.r.after"; d2.why = "hash"; d2.orig = tv::bytes("\x01\x02", 2, 256); d2.ours = tv::bytes("\x01\x03", 2, 256);
|
||||
d2.first_diff_offset = 1;
|
||||
r.diff.push_back(d2);
|
||||
r.note = "hello";
|
||||
emit_record(out, r);
|
||||
}
|
||||
{ // rec5: compare err (no ours)
|
||||
Record r;
|
||||
r.ts = 1185; r.hook = "X"; r.mode = Mode::Compare; r.call_id = 5; r.thread = 1;
|
||||
r.diverged = 1;
|
||||
r.has_diff = true;
|
||||
DiffEntry d;
|
||||
d.path = "call"; d.why = "err"; d.ours_raw = "\"ours threw\"";
|
||||
r.diff.push_back(d);
|
||||
r.err = "ours threw";
|
||||
emit_record(out, r);
|
||||
}
|
||||
{ // rec6: replace
|
||||
Record r;
|
||||
r.ts = 1222; r.hook = "Y"; r.mode = Mode::Replace; r.call_id = 6; r.thread = 2;
|
||||
r.args.push_back(tv::boolean(false).named("flag"));
|
||||
r.ret = tv::ptr(static_cast<std::uintptr_t>(0));
|
||||
emit_record(out, r);
|
||||
}
|
||||
CHECK(out.ok());
|
||||
std::FILE* f = std::fopen(path.c_str(), "wb");
|
||||
CHECK(f != nullptr);
|
||||
if (f) {
|
||||
std::fwrite(out.data(), 1, out.size(), f);
|
||||
std::fclose(f);
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
golden_strings();
|
||||
golden_numbers();
|
||||
golden_bytes_and_containers();
|
||||
golden_records();
|
||||
buf_growth();
|
||||
|
||||
const std::string fixture = argc > 1 ? argv[1] : "emitter_oracle.jsonl";
|
||||
write_oracle_fixture(fixture);
|
||||
std::printf("oracle fixture: %s\n", fixture.c_str());
|
||||
if (tracetest::harness_present()) {
|
||||
// byte-exact against mkfixture.emit_record / emit_meta
|
||||
const std::string script = argc > 2 ? argv[2] : "oracle_emit.py";
|
||||
const int rc = tracetest::run_python(script, tracetest::harness_dir() + " " + fixture);
|
||||
CHECK_EQ(rc, 0);
|
||||
// and the harness reads it back as valid, with the injected divergence counted
|
||||
const int tc = tracetest::run_tracecmp(fixture);
|
||||
CHECK_EQ(tc, 1);
|
||||
} else {
|
||||
std::printf(" SKIP python oracle (harness absent)\n");
|
||||
}
|
||||
return tracetest::finish("shim_trace_emitter");
|
||||
}
|
||||
213
tests/shim_trace/test_hook.cpp
Normal file
213
tests/shim_trace/test_hook.cpp
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
// End-to-end: the hook template in every mode over the self-test hook, the tracer writing a
|
||||
// real JSONL file, and tracecmp.py judging it (exit 0 clean / 1 divergence / 2 invalid).
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "check.h"
|
||||
#include "harness.h"
|
||||
#include "shim/trace/hook.h"
|
||||
#include "shim/trace/selftest.h"
|
||||
#include "shim/trace/tracer.h"
|
||||
|
||||
using namespace shim::trace;
|
||||
using namespace shim::selftest;
|
||||
|
||||
static std::string slurp(const std::string& p) {
|
||||
std::ifstream f(p, std::ios::binary);
|
||||
std::stringstream ss;
|
||||
ss << f.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
static std::vector<std::string> lines(const std::string& s) {
|
||||
std::vector<std::string> out;
|
||||
std::size_t start = 0;
|
||||
while (start < s.size()) {
|
||||
std::size_t nl = s.find('\n', start);
|
||||
if (nl == std::string::npos) nl = s.size();
|
||||
out.push_back(s.substr(start, nl - start));
|
||||
start = nl + 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static bool has(const std::string& hay, const char* needle) { return hay.find(needle) != std::string::npos; }
|
||||
|
||||
using HFill = Hook<FillHook>;
|
||||
using HWrong = Hook<FillWrongHook>;
|
||||
using HThrows = Hook<FillThrowsHook>;
|
||||
|
||||
static void wire() {
|
||||
HFill::original = &Fill;
|
||||
HWrong::original = &Fill;
|
||||
HThrows::original = &Fill;
|
||||
}
|
||||
|
||||
static std::uint32_t reference(std::uint32_t n, std::uint32_t seed) {
|
||||
std::vector<std::uint8_t> b(n);
|
||||
return Fill(b.data(), n, seed);
|
||||
}
|
||||
|
||||
// ---- without a tracer: modes still route correctly, nothing is written ----------------------------
|
||||
|
||||
static void modes_without_tracer() {
|
||||
wire();
|
||||
std::uint8_t buf[16];
|
||||
const std::uint32_t want = reference(16, 1);
|
||||
HFill::mode = Mode::Off;
|
||||
CHECK_EQ(HFill::detour()(buf, 16, 1), want);
|
||||
HFill::mode = Mode::Replace;
|
||||
CHECK_EQ(HFill::detour()(buf, 16, 1), want); // ours agrees with the original
|
||||
HWrong::mode = Mode::Replace;
|
||||
std::uint8_t wb[16];
|
||||
CHECK_EQ(HWrong::detour()(wb, 16, 1), want);
|
||||
CHECK_EQ(static_cast<int>(wb[15]), (static_cast<int>(buf[15]) + 1) & 0xff); // replace ran ours (the wrong one)
|
||||
HFill::mode = Mode::Trace;
|
||||
CHECK_EQ(HFill::detour()(buf, 16, 1), want); // tracer closed: record dropped, result intact
|
||||
HFill::mode = Mode::Compare;
|
||||
CHECK_EQ(HFill::detour()(buf, 16, 1), want);
|
||||
CHECK_EQ(Tracer::instance().records_written(), 0u);
|
||||
}
|
||||
|
||||
// ---- clean log: trace + compare (identical ours) + replace ------------------------------------------
|
||||
|
||||
static void clean_log(const std::string& path) {
|
||||
Config cfg;
|
||||
cfg.path = path;
|
||||
cfg.inline_max = 32;
|
||||
cfg.apply("hook.Shim::SelfTest::Fill", "trace");
|
||||
Tracer& tr = Tracer::instance();
|
||||
tr.configure(cfg);
|
||||
HFill::register_policy(tr);
|
||||
HWrong::register_policy(tr);
|
||||
CHECK(tr.open("shim_trace_test", std::string(64, 'a').c_str()));
|
||||
HFill::configure(tr);
|
||||
CHECK(HFill::mode == Mode::Trace);
|
||||
|
||||
std::uint8_t buf[64];
|
||||
CHECK_EQ(HFill::detour()(buf, 16, 7), reference(16, 7)); // trace, inline region
|
||||
CHECK_EQ(HFill::detour()(buf, 64, 8), reference(64, 8)); // trace, hashed region (64 > inline_max 32)
|
||||
HFill::mode = Mode::Compare;
|
||||
CHECK_EQ(HFill::detour()(buf, 16, 9), reference(16, 9)); // compare, clean
|
||||
CHECK_EQ(HFill::detour()(buf, 64, 10), reference(64, 10));
|
||||
CHECK_EQ(HFill::detour()(buf, 0, 11), reference(0, 11)); // empty region
|
||||
HFill::mode = Mode::Replace;
|
||||
CHECK_EQ(HFill::detour()(buf, 16, 12), reference(16, 12)); // replace: nothing emitted
|
||||
|
||||
// call ids are process-global and increase across threads; depth stays 0 per thread
|
||||
std::thread t([&] {
|
||||
std::uint8_t tb[8];
|
||||
HFill::mode = Mode::Trace;
|
||||
HFill::detour()(tb, 8, 13);
|
||||
});
|
||||
t.join();
|
||||
tr.close();
|
||||
CHECK_EQ(tr.records_written(), 6u);
|
||||
|
||||
const std::string text = slurp(path);
|
||||
const auto ls = lines(text);
|
||||
CHECK_EQ(ls.size(), static_cast<std::size_t>(7));
|
||||
CHECK(has(ls[0], "{\"meta\":{\"format\":1,\"build\":\"shim_trace_test\",\"exe_sha256\":\"aaaa"));
|
||||
CHECK(has(ls[0], "\"inline_max\":32,\"hooks\":{\"Shim::SelfTest::Fill\":{\"ftol\":0,\"ftol_kind\":\"abs\",\"ptr\":\"ignore\"},\"Shim::SelfTest::FillWrong\":{"));
|
||||
CHECK(has(ls[1], "\"hook\":\"Shim::SelfTest::Fill\",\"mode\":\"trace\",\"call_id\":"));
|
||||
CHECK(has(ls[1], "\"depth\":0,\"args\":[{\"t\":\"ptr\",\"v\":\"0x"));
|
||||
CHECK(has(ls[1], "\"n\":\"buf\"},{\"t\":\"u32\",\"v\":16,\"n\":\"n\"},{\"t\":\"u32\",\"v\":7,\"n\":\"seed\"}],\"ret\":{\"t\":\"u32\",\"v\":"));
|
||||
CHECK(has(ls[1], "\"side\":{\"buf\":{\"before\":{\"t\":\"bytes\",\"n\":16,\"sha256\":\""));
|
||||
CHECK(has(ls[1], "\"hex\":\""));
|
||||
CHECK(has(ls[2], "\"n\":64,\"sha256\":\""));
|
||||
CHECK(has(ls[2], "\"head\":\""));
|
||||
CHECK(!has(ls[2], "\"hex\":\""));
|
||||
CHECK(has(ls[3], "\"mode\":\"compare\""));
|
||||
CHECK(has(ls[3], "\"ours\":{\"ret\":{\"t\":\"u32\",\"v\":"));
|
||||
CHECK(has(ls[3], "\"diverged\":false,\"diff\":[]}"));
|
||||
CHECK(has(ls[5], "\"n\":0,\"sha256\":\"e3b0c442"));
|
||||
CHECK(!has(text, "\"mode\":\"replace\""));
|
||||
// the thread's record: different thread id, depth 0, a later call_id than the main thread's
|
||||
CHECK(has(ls[6], "\"mode\":\"trace\""));
|
||||
CHECK(has(ls[6], "\"v\":13,\"n\":\"seed\"}"));
|
||||
// ASCII only, LF only
|
||||
for (char c : text) CHECK(static_cast<unsigned char>(c) < 0x80 && c != '\r');
|
||||
|
||||
const int rc = tracetest::run_tracecmp(path);
|
||||
if (rc != tracetest::kSkipped) CHECK_EQ(rc, 0);
|
||||
}
|
||||
|
||||
// ---- divergent log: wrong ours (region differs), throwing ours (err) --------------------------------
|
||||
|
||||
static void bad_log(const std::string& path) {
|
||||
Config cfg;
|
||||
cfg.path = path;
|
||||
cfg.default_mode = Mode::Compare;
|
||||
Tracer& tr = Tracer::instance();
|
||||
tr.configure(cfg);
|
||||
CHECK(tr.open("shim_trace_test", ""));
|
||||
HWrong::configure(tr);
|
||||
HThrows::configure(tr);
|
||||
HFill::configure(tr);
|
||||
CHECK(HWrong::mode == Mode::Compare);
|
||||
|
||||
std::uint8_t buf[16];
|
||||
CHECK_EQ(HWrong::detour()(buf, 16, 21), reference(16, 21)); // caller still gets the original's result
|
||||
CHECK_EQ(static_cast<int>(buf[15]), static_cast<int>(buf[15])); // and the original's memory (ours wrote scratch)
|
||||
std::uint8_t ref[16];
|
||||
Fill(ref, 16, 21);
|
||||
CHECK(std::memcmp(buf, ref, 16) == 0);
|
||||
CHECK_EQ(HThrows::detour()(buf, 16, 22), reference(16, 22)); // a throwing ours is contained
|
||||
CHECK_EQ(HFill::detour()(buf, 16, 23), reference(16, 23)); // and a clean call after it is still clean
|
||||
tr.close();
|
||||
|
||||
const auto ls = lines(slurp(path));
|
||||
CHECK_EQ(ls.size(), static_cast<std::size_t>(4));
|
||||
CHECK(has(ls[1], "\"hook\":\"Shim::SelfTest::FillWrong\",\"mode\":\"compare\""));
|
||||
CHECK(has(ls[1], "\"diverged\":true,\"diff\":[{\"path\":\"side.buf.after\",\"why\":\"hash\",\"orig\":{\"t\":\"bytes\",\"n\":16"));
|
||||
CHECK(has(ls[1], "\"first_diff_offset\":15}]}"));
|
||||
CHECK(has(ls[2], "\"hook\":\"Shim::SelfTest::FillThrows\",\"mode\":\"compare\""));
|
||||
CHECK(!has(ls[2], ",\"ours\":{\"ret\"")); // err record: no ours block (the diff's "ours" key is the err text)
|
||||
CHECK(has(ls[2], "\"diverged\":true,\"diff\":[{\"path\":\"call\",\"why\":\"err\",\"orig\":null,\"ours\":\"ours: selftest: deliberate throw\"}],\"err\":\"ours: selftest: deliberate throw\"}"));
|
||||
CHECK(has(ls[3], "\"diverged\":false,\"diff\":[]}"));
|
||||
|
||||
const int rc = tracetest::run_tracecmp(path);
|
||||
if (rc != tracetest::kSkipped) CHECK_EQ(rc, 1);
|
||||
// --hook filter on the clean hook only -> clean
|
||||
const int rc2 = tracetest::run_tracecmp(path, "--hook Shim::SelfTest::Fill");
|
||||
if (rc2 != tracetest::kSkipped) CHECK_EQ(rc2, 0);
|
||||
}
|
||||
|
||||
// ---- an unusable log (truncated last line) is exit 2 --------------------------------------------------
|
||||
|
||||
static void truncated_log(const std::string& src, const std::string& dst) {
|
||||
std::string text = slurp(src);
|
||||
text.resize(text.size() - 10);
|
||||
std::FILE* f = std::fopen(dst.c_str(), "wb");
|
||||
if (f) {
|
||||
std::fwrite(text.data(), 1, text.size(), f);
|
||||
std::fclose(f);
|
||||
}
|
||||
const int rc = tracetest::run_tracecmp(dst);
|
||||
if (rc != tracetest::kSkipped) CHECK_EQ(rc, 2);
|
||||
const int rc2 = tracetest::run_tracecmp(dst, "--skip-invalid");
|
||||
if (rc2 != tracetest::kSkipped) CHECK_EQ(rc2, 0);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
const std::string dir = argc > 1 ? argv[1] : ".";
|
||||
modes_without_tracer();
|
||||
clean_log(dir + "/hook_clean.jsonl");
|
||||
bad_log(dir + "/hook_bad.jsonl");
|
||||
truncated_log(dir + "/hook_clean.jsonl", dir + "/hook_truncated.jsonl");
|
||||
// the packaged self-test entry point used by the shim at startup
|
||||
Config cfg;
|
||||
cfg.path = dir + "/selftest.jsonl";
|
||||
Tracer::instance().configure(cfg);
|
||||
CHECK(Tracer::instance().open("x", ""));
|
||||
CHECK_EQ(run_once(Mode::Compare), reference(64, 0x5eed));
|
||||
Tracer::instance().close();
|
||||
const int rc = tracetest::run_tracecmp(dir + "/selftest.jsonl");
|
||||
if (rc != tracetest::kSkipped) CHECK_EQ(rc, 0);
|
||||
return tracetest::finish("shim_trace_hook");
|
||||
}
|
||||
57
tests/shim_trace/test_sha256.cpp
Normal file
57
tests/shim_trace/test_sha256.cpp
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
#include "shim/trace/sha256.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#include "check.h"
|
||||
|
||||
using shim::Sha256;
|
||||
|
||||
static std::string hex_of(const std::string& s) {
|
||||
char h[65];
|
||||
Sha256::digest_hex(s.data(), s.size(), h);
|
||||
return h;
|
||||
}
|
||||
|
||||
int main() {
|
||||
// FIPS 180-2 vectors + the harness's empty-string value (mkfixture test_float_and_bigint_forms).
|
||||
CHECK_STR(hex_of(""), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
|
||||
CHECK_STR(hex_of("abc"), "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
|
||||
CHECK_STR(hex_of("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"),
|
||||
"248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1");
|
||||
CHECK_STR(hex_of(std::string(1000000, 'a')), "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0");
|
||||
CHECK_STR(hex_of(std::string("\x00\xff", 2)), "06eb7d6a69ee19e5fbdf749018d3d2abfa04bcbd1365db312eb86dc7169389b8");
|
||||
|
||||
// Incremental updates across block boundaries equal the one-shot digest.
|
||||
const std::string big(1000000, 'a');
|
||||
Sha256 s;
|
||||
std::size_t pos = 0;
|
||||
const std::size_t steps[] = {1, 63, 64, 65, 1000, 4096, 55, 56, 57, 100000};
|
||||
while (pos < big.size()) {
|
||||
for (std::size_t st : steps) {
|
||||
const std::size_t n = (pos + st > big.size()) ? big.size() - pos : st;
|
||||
s.update(big.data() + pos, n);
|
||||
pos += n;
|
||||
if (pos >= big.size()) break;
|
||||
}
|
||||
}
|
||||
std::uint8_t d[32];
|
||||
s.finish(d);
|
||||
char h[65];
|
||||
Sha256::to_hex(d, h);
|
||||
CHECK_STR(std::string(h), "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0");
|
||||
|
||||
// Exactly 55 / 56 / 64 bytes (padding edge cases) agree with themselves chunked.
|
||||
for (std::size_t n : {55u, 56u, 63u, 64u, 65u, 119u, 120u, 128u}) {
|
||||
const std::string m(n, 'x');
|
||||
Sha256 a;
|
||||
a.update(m.data(), 20);
|
||||
a.update(m.data() + 20, n - 20);
|
||||
std::uint8_t da[32];
|
||||
a.finish(da);
|
||||
std::uint8_t db[32];
|
||||
Sha256::digest(m.data(), n, db);
|
||||
CHECK(std::memcmp(da, db, 32) == 0);
|
||||
}
|
||||
return tracetest::finish("shim_trace_sha256");
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue