GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 100.0% 11 / 0 / 11
Functions: 100.0% 3 / 0 / 3
Branches: -% 0 / 0 / 0

include/DetourModKit/manifest.hpp
Line Branch Exec Source
1 #ifndef DETOURMODKIT_MANIFEST_HPP
2 #define DETOURMODKIT_MANIFEST_HPP
3
4 /**
5 * @file manifest.hpp
6 * @brief Signature manifest: the resolved patch-fragile contract as editable data, so a broken mod is a text edit.
7 * @details SignatureRecord owns serializable anchor evidence and its consumer binding. Signature compiles candidate
8 * ladders into owned storage. @ref resolve_and_gate resolves those contracts, checks their fingerprints and
9 * quality, and safe-disables unresolved or drifted entries before a wrong register or offset can be consumed.
10 * @note The file format is a separate INI parsed by the already-linked simpleini, never the settings INI. The parser
11 * and emitter live entirely in the implementation; this header names no INI type.
12 * @warning `[B-100]` Never parse, compile, resolve, gate, or derive a scope under the loader lock. Pure value
13 * accessors on a compiled @ref Signature do not allocate or query the loader.
14 */
15
16 #include "DetourModKit/anchor.hpp"
17 #include "DetourModKit/error.hpp"
18 #include "DetourModKit/region.hpp"
19 #include "DetourModKit/scan.hpp"
20
21 #include <cstddef>
22 #include <cstdint>
23 #include <filesystem>
24 #include <limits>
25 #include <span>
26 #include <string>
27 #include <string_view>
28 #include <vector>
29
30 namespace DetourModKit
31 {
32 // Forward-declare the one hook:: type a Binding names: a scoped enum with a fixed underlying type is complete
33 // enough to declare a member, so a consumer reading only Address / PointerChain bindings need not pull the whole
34 // hooking surface into every manifest TU. The full definition is reached only where the register is used.
35 namespace hook
36 {
37 enum class Gpr : std::uint8_t;
38 }
39
40 namespace manifest
41 {
42 namespace detail
43 {
44 class GateAccess;
45 } // namespace detail
46
47 /**
48 * @enum BindingKind
49 * @brief How a consumer interprets what a signature located. This is the register / offset / vtable repair
50 * surface.
51 * @details The @ref anchor backends locate the address and the binding says what to read there. The binding
52 * itself is inert. This module resolves the address and returns the binding, and the consumer
53 * performs the register read (@ref hook::gpr), the pointer-chain walk (@ref memory::walk with
54 * @ref memory::read), or the virtual-method hook (@ref hook::VmtHook::hook_method).
55 */
56 enum class BindingKind : std::uint8_t
57 {
58 /// The resolved value IS the address the mod wants (an inline-hook target or a resolved global).
59 Address,
60 /// The resolved value is a chain base: walk @ref Binding::offsets, then read value_width bytes.
61 PointerChain,
62 /// The resolved value is a mid-hook site; the callback reads @ref Binding::read_register via hook::gpr.
63 MidHookRegister,
64 /// The resolved value is a vtable base; hook the virtual slot at @ref Binding::vmt_index.
65 VmtMethod
66 };
67
68 /// Sentinel for @ref Binding::xmm_index: no XMM register bound (the site reads a GPR, not a float slot).
69 inline constexpr std::uint8_t XMM_INDEX_UNUSED = 0xFF;
70
71 /**
72 * @struct Binding
73 * @brief The consumer-facing interpretation of a resolved signature: which register, which offset chain, which
74 * slot.
75 * @details Only the fields the active @ref kind uses are meaningful. The rest keep their defaults, which is
76 * the designated-initializer discipline @ref anchor::Anchor follows.
77 */
78 struct Binding
79 {
80 /// How to interpret the resolved value.
81 BindingKind kind = BindingKind::Address;
82 /// PointerChain: byte offsets walked left to right from the resolved base (@ref memory::walk semantics).
83 std::vector<std::ptrdiff_t> offsets;
84 /// PointerChain: byte width of the leaf read at the end of the walk (e.g. 4 for a float, 8 for a pointer).
85 std::uint8_t value_width = 8;
86 /// MidHookRegister: the register the mid-hook callback reads (edit after a rcx -> rax drift).
87 hook::Gpr read_register{};
88 /// MidHookRegister: an XMM lane for a float site, or @ref XMM_INDEX_UNUSED when the value lives in a GPR.
89 std::uint8_t xmm_index = XMM_INDEX_UNUSED;
90 /// VmtMethod: the zero-based virtual-table slot to hook; valid values are 0 through 4095.
91 std::size_t vmt_index = 0;
92 };
93
94 /**
95 * @struct CandidateSpec
96 * @brief One candidate-ladder rung in owning, text-editable form; compiled into a @ref scan::Candidate at load.
97 * @details The serializable twin of a @ref scan::Candidate, which owns compiled Pattern bytes that no author
98 * can edit by hand. For a byte-tier rung the file carries the source AOB string and decode
99 * parameters, and @ref Signature::compile turns them back into a @ref scan::Candidate. Only the
100 * fields the active @ref mode uses are read.
101 */
102 struct CandidateSpec
103 {
104 /// Human-readable rung name, carried into the winning @ref scan::Hit for diagnostics.
105 std::string name;
106 /// Which resolution strategy this rung uses.
107 scan::Mode mode = scan::Mode::Direct;
108 /// Direct / RipRelative: the AOB DSL string, e.g. "48 8B 05 ?? ?? ?? ??".
109 std::string pattern;
110 /// Direct: signed byte delta added to the match (negative walks backward); 0 returns the match itself.
111 std::ptrdiff_t walk_back = 0;
112 /// RipRelative: byte offset from the match to the signed 4-byte displacement field.
113 std::ptrdiff_t displacement_at = 0;
114 /// RipRelative: total length of the referencing instruction (the next-IP base for the displacement).
115 std::size_t instruction_length = 0;
116 /// RttiVtable: the MSVC mangled type name, e.g. ".?AVCameraManager@@".
117 std::string mangled;
118 /// StringXref: the exact literal content to anchor on (no quotes).
119 std::string string_text;
120 /// StringXref: how the literal is stored in the image. Utf16le follows the @ref SignatureRecord rule.
121 scan::StringEncoding string_encoding = scan::StringEncoding::Utf8;
122 /// StringXref: whether to return the referencing instruction, its enclosing function, or the pointer slot.
123 scan::XrefReturn string_return = scan::XrefReturn::ReferencingInstruction;
124 /// StringXref: match a trailing NUL so a prefix of a longer literal is not matched.
125 bool string_require_terminator = true;
126 /// StringXref: keep the lea/mov shape scan and add the broad Zydis sweep for rarer reference shapes.
127 bool string_broad_match = false;
128 };
129
130 /**
131 * @struct SignatureRecord
132 * @brief An owning, serializable superset of @ref anchor::Anchor plus its @ref Binding: the unit an INI file
133 * round-trips.
134 * @details Where @ref anchor::Anchor is a static aggregate of non-owning views authored in code, a
135 * SignatureRecord owns every string and ladder rung so it survives being read from a file and stored.
136 * Only the fields the active @ref kind uses are meaningful (the RipGlobal / CodeOperand ladder, the
137 * VtableIdentity mangled name, the StringXref facets, the ExportName module + export name, or the
138 * Manual literal); the rest keep their defaults. The two composite anchor kinds
139 * @ref anchor::AnchorKind::Quorum and
140 * @ref anchor::AnchorKind::CallArgHome are deliberately not serializable here: a Quorum composes
141 * voting members by pointer and CallArgHome has no resolver, so both stay in-code constructs gated
142 * through @ref anchor::evaluate_gate rather than the file.
143 */
144 struct SignatureRecord
145 {
146 /// Stable merge / lookup key, e.g. "player.health"; echoed into the drift report and the gate result.
147 std::string label;
148 /// Which anchor backend resolves this signature (one of the six serializable kinds).
149 anchor::AnchorKind kind = anchor::AnchorKind::RipGlobal;
150 /**
151 * @brief Empty resolves within the host image (or the fallback scope); otherwise a module basename scoped
152 * through @ref Region::module_named. For ExportName this names the module whose export table holds
153 * @ref export_name.
154 */
155 std::string module;
156
157 /// RipGlobal / CodeOperand: the candidate ladder resolving to the address or the instruction site.
158 std::vector<CandidateSpec> ladder;
159
160 /// VtableIdentity: the MSVC mangled type name to resolve through the reverse-RTTI walk.
161 std::string mangled;
162
163 /// CodeOperand: whether to read an immediate or a memory-operand displacement.
164 scan::OperandKind operand_kind = scan::OperandKind::Immediate;
165 /// CodeOperand: index into the instruction's visible operands.
166 std::uint8_t operand_index = 0;
167 /// CodeOperand: 0 preserves the decoded value; 1 through 8 narrows non-RIP low bytes and sign-extends.
168 std::uint8_t byte_width = 0;
169
170 /// StringXref: the exact literal content to anchor on (no quotes).
171 std::string xref_text;
172 /**
173 * @brief StringXref: byte encoding of the literal in the image (Utf16le for wchar_t literals).
174 * @details Utf16le evidence must contain well-formed UTF-8 because resolution converts it to UTF-16LE.
175 * @ref parse, @ref Signature::compile, @ref Signature::adopt, and @ref serialize_checked reject
176 * malformed text. Utf8 evidence remains byte-transparent.
177 */
178 scan::StringEncoding xref_encoding = scan::StringEncoding::Utf8;
179 /// StringXref: whether to return the referencing instruction, its enclosing function, or the pointer slot.
180 scan::XrefReturn xref_return = scan::XrefReturn::ReferencingInstruction;
181 /// StringXref: match a trailing NUL so a prefix of a longer literal is not matched.
182 bool xref_require_terminator = true;
183 /// StringXref: keep the lea/mov shape scan and add the broad Zydis sweep for rarer reference shapes.
184 bool xref_broad_match = false;
185
186 /// Manual: the pinned literal value, taken as-is.
187 std::int64_t manual_value = 0;
188
189 /**
190 * @brief Optional post-resolve validator threaded onto the compiled @ref anchor::Anchor, mirroring @ref
191 * anchor::Anchor::validator. In-memory only: a function pointer cannot round-trip through an INI
192 * file, so @ref parse never populates it and @ref serialize_checked never writes it. A consumer
193 * attaches it programmatically (after loading a manifest, or on a hand-built record) so a
194 * file-loaded or adopted signature can still assert a domain invariant instead of trusting the raw
195 * resolved address.
196 */
197 anchor::AnchorValidator validator = nullptr;
198 /**
199 * @brief Opaque pointer forwarded verbatim to @ref validator.
200 * @details This field is in-memory only.
201 * The pointer is copied, but its pointee remains borrowed.
202 * The consumer must keep that pointee valid during each signature resolve.
203 */
204 const void *validator_context = nullptr;
205 /**
206 * @brief Run @ref validator on a Manual anchor too, instead of taking the pinned literal unchecked.
207 * @details In-memory only.
208 */
209 bool validate_manual = false;
210 /// Reject a backend-resolvable anchor that carries no @ref validator (fails closed). In-memory only.
211 bool require_validator = false;
212
213 /// How the consumer interprets the resolved value.
214 Binding binding{};
215
216 /**
217 * @brief The @ref anchor::anchor_fingerprint captured when authored. A zero value means "not captured yet".
218 * @details The fingerprint is a content hash of the signature's own declarative definition: its locate
219 * evidence (pattern bytes / mangled name / xref literal), its @ref Binding contract, and its
220 * label and module scope. It never reads the game's code. Persist it so the gate can distinguish
221 * a relocated target from a signature edit. A relocation retains the same fingerprint. A
222 * signature edit without a new baseline changes it. A value of 0 reports as "unknown", never as
223 * "drifted", so an author without a baseline is not falsely rejected.
224 */
225 std::uint64_t expected_fingerprint = 0;
226
227 /**
228 * @brief RipGlobal: page-protection class for byte-tier candidates. Defaults to @ref scan::Pages::Readable
229 * for backward-compatible data-global resolution; set @ref scan::Pages::Executable when every rung
230 * anchors on an instruction. Serialized as the optional `pages` key for RipGlobal records only.
231 * @details Ignored by other kinds.
232 */
233 scan::Pages pages = scan::Pages::Readable;
234
235 /**
236 * @brief ExportName: the exact, case-sensitive export symbol name (no decoration), e.g. "Sleep". The owning
237 * module is the shared @ref module field (empty resolves the export within the fallback scope).
238 * @details Serialized as the `export_name` key for ExportName records only; ignored by other kinds.
239 */
240 std::string export_name;
241
242 /**
243 * @brief The optional live-image baseline captured for this signature.
244 * @details Serialized as `image_identity` when present. A configured identity gate rejects a captured
245 * baseline that does not match the resolved image.
246 */
247 scan::ImageIdentity expected_image_identity{};
248
249 /**
250 * @brief The optional winning-span content baseline captured for this signature.
251 * @details Serialized as `winning_bytes`, a lowercase hex string of the captured span. This is the only
252 * baseline that sees target CONTENT: @ref expected_fingerprint hashes the record's own
253 * declaration and @ref expected_image_identity folds PE header fields, so an in-place code patch
254 * that preserves the section table moves this and neither of the others. Only a byte-signature
255 * rung can produce one.
256 */
257 scan::WinningEvidence expected_winning_bytes{};
258 };
259
260 /**
261 * @enum FingerprintState
262 * @brief The drift verdict for one signature: no baseline, the declared definition is unchanged, or it changed.
263 */
264 enum class FingerprintState : std::uint8_t
265 {
266 /// No baseline was captured (@ref SignatureRecord::expected_fingerprint is 0); drift cannot be judged.
267 Unset,
268 /// The live fingerprint equals the captured baseline: the signature's declared definition is unchanged.
269 Match,
270 /// The live fingerprint differs from the baseline (see @ref SignatureRecord::expected_fingerprint).
271 Drifted
272 };
273
274 /**
275 * @class Signature
276 * @brief A compiled, resolvable signature: owns its candidate storage and presents an @ref anchor::Anchor view.
277 * @details The bridge from the owning, serializable @ref SignatureRecord to the borrowed @ref anchor::Anchor
278 * the engine resolves. It owns the compiled ladder (a std::vector<scan::Candidate>) and the record's
279 * owned strings, and it rebuilds a borrowed @ref anchor::Anchor on demand rather than
280 * caching one, so moving a Signature can never leave a stored view dangling - the same discipline
281 * @ref scan::OwnedScanRequest::view uses. Construct one from a file record with @ref compile, or adopt
282 * an in-code anchor with @ref adopt.
283 */
284 class Signature
285 {
286 public:
287 /**
288 * @brief Compiles a file record into a resolvable signature, failing closed on an uncompilable rung.
289 * @param record The owning record (moved in; its strings back the resolved anchor view).
290 * @return The compiled Signature, or an Error: BadPattern (a ladder rung's AOB failed to compile),
291 * EmptyCandidates (a RipGlobal / CodeOperand record with no ladder), or InvalidArg (a record whose
292 * kind is the non-serializable Quorum / CallArgHome / Unset, whose kind's required evidence is
293 * empty, whose persisted policy fields (including CodeOperand byte_width) are out of range, whose
294 * label or string fields that cannot round-trip through the file grammar, whose Utf16le string
295 * evidence breaks the @ref SignatureRecord::xref_encoding rule, or whose binding carries a
296 * non-default value in a field its @ref BindingKind never reads).
297 * @note Setup/control-plane only: compiling a ladder parses each rung's Pattern.
298 */
299 [[nodiscard]] static Result<Signature> compile(SignatureRecord record);
300
301 /**
302 * @brief Adopts an in-code @ref anchor::Anchor and owns its evidence.
303 * @param source The in-code anchor.
304 * The function copies its borrowed views.
305 * @return The owning Signature, or an Error: InvalidArg (a Quorum, CallArgHome, or Unset anchor, a
306 * serializable anchor whose required evidence is empty, an out-of-range persisted policy field
307 * (including CodeOperand byte_width), a label or string field that cannot round-trip through
308 * the file grammar, or Utf16le string evidence that breaks the
309 * @ref SignatureRecord::xref_encoding rule).
310 * @details The counterpart to @ref compile for a signature that originates in code rather than a file. It
311 * copies the anchor's borrowed site candidates and strings into this object so the adopted
312 * signature outlives the caller's anchor table. The resulting record carries no ladder text (a
313 * compiled Pattern cannot be turned back into its source AOB), so @ref serialize_checked of an
314 * adopted signature's record omits its ladder; capture a fresh record from the file side to
315 * serialize it.
316 * @note Setup/control-plane only: the adoption copies the anchor's evidence into owned storage.
317 */
318 [[nodiscard]] static Result<Signature> adopt(const anchor::Anchor &source);
319
320 /**
321 * @brief Resolves this signature to a value through its anchor backend, fail-closed.
322 * @param fallback_scope The module image to resolve within when the record names no module; defaults to the
323 * host executable. A record that names a module always resolves within that module,
324 * ignoring this argument.
325 * @return A @ref anchor::ResolvedAnchor carrying the outcome and (on success) the value.
326 * @note Setup/control-plane only (see @ref anchor::resolve).
327 */
328 [[nodiscard]] anchor::ResolvedAnchor resolve(Region fallback_scope = Region::host()) const;
329
330 /**
331 * @brief The effective scope this signature resolves within.
332 * @return @ref Region::module_named for the record's module, or @ref Region::host when it names none.
333 * @note Setup/control-plane only: queries the loader.
334 */
335 [[nodiscard]] Region scope() const noexcept;
336
337 /**
338 * @brief The live fingerprint of this signature, recomputed from its current declarative inputs.
339 * @return A content hash over the signature's declared definition: the @ref anchor::anchor_fingerprint of
340 * the locate evidence (compiled ladder, mangled name, xref literal) combined with the @ref Binding
341 * contract (register / offset chain / value width / vtable slot), the record label, and the module
342 * scope @ref resolve walks.
343 * @details Content-derived and address-independent: it reads no game memory, so it is stable across runs
344 * and rebuilds on one platform and changes exactly when the signature's declared definition
345 * changes - a re-authored pattern, a renamed type, a different literal, or an edited binding.
346 */
347 [[nodiscard]] std::uint64_t current_fingerprint() const noexcept;
348
349 /**
350 * @brief Compares the live fingerprint to the captured baseline.
351 * @return @ref FingerprintState::Unset when no baseline was captured, @ref FingerprintState::Match when the
352 * declared definition is unchanged, else @ref FingerprintState::Drifted.
353 */
354 [[nodiscard]] FingerprintState fingerprint_state() const noexcept;
355
356 /**
357 * @brief Adopts the live fingerprint as the new baseline, after a verified repair.
358 * @details Call this once a hand edit (new pattern, moved register, shifted offset) has been confirmed
359 * correct, so the gate trusts the repaired signature again on the next run. Persist the updated
360 * @ref record afterward to make the recapture durable.
361 * @note Setup/control-plane only: the recapture mutates the trust baseline.
362 */
363 void recapture_fingerprint() noexcept;
364
365 /**
366 * @brief Re-resolves this signature and adopts the live fingerprint, image identity, and winning-span
367 * content as the new baselines.
368 * @param fallback_scope The default module image for a signature that names no module; must be the same
369 * scope the consumer will gate under, since a baseline captured in one scope does not
370 * describe another.
371 * @return Nothing on success, or an Error explaining why no baseline was adopted.
372 * @details The recapture @ref GatePolicy::mutation_strict needs: it is the only operation that fills
373 * @ref SignatureRecord::expected_image_identity and
374 * @ref SignatureRecord::expected_winning_bytes from live evidence.
375 *
376 * Atomic: every baseline is computed before any is stored, so a failure leaves all three at their
377 * previous values rather than a half-updated mixture that would gate on one game version's
378 * content and another's identity. Fails with @ref ErrorCode::NoMatch when the signature does not
379 * resolve, and with @ref ErrorCode::UnexpectedShape when the resolved rung witnesses no owning
380 * image or no usable content span - an RTTI, export, string-xref, or Manual kind, or evidence
381 * longer than @ref scan::MAX_MUTATION_WITNESS_BYTES. Persist @ref record afterward to make it
382 * durable.
383 * @note Setup/control-plane only: re-resolving walks the signature's scope.
384 */
385 [[nodiscard]] Result<void> recapture(Region fallback_scope = Region::host());
386
387 /// The signature's stable key.
388 [[nodiscard]] std::string_view label() const noexcept;
389 /// Which anchor backend resolves this signature.
390 [[nodiscard]] anchor::AnchorKind kind() const noexcept;
391 /// The consumer-facing binding (register / offsets / vtable slot).
392 [[nodiscard]] const Binding &binding() const noexcept;
393 /// The owning record backing this signature (for @ref serialize_checked after @ref recapture_fingerprint).
394 [[nodiscard]] const SignatureRecord &record() const noexcept;
395
396 private:
397 friend class detail::GateAccess;
398
399 // The two factories are the only construction path: compile() parses a record's ladder text into m_ladder,
400 // adopt() copies an anchor's site into m_ladder, and both keep the owning record so make_anchor() can view
401 // its strings. The compiled ladder is stored separately from the record's text ladder because the resolver
402 // needs scan::Candidate objects, which are not what the file round-trips.
403 Signature(SignatureRecord record, std::vector<scan::Candidate> ladder) noexcept;
404
405 // Builds a borrowed anchor::Anchor viewing this object's owned storage. Rebuilt on demand (never cached) so
406 // no view outlives a move of *this; the returned Anchor is valid only for the duration of the call it
407 // feeds.
408 [[nodiscard]] anchor::Anchor make_anchor() const noexcept;
409
410 // Resolves through the private provenance path and returns the selected match span for the mutation gate.
411 [[nodiscard]] anchor::ResolvedAnchor resolve_for_gate(Region fallback_scope, Region &winning_span) const;
412
413 SignatureRecord m_record;
414 std::vector<scan::Candidate> m_ladder;
415 };
416
417 /// The manifest INI format version this build reads and writes. Bumped only on an incompatible format change.
418 inline constexpr std::uint32_t SCHEMA_VERSION = 1;
419
420 /**
421 * @struct ManifestHeader
422 * @brief The `[manifest]` metadata: the DetourModKit parse-format schema and the author's contract revision.
423 * @details Two independent version axes. @ref schema is the file-format version, which states whether this
424 * build can parse the file at all. @ref parse rejects a schema it does not understand. @ref revision
425 * is the mod author's own signature-contract epoch, bumped only when an in-code change makes older
426 * manifests incompatible (a renamed label, a re-meaning of a binding, a dropped signature).
427 * DetourModKit never interprets @ref revision; a consumer compares it to its build's expected value
428 * through @ref revision_compatible and safe-ignores a stale file. This catches staleness the
429 * per-signature fingerprint gate cannot, such as a renamed label or a changed meaning for an existing
430 * binding.
431 */
432 struct ManifestHeader
433 {
434 /// The format version the file declares; @ref parse rejects a value this build cannot read.
435 std::uint32_t schema = SCHEMA_VERSION;
436 /// The author's signature-contract epoch (0 = unversioned); compared to a build revision, never by DMK.
437 std::uint32_t revision = 0;
438 };
439
440 /**
441 * @struct Manifest
442 * @brief A parsed manifest: its @ref ManifestHeader plus the signature records in file order.
443 */
444 struct Manifest
445 {
446 /// The `[manifest]` metadata (schema and contract revision).
447 ManifestHeader header{};
448 /// The signatures, one per `[sig.<label>]` section, in file order.
449 std::vector<SignatureRecord> records{};
450 };
451
452 /**
453 * @struct ManifestLimits
454 * @brief The resource caps the manifest parser and checked persistence functions enforce.
455 * @details A default-constructed value is @ref conservative(). Trusted authoring tools may opt into
456 * @ref advanced(); untrusted files must use bounded limits. A violation returns
457 * @ref ErrorCode::SizeTooLarge without publishing a partial result.
458 */
459 struct ManifestLimits
460 {
461 /// Largest accepted encoded text size in bytes.
462 std::size_t max_file_bytes{1u << 20};
463 /// Largest accepted number of INI sections (header, records, and rung sub-sections combined).
464 std::size_t max_sections{1u << 15};
465 /// Largest accepted number of keys within any one section.
466 std::size_t max_keys_per_section{64};
467 /// Largest accepted number of `[sig.<label>]` records.
468 std::size_t max_records{512};
469 /// Largest accepted number of candidate-ladder rungs on any one record.
470 std::size_t max_rungs_per_record{32};
471 /// Largest accepted size in bytes of any single string field or heredoc value.
472 std::size_t max_field_bytes{64u << 10};
473 /// Largest accepted sum of all decoded value bytes across the manifest.
474 std::size_t max_total_decoded_bytes{4u << 20};
475
476 /// Returns limits equal to a default-constructed @ref ManifestLimits.
477 376 [[nodiscard]] static constexpr ManifestLimits conservative() noexcept { return ManifestLimits{}; }
478
479 /**
480 * @brief Raises every numeric cap to its maximum while retaining grammar and semantic validation.
481 * @return Limits intended only for a trusted authoring tool, never for an untrusted file.
482 */
483 [[nodiscard]] static constexpr ManifestLimits advanced() noexcept
484 {
485 // Parenthesized because public headers must compile with <windows.h>'s function-like max macro active.
486 constexpr std::size_t MAX_VALUE = (std::numeric_limits<std::size_t>::max)();
487 return ManifestLimits{
488 .max_file_bytes = MAX_VALUE,
489 .max_sections = MAX_VALUE,
490 .max_keys_per_section = MAX_VALUE,
491 .max_records = MAX_VALUE,
492 .max_rungs_per_record = MAX_VALUE,
493 .max_field_bytes = MAX_VALUE,
494 .max_total_decoded_bytes = MAX_VALUE,
495 };
496 }
497 };
498
499 /**
500 * @brief Reports whether a manifest may be applied under a build's signature-contract revision.
501 * @param header The parsed manifest header.
502 * @param build_revision The revision this build authored its in-code signatures against; 0 disables the check.
503 * @return true when @p build_revision is 0 (the consumer opts out of revision gating) or the manifest's
504 * @ref ManifestHeader::revision equals it; false when the file targets a different contract epoch.
505 * @details Bump @p build_revision (and the file's `revision`) only on an incompatible contract change (see
506 * @ref ManifestHeader). On a false result a consumer logs and falls back to its in-code defaults.
507 */
508 [[nodiscard]] bool revision_compatible(const ManifestHeader &header, std::uint32_t build_revision) noexcept;
509
510 /**
511 * @brief Parses a manifest's INI text.
512 * @param text The manifest text (a `[manifest]` header plus one `[sig.<label>]` section per contract).
513 * @param limits The resource caps to enforce; the default is @ref ManifestLimits::conservative().
514 * @return The parsed @ref Manifest (header plus records in file order), or an Error: MissingHeader (no
515 * `[manifest]` section or an unsupported schema), MalformedLine (a line, field, or enum token that
516 * does not parse, a noncomment key line without `=`, an empty key, a non-canonical section or key
517 * spelling, a key that is inert for its record's declared binding kind or its rung's mode, or Utf16le
518 * string evidence that breaks the @ref SignatureRecord::xref_encoding rule), ManifestIdentityCollision
519 * (a case-, whitespace-, or
520 * exactly-duplicated section, or a whitespace-variant or exactly-duplicated key, but a miscased key is
521 * MalformedLine before collision detection), ManifestFramingUnsafe (an unterminated `<<<` heredoc
522 * value, an opener with an empty tag, or a heredoc whose first body line is its terminator),
523 * SizeTooLarge (encoded text, a section, key, field, record, rung, or aggregate exceeding @p limits),
524 * or OutOfMemory (an allocation failed).
525 * @details Fails closed: a manifest that cannot be trusted to describe the signatures faithfully is rejected
526 * whole, never partially applied. A raw prepass rejects every identity collision before the
527 * case-sensitive backend reads the text, so no merged or swallowed record can masquerade as another.
528 * A missing optional key falls back to its default, so an absent `revision` is 0. A key that is
529 * present must parse, so a blank enum, numeric, or boolean value is MalformedLine instead of a
530 * default. A blank string-valued key reads as empty.
531 * @note Setup/control-plane only: parses and allocates bounded manifest state.
532 */
533 [[nodiscard]] Result<Manifest>
534 parse(std::string_view text, const ManifestLimits &limits = ManifestLimits::conservative());
535
536 /**
537 * @brief Serializes a manifest to INI text, rejecting anything that could not round-trip.
538 * @param manifest The header (its @ref ManifestHeader::revision is emitted when non-zero) and records to emit.
539 * @param limits The resource caps to enforce; the default is @ref ManifestLimits::conservative().
540 * @return The manifest text, round-trippable through @ref parse, or an Error: InvalidArg (a record whose label
541 * or a string field cannot be framed, an out-of-range persisted policy field (including CodeOperand
542 * byte_width), a binding carrying a non-default inert field, or Utf16le string evidence that breaks
543 * the @ref SignatureRecord::xref_encoding rule), ManifestIdentityCollision (two records
544 * whose labels fold to one section, or a record whose label folds into another record's rung section),
545 * SizeTooLarge (encoded text, a record, rung, field, or aggregate exceeding @p limits), or OutOfMemory.
546 * The `schema` line always reflects this build's @ref SCHEMA_VERSION.
547 * @details The single encoder: @ref save routes through it, so a value that a later @ref parse could not read
548 * back is refused at write time rather than persisted. A rejection is a typed error, never an empty or
549 * truncated string.
550 * @note Setup/control-plane only: validates and allocates bounded manifest text.
551 */
552 [[nodiscard]] Result<std::string>
553 serialize_checked(const Manifest &manifest, const ManifestLimits &limits = ManifestLimits::conservative());
554
555 /**
556 * @brief Reads and parses a manifest file.
557 * @param path Source file path.
558 * @param limits The resource caps to enforce; the default is @ref ManifestLimits::conservative().
559 * @return The parsed @ref Manifest, or FileOpenFailed (missing, locked, denied, or not a regular disk file), a
560 * parse error (MissingHeader / MalformedLine / ManifestIdentityCollision / ManifestFramingUnsafe) when
561 * the file is present but its contents are corrupt, SizeTooLarge (the file exceeds
562 * @ref ManifestLimits::max_file_bytes at the size query, or the bytes already read overrun the cap), or
563 * OutOfMemory. Any other length change detected after the size query fails as FileOpenFailed, including
564 * growth whose cap overrun would only land in a later read chunk.
565 * @details The read is materialized whole into a bounded buffer or not at all: a non-disk special file, an
566 * oversize file, a file a writer extends after the size query, and an allocation failure each return a
567 * typed error and touch no previously loaded manifest, so the caller's trusted generation survives a
568 * failed reload and the same input is retryable.
569 * @note A missing file is a distinct, recoverable FileOpenFailed, so an overlay can treat "no file" as "no
570 * overrides" (the defaults pass through) rather than a hard failure.
571 * @note Setup/control-plane only: performs bounded file I/O and parsing.
572 */
573 [[nodiscard]] Result<Manifest>
574 load(const std::filesystem::path &path, const ManifestLimits &limits = ManifestLimits::conservative());
575
576 /**
577 * @brief Writes a manifest to a file via @ref serialize_checked.
578 * @param path Destination file path.
579 * @param manifest The manifest to serialize.
580 * @param limits The resource caps to enforce; the default is @ref ManifestLimits::conservative().
581 * @return Empty on success, or an Error: any @ref serialize_checked rejection (the manifest could not be
582 * encoded to a round-trippable form), SizeTooLarge when the encoded text exceeds the platform's
583 * single-write bound, FileOpenFailed when the file could not be opened for writing, FileWriteFailed
584 * when the stream failed during the write or flush, or OutOfMemory when the write phase itself fails
585 * to allocate.
586 * @details The encode is validated before the file is opened, so a manifest that cannot round-trip never
587 * reaches disk. The write truncates @p path in place and is not atomic across a crash. A tear
588 * inside a line or heredoc fails the next @ref load closed, so the in-code defaults stay in effect.
589 * A tear at a record boundary parses as a valid shorter manifest. For a crash-durable replacement,
590 * stage @ref serialize_checked output through a temporary file, flush it to disk, and replace the
591 * target with the platform's atomic replace.
592 * @note Setup/control-plane only: performs bounded serialization and file I/O.
593 */
594 [[nodiscard]] Result<void> save(
595 const std::filesystem::path &path,
596 const Manifest &manifest,
597 const ManifestLimits &limits = ManifestLimits::conservative()
598 );
599
600 /**
601 * @brief Merges in-code anchor defaults with optional file overrides.
602 * @param defaults The in-code anchors.
603 * The function copies each borrowed view.
604 * @param overrides The file records from @ref load.
605 * An empty span passes the defaults through untouched.
606 * @return The merged, compiled signatures in @p defaults order. A per-signature problem never fails the whole
607 * overlay (fail-soft); the Result carries a failure only if a future merge-wide error mode is added.
608 * @note Setup/control-plane only, and not noexcept: like the resolvers it drives, its sole throwing path is
609 * allocation failure. A bad file entry does not throw or fail; it falls back to the in-code default.
610 * @details The adoption model in one call, fail-soft like @ref config::bind.
611 * - A default with no same-label override is adopted as-is (@ref Signature::adopt).
612 * - A default with a same-label override is replaced by the file (@ref Signature::compile), so a
613 * game update that broke two of twenty signatures needs only those two file entries.
614 * - A malformed override falls back to the in-code default, so an override never makes the result
615 * worse than a missing file.
616 * - An override whose label matches no default is inert and is not included.
617 * An accepted override supplies the complete serializable record.
618 * The effective override inherits these code-owned fields:
619 * - @ref SignatureRecord::validator
620 * - @ref SignatureRecord::validator_context
621 * - @ref SignatureRecord::validate_manual
622 * - @ref SignatureRecord::require_validator
623 * These contract changes fall back to the default:
624 * - An override that changes the default's declared @ref anchor::ResultDomain falls back to the
625 * default.
626 * - An override that crosses between Manual and a backend kind falls back to the default.
627 * An override for a non-serializable default is ignored.
628 * A flat file rung cannot preserve a quorum's corroboration.
629 * A default with a non-serializable kind or empty required evidence cannot be adopted.
630 * Callers use @ref anchor::evaluate_gate for non-serializable anchors.
631 */
632 [[nodiscard]] Result<std::vector<Signature>>
633 overlay(std::span<const anchor::Anchor> defaults, std::span<const SignatureRecord> overrides);
634
635 /**
636 * @struct GatePolicy
637 * @brief The trust thresholds @ref resolve_and_gate applies. Defaults reject drift but tolerate an unset
638 * baseline.
639 */
640 struct GatePolicy
641 {
642 /**
643 * @brief When true (the default), a signature whose fingerprint no longer matches its captured baseline
644 * (see @ref SignatureRecord::expected_fingerprint) is safe-disabled.
645 */
646 bool reject_on_fingerprint_drift = true;
647 /**
648 * @brief When true, a signature with no captured baseline (@ref FingerprintState::Unset) is also
649 * safe-disabled. The default false treats "unknown" as trusted, so an author who has not captured
650 * fingerprints yet is not blocked.
651 */
652 bool reject_unset_fingerprint = false;
653 /**
654 * @brief Optional whole-manifest health floor in [0, 1]: if the fraction of trusted signatures falls below
655 * it, every signature is rejected. The default 0 imposes no floor (each signature stands alone).
656 */
657 double min_resolved_fraction = 0.0;
658 /**
659 * @brief When true, a resolved signature is trusted to AUTHORIZE A WRITE only when its binding can safely
660 * mutate the resolved typed domain: a Manual pin (no live evidence, cannot self-heal) authorizes no
661 * mutation, and the binding kind must match the resolved domain - a MidHook needs a code site, a
662 * VmtMethod a vtable, and an Address / pointer chain a CodeSite or DataAddress, never a vtable or
663 * Scalar. The default false leaves a read-only manifest free to carry a Manual or value-only
664 * binding.
665 */
666 bool require_mutation_safe_binding = false;
667 /**
668 * @brief When true, a captured image baseline must match the live image for a mutation-capable entry.
669 * @details An absent baseline leaves the entry image-agnostic. Manual values are unaffected. Pair with
670 * @ref require_captured_image_identity to make the baseline mandatory rather than optional.
671 */
672 bool require_live_image_identity = false;
673 /**
674 * @brief When true, a mutation-capable entry with no captured image baseline is safe-disabled.
675 * @details Closes the read-only default's tolerance of an absent baseline.
676 */
677 bool require_captured_image_identity = false;
678 /**
679 * @brief When true, a mutation-capable entry must carry a winning-span content baseline that still matches.
680 * @details This gate compares target content. It first compares the baseline with the scan witness.
681 * Directly before trust publication, it reads the selected match span through the guarded memory
682 * primitive. It compares every byte again. An absent baseline, an absent span, an over-long span,
683 * a read fault, or any byte difference rejects the entry. This check catches an equal-layout
684 * in-place code patch, which @ref require_live_image_identity cannot see. It checks freshness
685 * directly before gate publication, not at a later consumer write. A consumer that requires
686 * write-time certainty must use a checked mutation or install operation.
687 */
688 bool require_winning_evidence_baseline = false;
689 /**
690 * @brief When true, a mutation-capable entry is safe-disabled unless a contract revision was actually
691 * checked.
692 * @details The plain @ref resolve_and_gate overload runs no revision check at all, and the header-threaded
693 * overload skips it when @c build_revision is 0. Either path would otherwise authorize a write
694 * against a manifest whose author contract was never compared.
695 */
696 bool require_contract_revision = false;
697
698 /**
699 * @brief The strictest gate. Reject drift and an unset baseline, and require every signature to resolve.
700 * @details Inverts the lenient default: an unset baseline is treated as untrusted. The manifest passes
701 * only when the ENTIRE set is trusted (min_resolved_fraction 1.0). A single drifted or unresolved
702 * feature therefore safe-disables the whole manifest.
703 * @return A GatePolicy with reject_on_fingerprint_drift and reject_unset_fingerprint both true and
704 * min_resolved_fraction 1.0.
705 */
706 9 [[nodiscard]] static constexpr GatePolicy strict() noexcept
707 {
708 return GatePolicy{
709 .reject_on_fingerprint_drift = true,
710 .reject_unset_fingerprint = true,
711 .min_resolved_fraction = 1.0,
712 9 };
713 }
714
715 /**
716 * @brief The strict gate PLUS every mutation-authorization requirement, for a manifest that drives a patch.
717 * @details A mutation-capable entry needs a captured fingerprint and a captured live image identity that
718 * matches. It also needs resolve evidence that matches its baseline and a fresh guarded read. The
719 * entry needs a mutation-safe typed binding that is not a Manual and a checked contract revision.
720 * The revision check requires the @ref ManifestHeader overload with a nonzero build revision.
721 * Read-only lookup is unaffected. The plain overload, a zero build revision, and an uncaptured
722 * baseline all remain usable for resolution. They cannot authorize a write.
723 * @return A strict policy with every mutation requirement armed.
724 */
725 9 [[nodiscard]] static constexpr GatePolicy mutation_strict() noexcept
726 {
727 9 GatePolicy policy = strict();
728 9 policy.require_mutation_safe_binding = true;
729 9 policy.require_live_image_identity = true;
730 9 policy.require_captured_image_identity = true;
731 9 policy.require_winning_evidence_baseline = true;
732 9 policy.require_contract_revision = true;
733 9 return policy;
734 }
735 };
736
737 /**
738 * @struct GatedSignature
739 * @brief One trusted signature: its resolved address paired with the binding that says how to read it.
740 * @details @ref label and @ref binding are views into the source @ref Signature, so a GateResult is valid only
741 * while the signatures it gated stay alive.
742 */
743 struct GatedSignature
744 {
745 /// The signature's key (a view into the source Signature).
746 std::string_view label;
747 /// Which anchor backend resolved it.
748 anchor::AnchorKind kind = anchor::AnchorKind::Manual;
749 /// The resolved value as an address; interpret it per @ref binding.
750 Address address;
751 /// The consumer-facing binding (a pointer into the source Signature).
752 const Binding *binding = nullptr;
753 };
754
755 /**
756 * @enum GateReason
757 * @brief Which gate safe-disabled a signature, so a log can tell a locate failure from a refused write
758 * authorization.
759 */
760 enum class GateReason : std::uint8_t
761 {
762 /// Not rejected.
763 None,
764 /// @ref Signature::resolve did not return a unique @ref anchor::AnchorStatus::Resolved.
765 Unresolved,
766 /// The declared definition was edited without re-capturing its baseline.
767 FingerprintDrifted,
768 /// No fingerprint baseline was captured and the policy requires one.
769 FingerprintUnset,
770 /// The binding cannot safely mutate the resolved typed domain, or it is a Manual pin.
771 BindingCannotMutate,
772 /// No contract revision was checked, or the checked revision is incompatible.
773 ContractRevision,
774 /// The captured image baseline is absent, or it no longer matches the live image.
775 ImageIdentity,
776 /// The winning-span content baseline is absent, unwitnessed, over-long, or no longer matches.
777 WinningEvidence,
778 /// The whole-manifest trusted fraction fell below @ref GatePolicy::min_resolved_fraction.
779 HealthFloor,
780 };
781
782 /**
783 * @struct RejectedSignature
784 * @brief One safe-disabled signature and why it was not trusted.
785 */
786 struct RejectedSignature
787 {
788 /// The signature's key (a view into the source Signature).
789 std::string_view label;
790 /// The resolve outcome; a non-Resolved status is why locate failed, if it did.
791 anchor::AnchorStatus status = anchor::AnchorStatus::Unresolved;
792 /// The drift verdict (see @ref FingerprintState).
793 FingerprintState fingerprint = FingerprintState::Unset;
794 /// The specific gate that rejected this entry.
795 GateReason reason = GateReason::None;
796 };
797
798 /**
799 * @struct GateResult
800 * @brief The partition of a gated manifest into trusted and safe-disabled, plus the health summary.
801 */
802 struct GateResult
803 {
804 /// Signatures healthy enough to act on.
805 std::vector<GatedSignature> trusted;
806 /// Signatures safe-disabled because they failed to resolve, drifted, or fell under the health floor.
807 std::vector<RejectedSignature> rejected;
808 /// The robustness summary of the whole manifest, from @ref anchor::assess_quality.
809 anchor::AnchorQuality quality;
810
811 /**
812 * @brief Looks up a trusted signature by label.
813 * @param label The signature key.
814 * @return The trusted entry, or nullptr when no trusted signature carries that label (it was rejected or
815 * never present). A consumer that safe-disables a feature then finds nothing and does not act.
816 */
817 [[nodiscard]] const GatedSignature *find(std::string_view label) const noexcept;
818 };
819
820 /**
821 * @brief Resolves a manifest and partitions it into trusted vs safe-disabled signatures.
822 * @param signatures The compiled signatures (from @ref overlay or @ref Signature::compile). Kept alive by the
823 * caller; the result borrows their labels and bindings.
824 * @param policy The trust thresholds.
825 * @param scope The default module image for signatures that name no module; defaults to the host executable.
826 * @return The partition plus the manifest health summary.
827 * @details A signature is rejected when its @ref Signature::resolve does not return a unique
828 * @ref anchor::AnchorStatus::Resolved, when its fingerprint drifted under
829 * @ref GatePolicy::reject_on_fingerprint_drift or is unset under
830 * @ref GatePolicy::reject_unset_fingerprint, when a configured mutation-authorization gate under
831 * @ref GatePolicy rejects its cleanly resolved entry, or when the whole-manifest trusted fraction
832 * falls below @ref GatePolicy::min_resolved_fraction. The entry's @ref GateReason names the gate
833 * that rejected it. A rejected feature installs no hook and reads no pointer. It stays off.
834 * @note Setup/control-plane only: resolving a manifest walks each signature's scope.
835 */
836 [[nodiscard]] GateResult resolve_and_gate(
837 std::span<const Signature> signatures,
838 const GatePolicy &policy = {},
839 Region scope = Region::host()
840 );
841
842 /**
843 * @brief Resolves and gates a manifest under a mandatory build-revision check for mutation-capable entries.
844 * @param signatures The compiled signatures (from @ref overlay or @ref Signature::compile), kept alive by the
845 * caller.
846 * @param header The parsed @ref ManifestHeader carrying the file's author-contract
847 * @ref ManifestHeader::revision.
848 * @param build_revision The revision this build authored its in-code signatures against; 0 opts out of
849 * @ref revision_compatible, leaving this overload equivalent to the plain
850 * @ref resolve_and_gate.
851 * @param policy The trust thresholds; compose @ref GatePolicy::mutation_strict for a manifest that drives a
852 * write.
853 * @param scope The default module image for signatures that name no module.
854 * @return The partition plus the manifest health summary.
855 * @details A non-zero incompatible revision rejects mutation-capable entries even when they resolve. Manual
856 * values remain available.
857 * @note Setup/control-plane only: resolving a manifest walks each signature's scope.
858 */
859 [[nodiscard]] GateResult resolve_and_gate(
860 std::span<const Signature> signatures,
861 const ManifestHeader &header,
862 std::uint32_t build_revision,
863 const GatePolicy &policy = {},
864 Region scope = Region::host()
865 );
866
867 /**
868 * @brief Maps a @ref BindingKind to a short human-readable label (its file token).
869 * @param kind The binding kind.
870 * @return A static string view naming the kind.
871 */
872 [[nodiscard]] std::string_view binding_kind_to_string(BindingKind kind) noexcept;
873
874 /**
875 * @brief Maps a @ref FingerprintState to a short human-readable label.
876 * @param state The fingerprint state.
877 * @return A static string view naming the state.
878 */
879 [[nodiscard]] std::string_view fingerprint_state_to_string(FingerprintState state) noexcept;
880
881 /**
882 * @brief Maps a @ref GateReason to a short human-readable label.
883 * @param reason The rejection reason.
884 * @return A static string view naming the reason.
885 */
886 [[nodiscard]] std::string_view gate_reason_to_string(GateReason reason) noexcept;
887 } // namespace manifest
888 } // namespace DetourModKit
889
890 #endif // DETOURMODKIT_MANIFEST_HPP
891