./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
11 / 11
100.0%
Branches:
-
-%
Functions:
3 / 3
100.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: patch-fragile resolve contracts as editable data in its own INI, not the settings INI.
7 * @warning `[B-100]` Never parse, compile, resolve, gate, or derive a scope under the loader lock. Pure value accessors
8 * on a compiled @ref Signature do not allocate or query the loader.
9 */
10
11 #include "DetourModKit/anchor.hpp"
12 #include "DetourModKit/error.hpp"
13 #include "DetourModKit/region.hpp"
14 #include "DetourModKit/scan.hpp"
15
16 #include <cstddef>
17 #include <cstdint>
18 #include <filesystem>
19 #include <limits>
20 #include <span>
21 #include <string>
22 #include <string_view>
23 #include <vector>
24
25 namespace DetourModKit
26 {
27 // Binding::read_register needs only this declaration. Include hook.hpp to name a hook::Gpr value.
28 namespace hook
29 {
30 enum class Gpr : std::uint8_t;
31 }
32
33 namespace manifest
34 {
35 namespace detail
36 {
37 class GateAccess;
38 } // namespace detail
39
40 /** @brief How a consumer interprets a resolved value. The consumer, not this module, acts on it. */
41 enum class BindingKind : std::uint8_t
42 {
43 /// The resolved value is the target address, for example an inline-hook target or a global.
44 Address,
45 /// The resolved value is a chain base that the consumer walks through @ref Binding::offsets.
46 PointerChain,
47 /// The resolved value is a mid-hook site, where @ref hook::gpr reads @ref Binding::read_register.
48 MidHookRegister,
49 /** @brief The resolved value is a vtable base. Hook the slot at @ref Binding::vmt_index. */
50 VmtMethod
51 };
52
53 /// The @ref Binding::xmm_index sentinel: the site reads a GPR, not an XMM lane.
54 inline constexpr std::uint8_t XMM_INDEX_UNUSED = 0xFF;
55
56 /** @brief How to read a resolved value. A field that @ref kind does not read is inert and must stay default. */
57 struct Binding
58 {
59 /// How to interpret the resolved value.
60 BindingKind kind = BindingKind::Address;
61 /// PointerChain: one or more byte offsets walked from the resolved base with @ref memory::walk semantics.
62 std::vector<std::ptrdiff_t> offsets;
63 /// PointerChain: byte width of the final read (1, 2, 4, or 8), for example 4 for a float.
64 std::uint8_t value_width = 8;
65 /// MidHookRegister: the register that the mid-hook callback reads.
66 hook::Gpr read_register{};
67 /// MidHookRegister: XMM lane 0 through 15 for a float site, or @ref XMM_INDEX_UNUSED for a GPR value.
68 std::uint8_t xmm_index = XMM_INDEX_UNUSED;
69 /// VmtMethod: the zero-based vtable slot to hook, from 0 through 4095.
70 std::size_t vmt_index = 0;
71 };
72
73 /** @brief One editable ladder rung for a @ref scan::Candidate. Resolution ignores the fields of other modes. */
74 struct CandidateSpec
75 {
76 /// Human-readable rung name, carried into the winning @ref scan::Hit for diagnostics.
77 std::string name;
78 /// Which resolution strategy this rung uses.
79 scan::Mode mode = scan::Mode::Direct;
80 /// Direct / RipRelative: the AOB DSL string, for example "48 8B 05 ?? ?? ?? ??".
81 std::string pattern;
82 /// Direct: signed byte delta added to the match, where a negative value walks backward.
83 std::ptrdiff_t walk_back = 0;
84 /// RipRelative: byte offset from the match to the signed 4-byte displacement field.
85 std::ptrdiff_t displacement_at = 0;
86 /// RipRelative: total length of the referencing instruction (the next-IP base for the displacement).
87 std::size_t instruction_length = 0;
88 /// RttiVtable: the MSVC mangled type name, for example ".?AVCameraManager@@".
89 std::string mangled;
90 /// StringXref: same as @ref SignatureRecord::xref_text.
91 std::string string_text;
92 /// StringXref: same as @ref SignatureRecord::xref_encoding.
93 scan::StringEncoding string_encoding = scan::StringEncoding::Utf8;
94 /// StringXref: same as @ref SignatureRecord::xref_return.
95 scan::XrefReturn string_return = scan::XrefReturn::ReferencingInstruction;
96 /// StringXref: same as @ref SignatureRecord::xref_require_terminator.
97 bool string_require_terminator = true;
98 /// StringXref: same as @ref SignatureRecord::xref_broad_match.
99 bool string_broad_match = false;
100 };
101
102 /**
103 * @brief An owning, serializable @ref anchor::Anchor plus @ref Binding. Resolution ignores the fields of other
104 * kinds.
105 */
106 struct SignatureRecord
107 {
108 /// Stable merge and lookup key, for example "player.health", echoed into the drift report and gate result.
109 std::string label;
110 /// Which anchor backend resolves this signature (one of the six serializable kinds).
111 anchor::AnchorKind kind = anchor::AnchorKind::RipGlobal;
112 /** @brief The module basename for @ref Region::module_named. Empty resolves within the fallback scope. */
113 std::string module;
114
115 /// RipGlobal / CodeOperand: the candidate ladder that resolves to the address or the instruction site.
116 std::vector<CandidateSpec> ladder;
117
118 /// VtableIdentity: the MSVC mangled type name to resolve through the reverse-RTTI walk.
119 std::string mangled;
120
121 /// CodeOperand: whether to read an immediate or a memory-operand displacement.
122 scan::OperandKind operand_kind = scan::OperandKind::Immediate;
123 /// CodeOperand: index into the instruction's visible operands.
124 std::uint8_t operand_index = 0;
125 /// CodeOperand: 0 keeps the decoded value, and 1 through 8 narrows non-RIP low bytes and sign-extends.
126 std::uint8_t byte_width = 0;
127
128 /// StringXref: the exact literal content to anchor on (no quotes).
129 std::string xref_text;
130 /**
131 * @brief StringXref: the literal's byte encoding. Utf16le, for a wchar_t literal, needs well-formed UTF-8
132 * that resolution converts to UTF-16LE. @ref parse, @ref Signature::compile, @ref Signature::adopt,
133 * and @ref serialize_checked reject malformed text. Utf8 evidence stays byte-transparent.
134 */
135 scan::StringEncoding xref_encoding = scan::StringEncoding::Utf8;
136 /// StringXref: whether to return the referencing instruction, its enclosing function, or the pointer slot.
137 scan::XrefReturn xref_return = scan::XrefReturn::ReferencingInstruction;
138 /// StringXref: match a trailing NUL so a prefix of a longer literal is not matched.
139 bool xref_require_terminator = true;
140 /// StringXref: keep the lea/mov shape scan and add the broad Zydis sweep for rarer reference shapes.
141 bool xref_broad_match = false;
142
143 /// Manual: the pinned literal value, taken as-is.
144 std::int64_t manual_value = 0;
145
146 /** @brief In-memory only, never in a file: a post-resolve validator, as @ref anchor::Anchor::validator. */
147 anchor::AnchorValidator validator = nullptr;
148 /** @brief In-memory only: the @ref validator context. The borrowed pointee must outlive each resolve. */
149 const void *validator_context = nullptr;
150 /** @brief In-memory only: run @ref validator on a Manual anchor too, so the pinned literal is checked. */
151 bool validate_manual = false;
152 /// In-memory only: fail closed when a backend-resolvable anchor carries no @ref validator.
153 bool require_validator = false;
154
155 /// How the consumer interprets the resolved value.
156 Binding binding{};
157
158 /** @brief The @ref Signature::current_fingerprint baseline captured when authored, or 0 if none exists. */
159 std::uint64_t expected_fingerprint = 0;
160
161 /** @brief RipGlobal: page class of byte-tier rungs. If all rungs anchor on instructions, use Executable. */
162 scan::Pages pages = scan::Pages::Readable;
163
164 /** @brief ExportName: the exact, case-sensitive, undecorated export in @ref module, such as "Sleep". */
165 std::string export_name;
166
167 /** @brief The optional live-image baseline (file key `image_identity`) that the identity gates compare. */
168 scan::ImageIdentity expected_image_identity{};
169
170 /** @brief The optional winning-span content baseline, file key `winning_bytes` in lowercase hex. */
171 scan::WinningEvidence expected_winning_bytes{};
172 };
173
174 /** @brief The drift verdict of one signature. */
175 enum class FingerprintState : std::uint8_t
176 {
177 /// No baseline exists (@ref SignatureRecord::expected_fingerprint is 0), so drift is unknown.
178 Unset,
179 /// The live fingerprint equals the baseline.
180 Match,
181 /// The live fingerprint differs from the baseline.
182 Drifted
183 };
184
185 /** @brief A compiled signature that owns its evidence. Build one with @ref compile or @ref adopt. */
186 class Signature
187 {
188 public:
189 /**
190 * @brief Compiles a file record into a resolvable signature.
191 * @return The Signature, or one of these errors:
192 * - BadPattern: a rung AOB that does not compile.
193 * - EmptyCandidates: a RipGlobal or CodeOperand record with no ladder.
194 * - InvalidArg: a Quorum, CallArgHome, or Unset kind, or empty required evidence of the record or
195 * of an RttiVtable or StringXref rung. It also means a ladder on a kind other than RipGlobal or
196 * CodeOperand, or an out-of-range persisted policy field such as CodeOperand byte_width. It also
197 * means a label or string field that cannot round-trip, or a nonzero image baseline with a zero
198 * size_of_image. It also means a truncated or over-long content baseline, Utf16le evidence that
199 * breaks the @ref SignatureRecord::xref_encoding rule, or a @ref Binding that breaks a field
200 * rule. It also means a RipRelative rung that @ref scan::Candidate::rip_relative rejects.
201 * @details A record that meets several conditions returns the code of the first failed check.
202 * @note Setup/control-plane only.
203 */
204 [[nodiscard]] static Result<Signature> compile(SignatureRecord record);
205
206 /**
207 * @brief Adopts an in-code @ref anchor::Anchor and copies its borrowed views, so it can outlive @p source.
208 * @return The Signature, or InvalidArg for a @ref compile InvalidArg condition outside the ladder, or a
209 * RipGlobal or CodeOperand anchor without candidates. It checks each candidate only against the
210 * @ref SignatureRecord::xref_encoding rule.
211 * @details For a RipGlobal or CodeOperand kind, the record holds each candidate as a rung. A byte rung
212 * holds its pattern in canonical AOB text, and a text rung holds its mangled name or string. A
213 * @ref compile of that record rebuilds the same candidates and fingerprint, which
214 * `ManifestAdoptTest.RenderedLadderCompilesToTheSameCandidates` proves. A candidate that
215 * @ref compile rejects makes @ref serialize_checked reject the record.
216 * @note Setup/control-plane only.
217 */
218 [[nodiscard]] static Result<Signature> adopt(const anchor::Anchor &source);
219
220 /**
221 * @brief Resolves this signature through its anchor backend, fail-closed. See @ref SignatureRecord::module.
222 * @note Setup/control-plane only (see @ref anchor::resolve).
223 */
224 [[nodiscard]] anchor::ResolvedAnchor resolve(Region fallback_scope = Region::host()) const;
225
226 /**
227 * @brief The scope: @ref Region::module_named for the record's module, or @ref Region::host for none.
228 * @note Setup/control-plane only: queries the loader.
229 */
230 [[nodiscard]] Region scope() const noexcept;
231
232 /**
233 * @brief The live fingerprint: a hash of @ref anchor::anchor_fingerprint, @ref Binding, label, and module.
234 * It reads no game memory, so it is stable across runs and rebuilds on one platform.
235 */
236 [[nodiscard]] std::uint64_t current_fingerprint() const noexcept;
237
238 /** @brief Compares @ref current_fingerprint to the record baseline. See @ref FingerprintState. */
239 [[nodiscard]] FingerprintState fingerprint_state() const noexcept;
240
241 /**
242 * @brief Makes the live fingerprint the baseline after a verified repair. Persist @ref record to keep it.
243 * @note Setup/control-plane only.
244 */
245 void recapture_fingerprint() noexcept;
246
247 /**
248 * @brief Re-resolves and recaptures the fingerprint, image identity, and winning-span content baselines.
249 * @param fallback_scope The scope for a record that names no module. It must match the later gate scope.
250 * @return Success, NoMatch if the signature does not resolve, or UnexpectedShape if the resolved rung
251 * witnesses no image or content span. Only a RipGlobal hit from a Direct or RipRelative rung can
252 * witness both, with at most @ref scan::MAX_MUTATION_WITNESS_BYTES of content.
253 * @details Only this call captures live image and content baselines for @ref GatePolicy::mutation_strict. A
254 * failure keeps all three previous baselines. Persist @ref record to keep the new ones.
255 * @note Setup/control-plane only.
256 */
257 [[nodiscard]] Result<void> recapture(Region fallback_scope = Region::host());
258
259 /// The signature's stable key.
260 [[nodiscard]] std::string_view label() const noexcept;
261 /// Which anchor backend resolves this signature.
262 [[nodiscard]] anchor::AnchorKind kind() const noexcept;
263 /// The consumer-facing binding (register / offsets / vtable slot).
264 [[nodiscard]] const Binding &binding() const noexcept;
265 /// The owning record, for @ref serialize_checked after a recapture.
266 [[nodiscard]] const SignatureRecord &record() const noexcept;
267
268 private:
269 friend class detail::GateAccess;
270
271 // Only compile() and adopt() construct.
272 Signature(SignatureRecord record, std::vector<scan::Candidate> ladder) noexcept;
273
274 // A borrowed view of this object's storage. It is rebuilt per call and valid only for that call.
275 [[nodiscard]] anchor::Anchor make_anchor() const noexcept;
276
277 // Also returns the winning match span for the mutation gate.
278 [[nodiscard]] anchor::ResolvedAnchor resolve_for_gate(Region fallback_scope, Region &winning_span) const;
279
280 SignatureRecord m_record;
281 std::vector<scan::Candidate> m_ladder;
282 };
283
284 /// The INI format version that this build reads and writes, bumped only for an incompatible format change.
285 inline constexpr std::uint32_t SCHEMA_VERSION = 1;
286
287 /** @brief The `[manifest]` metadata. Only an in-code change that breaks older manifests bumps @ref revision. */
288 struct ManifestHeader
289 {
290 /// The format version that the file declares.
291 std::uint32_t schema = SCHEMA_VERSION;
292 /// The author's signature-contract epoch, or 0 if unversioned, compared only by @ref revision_compatible.
293 std::uint32_t revision = 0;
294 };
295
296 /** @brief A parsed manifest: its @ref ManifestHeader plus the signature records in file order. */
297 struct Manifest
298 {
299 /// The `[manifest]` metadata (schema and contract revision).
300 ManifestHeader header{};
301 /// The signatures, one per `[sig.<label>]` section, in file order.
302 std::vector<SignatureRecord> records{};
303 };
304
305 /** @brief Parse and persistence caps. A violation returns SizeTooLarge and publishes no partial result. */
306 struct ManifestLimits
307 {
308 /// Largest accepted encoded text size in bytes.
309 std::size_t max_file_bytes{1u << 20};
310 /// Largest accepted INI section count, by default sized to the default record and rung caps: 1 + 512 * 33.
311 std::size_t max_sections{16897};
312 /// Largest accepted number of keys within any one section.
313 std::size_t max_keys_per_section{64};
314 /// Largest accepted number of `[sig.<label>]` records.
315 std::size_t max_records{512};
316 /// Largest accepted number of candidate-ladder rungs on any one record.
317 std::size_t max_rungs_per_record{32};
318 /// Largest accepted size in bytes of any single string field or heredoc value.
319 std::size_t max_field_bytes{64u << 10};
320 /// Largest accepted sum of all decoded value bytes across the manifest.
321 std::size_t max_total_decoded_bytes{4u << 20};
322
323 /// Returns limits equal to a default-constructed @ref ManifestLimits.
324 512 [[nodiscard]] static constexpr ManifestLimits conservative() noexcept { return ManifestLimits{}; }
325
326 /** @brief Uncapped limits for a trusted authoring tool only. Grammar and semantic checks stay on. */
327 [[nodiscard]] static constexpr ManifestLimits advanced() noexcept
328 {
329 // Parenthesized because public headers must compile with <windows.h>'s function-like max macro active.
330 constexpr std::size_t MAX_VALUE = (std::numeric_limits<std::size_t>::max)();
331 return ManifestLimits{
332 .max_file_bytes = MAX_VALUE,
333 .max_sections = MAX_VALUE,
334 .max_keys_per_section = MAX_VALUE,
335 .max_records = MAX_VALUE,
336 .max_rungs_per_record = MAX_VALUE,
337 .max_field_bytes = MAX_VALUE,
338 .max_total_decoded_bytes = MAX_VALUE,
339 };
340 }
341 };
342
343 /**
344 * @brief Returns true if @p build_revision is 0, which opts out, or equals @ref ManifestHeader::revision. If it
345 * returns false, pass no file records to @ref overlay.
346 */
347 [[nodiscard]] bool revision_compatible(const ManifestHeader &header, std::uint32_t build_revision) noexcept;
348
349 /**
350 * @brief Parses manifest INI text. A failure rejects the whole manifest.
351 * @return The @ref Manifest, OutOfMemory, or one of these errors:
352 * - MissingHeader: no `[manifest]` section, no `schema` key, or an unsupported schema.
353 * - MalformedLine: an unparsable line, field, or enum token, a noncomment key line without `=`, or an
354 * empty key. It also covers a non-canonical section or key spelling, a section that is neither
355 * `[manifest]` nor `sig.`-prefixed, and a key line before the first header. It also covers a key that
356 * its record's kind, binding kind, or rung mode does not read. It also covers a rung under a kind
357 * other than RipGlobal or CodeOperand, and Utf16le evidence that breaks the
358 * @ref SignatureRecord::xref_encoding rule.
359 * - ManifestIdentityCollision: a case-, whitespace-, or exactly-duplicated section, or a
360 * whitespace-variant or exactly-duplicated key. A miscased key returns MalformedLine instead.
361 * - ManifestFramingUnsafe: an unterminated `<<<` heredoc value, an opener with an empty tag, or a
362 * heredoc whose first body line is its terminator.
363 * - SizeTooLarge: encoded text, a section, key, field, record, rung, or aggregate over @p limits.
364 * @details A missing optional key takes its default, so an absent `revision` is 0. A present key must parse, so
365 * a blank enum, numeric, boolean, or baseline value is MalformedLine. A blank `schema` is
366 * MissingHeader, and a blank `offsets` is an empty list. A blank string value reads as empty.
367 * @note Setup/control-plane only.
368 */
369 [[nodiscard]] Result<Manifest>
370 parse(std::string_view text, const ManifestLimits &limits = ManifestLimits::conservative());
371
372 /**
373 * @brief Serializes a manifest to INI text that @ref parse reads back and @ref Signature::compile accepts.
374 * @return The text, OutOfMemory, or one of these errors:
375 * - InvalidArg, EmptyCandidates, or BadPattern: a record that @ref Signature::compile rejects, with the
376 * code that compile returns.
377 * - ManifestIdentityCollision: two record labels that fold to one section, or a label that folds into
378 * another record's rung section.
379 * - SizeTooLarge: encoded text, a record, rung, field, or aggregate that exceeds @p limits.
380 * @details A manifest that fails several checks returns the code of the first failed check. It writes
381 * `revision` only if non-zero, and `schema` is always @ref SCHEMA_VERSION.
382 * @note Setup/control-plane only.
383 */
384 [[nodiscard]] Result<std::string>
385 serialize_checked(const Manifest &manifest, const ManifestLimits &limits = ManifestLimits::conservative());
386
387 /**
388 * @brief Reads and parses a manifest file, whole or not at all.
389 * @return The @ref Manifest, a @ref parse error for corrupt contents, OutOfMemory, or one of these errors:
390 * - SizeTooLarge: the file exceeds @ref ManifestLimits::max_file_bytes at the size query, or the bytes
391 * already read overrun that cap.
392 * - FileOpenFailed: the file is missing, locked, denied, or not a regular disk file. Any other length
393 * change after the size query is also FileOpenFailed, even growth past the cap in a later read chunk.
394 * @details A failed load is retryable. A caller can treat a missing file as no overrides.
395 * @note Setup/control-plane only.
396 */
397 [[nodiscard]] Result<Manifest>
398 load(const std::filesystem::path &path, const ManifestLimits &limits = ManifestLimits::conservative());
399
400 /**
401 * @brief Writes @ref serialize_checked output to a file.
402 * @return Success, a @ref serialize_checked error, SizeTooLarge above the platform single-write bound,
403 * FileOpenFailed, FileWriteFailed if the write or flush fails, or OutOfMemory in the write phase.
404 * @details A @ref serialize_checked error or SizeTooLarge returns before the file opens and leaves it
405 * unchanged. The write then truncates @p path in place and is not atomic across a crash. A tear inside
406 * a heredoc, a section header, or a key line before its `=` fails the next @ref load closed. Any other
407 * tear can parse as a valid manifest with fewer keys, rungs, or records, or with a truncated value.
408 * For crash durability, atomically replace the target with a flushed temporary file.
409 * @note Setup/control-plane only.
410 */
411 [[nodiscard]] Result<void> save(
412 const std::filesystem::path &path,
413 const Manifest &manifest,
414 const ManifestLimits &limits = ManifestLimits::conservative()
415 );
416
417 /**
418 * @brief Merges in-code anchor defaults with optional file overrides by label. It copies each borrowed view.
419 * @return The merged signatures in @p defaults order. A per-signature problem never fails the call.
420 * @details The merge follows these rules:
421 * - Without a same-label override, @ref Signature::adopt adopts the default.
422 * - With one, @ref Signature::compile replaces it with the complete file record. The record inherits
423 * every in-memory-only field of the default.
424 * - An override that fails to compile, changes the declared @ref anchor::ResultDomain, or crosses
425 * between Manual and a backend kind falls back to the default.
426 * - An override whose label matches no default, or whose default is non-serializable, is ignored.
427 * - Without an accepted override, a default that @ref Signature::adopt rejects is left out. Gate a
428 * Quorum or CallArgHome default through @ref anchor::evaluate_gate.
429 * @note Setup/control-plane only. Only allocation failure throws.
430 */
431 [[nodiscard]] Result<std::vector<Signature>>
432 overlay(std::span<const anchor::Anchor> defaults, std::span<const SignatureRecord> overrides);
433
434 /** @brief Thresholds for @ref resolve_and_gate. */
435 struct GatePolicy
436 {
437 /// If true, safe-disable an entry whose fingerprint is @ref FingerprintState::Drifted.
438 bool reject_on_fingerprint_drift = true;
439 /// If true, also safe-disable an entry with no baseline (@ref FingerprintState::Unset).
440 bool reject_unset_fingerprint = false;
441 /** @brief Health floor in [0, 1]. If the trusted fraction falls below it, every entry is safe-disabled. */
442 double min_resolved_fraction = 0.0;
443 /**
444 * @brief If true, safe-disable an entry that is not mutation-capable: a Manual pin or a domain mismatch.
445 * @details MidHookRegister needs a code site, and VmtMethod needs a vtable. Address and PointerChain need a
446 * CodeSite or DataAddress, never a vtable or Scalar.
447 */
448 bool require_mutation_safe_binding = false;
449 /** @brief If true, a captured image baseline of a mutation-capable entry must match the live image. */
450 bool require_live_image_identity = false;
451 /** @brief If true, safe-disable a mutation-capable entry that has no captured image baseline. */
452 bool require_captured_image_identity = false;
453 /**
454 * @brief If true, a mutation-capable entry needs a winning-span content baseline that still matches.
455 * @details The baseline must equal the scan witness and a guarded reread of the match span directly before
456 * publication. Unlike the image identity gates, this check sees an in-place patch of the match
457 * span under equal PE headers. For write-time certainty, use a checked mutation or install.
458 */
459 bool require_winning_evidence_baseline = false;
460 /** @brief If true, safe-disable a mutation-capable entry unless the gate checked a contract revision. */
461 bool require_contract_revision = false;
462
463 /** @brief Rejects drift and an unset baseline. One untrusted entry safe-disables the whole manifest. */
464 10 [[nodiscard]] static constexpr GatePolicy strict() noexcept
465 {
466 return GatePolicy{
467 .reject_on_fingerprint_drift = true,
468 .reject_unset_fingerprint = true,
469 .min_resolved_fraction = 1.0,
470 10 };
471 }
472
473 /**
474 * @brief @ref strict plus every mutation requirement, for a manifest that authorizes a write. It passes no
475 * mutation-capable entry unless the @ref ManifestHeader overload gets a non-zero build revision.
476 */
477 10 [[nodiscard]] static constexpr GatePolicy mutation_strict() noexcept
478 {
479 10 GatePolicy policy = strict();
480 10 policy.require_mutation_safe_binding = true;
481 10 policy.require_live_image_identity = true;
482 10 policy.require_captured_image_identity = true;
483 10 policy.require_winning_evidence_baseline = true;
484 10 policy.require_contract_revision = true;
485 10 return policy;
486 }
487 };
488
489 /** @brief A trusted signature. Its label and binding borrow from a @ref Signature that must outlive it. */
490 struct GatedSignature
491 {
492 /// The signature's key (a view into the source Signature).
493 std::string_view label;
494 /// Which anchor backend resolved it.
495 anchor::AnchorKind kind = anchor::AnchorKind::Manual;
496 /** @brief The resolved value as an address. Interpret it through @ref binding. */
497 Address address;
498 /// The consumer-facing binding (a pointer into the source Signature).
499 const Binding *binding = nullptr;
500 };
501
502 /** @brief The gate that safe-disabled a signature, so a log can tell a locate failure from a refused write. */
503 enum class GateReason : std::uint8_t
504 {
505 /// Not rejected.
506 None,
507 /// @ref Signature::resolve did not return a unique @ref anchor::AnchorStatus::Resolved.
508 Unresolved,
509 /// The declared definition changed after its baseline capture.
510 FingerprintDrifted,
511 /// No fingerprint baseline was captured and the policy requires one.
512 FingerprintUnset,
513 /// The binding cannot safely mutate the resolved typed domain, or it is a Manual pin.
514 BindingCannotMutate,
515 /// No contract revision was checked, or the checked revision is incompatible.
516 ContractRevision,
517 /// The captured image baseline is absent, or it no longer matches the live image.
518 ImageIdentity,
519 /// The winning-span content baseline is absent, unwitnessed, over-long, unreadable, or no longer matches.
520 WinningEvidence,
521 /// The whole-manifest trusted fraction fell below @ref GatePolicy::min_resolved_fraction.
522 HealthFloor,
523 };
524
525 /** @brief One safe-disabled signature and why it was not trusted. */
526 struct RejectedSignature
527 {
528 /// The signature's key (a view into the source Signature).
529 std::string_view label;
530 /** @brief The resolve outcome. A status other than Resolved explains a locate failure. */
531 anchor::AnchorStatus status = anchor::AnchorStatus::Unresolved;
532 /// The drift verdict (see @ref FingerprintState).
533 FingerprintState fingerprint = FingerprintState::Unset;
534 /// The specific gate that rejected this entry.
535 GateReason reason = GateReason::None;
536 };
537
538 /** @brief A gated manifest: its trusted and safe-disabled signatures, plus the quality summary. */
539 struct GateResult
540 {
541 /// Signatures that no gate rejected.
542 std::vector<GatedSignature> trusted;
543 /// Safe-disabled signatures, each with the @ref GateReason that rejected it.
544 std::vector<RejectedSignature> rejected;
545 /// The quality summary of the whole manifest, from @ref anchor::assess_quality.
546 anchor::AnchorQuality quality;
547
548 /** @brief Returns the trusted entry for @p label, or nullptr if that label was rejected or is absent. */
549 [[nodiscard]] const GatedSignature *find(std::string_view label) const noexcept;
550 };
551
552 /**
553 * @brief Resolves each signature and partitions the set into trusted and safe-disabled entries.
554 * @param signatures The result borrows the labels and bindings of these signatures.
555 * @param scope The scope for a signature that names no module.
556 * @note Setup/control-plane only.
557 */
558 [[nodiscard]] GateResult resolve_and_gate(
559 std::span<const Signature> signatures,
560 const GatePolicy &policy = {},
561 Region scope = Region::host()
562 );
563
564 /**
565 * @brief Like @ref resolve_and_gate, and rejects a mutation-capable entry that fails @ref revision_compatible.
566 * @param build_revision The build's contract revision. 0 skips the check, as the plain overload does.
567 * @note Setup/control-plane only.
568 */
569 [[nodiscard]] GateResult resolve_and_gate(
570 std::span<const Signature> signatures,
571 const ManifestHeader &header,
572 std::uint32_t build_revision,
573 const GatePolicy &policy = {},
574 Region scope = Region::host()
575 );
576
577 /** @brief Returns the static file token that names @p kind. */
578 [[nodiscard]] std::string_view binding_kind_to_string(BindingKind kind) noexcept;
579
580 /** @brief Returns a static name for @p state. */
581 [[nodiscard]] std::string_view fingerprint_state_to_string(FingerprintState state) noexcept;
582
583 /** @brief Returns a static name for @p reason. */
584 [[nodiscard]] std::string_view gate_reason_to_string(GateReason reason) noexcept;
585 } // namespace manifest
586 } // namespace DetourModKit
587
588 #endif // DETOURMODKIT_MANIFEST_HPP
589