include/DetourModKit/drift_manifest.hpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #ifndef DETOURMODKIT_DRIFT_MANIFEST_HPP | ||
| 2 | #define DETOURMODKIT_DRIFT_MANIFEST_HPP | ||
| 3 | |||
| 4 | /** | ||
| 5 | * @file drift_manifest.hpp | ||
| 6 | * @brief Durable serialization of self-heal drift reports (@ref DetourModKit::Rtti::DriftEntry). | ||
| 7 | * @details Lets a consumer persist a @ref DetourModKit::Rtti::heal_report across game versions and diff the saved | ||
| 8 | * manifests to see which offsets moved between patches, instead of only logging the live telemetry once per | ||
| 9 | * run. | ||
| 10 | */ | ||
| 11 | |||
| 12 | #include "DetourModKit/rtti_dissect.hpp" | ||
| 13 | |||
| 14 | #include <cstddef> | ||
| 15 | #include <cstdint> | ||
| 16 | #include <expected> | ||
| 17 | #include <span> | ||
| 18 | #include <string> | ||
| 19 | #include <string_view> | ||
| 20 | #include <vector> | ||
| 21 | |||
| 22 | namespace DetourModKit | ||
| 23 | { | ||
| 24 | namespace Rtti | ||
| 25 | { | ||
| 26 | /** | ||
| 27 | * @struct DriftRecord | ||
| 28 | * @brief An owning, parsed drift entry read back from a manifest. | ||
| 29 | * @details The persisted counterpart to @ref DriftEntry. Unlike DriftEntry, whose @c name aliases caller | ||
| 30 | * storage, DriftRecord owns its name so it stays valid after the manifest text or file buffer is gone. | ||
| 31 | */ | ||
| 32 | struct DriftRecord | ||
| 33 | { | ||
| 34 | /// The landmark name (owned). | ||
| 35 | std::string name; | ||
| 36 | /// Last-known offset recorded at write time. | ||
| 37 | std::ptrdiff_t nominal_offset = 0; | ||
| 38 | /// Resolved offset (meaningful only when @ref ok). | ||
| 39 | std::ptrdiff_t healed_offset = 0; | ||
| 40 | /// healed_offset - nominal_offset (meaningful only when @ref ok). | ||
| 41 | std::ptrdiff_t delta = 0; | ||
| 42 | /// Whether the landmark healed. | ||
| 43 | bool ok = false; | ||
| 44 | /// Failure reason (meaningful only when @ref ok is false). | ||
| 45 | HealError error{}; | ||
| 46 | }; | ||
| 47 | |||
| 48 | /** | ||
| 49 | * @enum ManifestError | ||
| 50 | * @brief Why reading or parsing a drift manifest failed. Fails closed: no partial result. | ||
| 51 | */ | ||
| 52 | enum class ManifestError : std::uint8_t | ||
| 53 | { | ||
| 54 | /// The first non-blank line was not the manifest header (file present but corrupt). | ||
| 55 | MissingHeader, | ||
| 56 | /// A record line had the wrong field count or an unparseable field (file present but corrupt). | ||
| 57 | MalformedLine, | ||
| 58 | /// The file could not be opened (missing, locked, permission denied, or not a regular file). | ||
| 59 | FileOpenFailed | ||
| 60 | }; | ||
| 61 | |||
| 62 | /** | ||
| 63 | * @brief Human-readable mapping for @ref ManifestError. | ||
| 64 | * @param error The error code. | ||
| 65 | * @return A string view describing the error. | ||
| 66 | */ | ||
| 67 | 9 | [[nodiscard]] constexpr std::string_view manifest_error_to_string(ManifestError error) noexcept | |
| 68 | { | ||
| 69 |
3/4✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 4 taken 3 times.
✓ Branch 2 → 5 taken 3 times.
✗ Branch 2 → 6 not taken.
|
9 | switch (error) |
| 70 | { | ||
| 71 | 3 | case ManifestError::MissingHeader: | |
| 72 | 3 | return "Manifest header line is missing or wrong"; | |
| 73 | 3 | case ManifestError::MalformedLine: | |
| 74 | 3 | return "A manifest record line is malformed"; | |
| 75 | 3 | case ManifestError::FileOpenFailed: | |
| 76 | 3 | return "Manifest file could not be opened"; | |
| 77 | } | ||
| 78 | ✗ | return "Unknown manifest error"; | |
| 79 | } | ||
| 80 | |||
| 81 | /** | ||
| 82 | * @brief Serializes a drift report to a durable, line-oriented manifest. | ||
| 83 | * @details Emits a versioned header line followed by one tab-separated line per entry (name, nominal_offset, | ||
| 84 | * healed_offset, delta, ok, error). The error is written as a stable token, not the human-readable | ||
| 85 | * @ref heal_error_to_string text, so the manifest round-trips. Names are assumed free of tab and | ||
| 86 | * newline (MSVC mangled type names are). | ||
| 87 | * @param entries The drift entries to serialize (e.g. from @ref heal_report). | ||
| 88 | * @return The manifest text. | ||
| 89 | */ | ||
| 90 | [[nodiscard]] std::string serialize_drift_report(std::span<const DriftEntry> entries); | ||
| 91 | |||
| 92 | /** | ||
| 93 | * @brief Parses a drift manifest produced by @ref serialize_drift_report. | ||
| 94 | * @details Tolerates blank lines and trailing carriage returns (CRLF input). Fails closed on a missing header | ||
| 95 | * or any malformed record line. | ||
| 96 | * @param text The manifest text. | ||
| 97 | * @return The parsed records, or a @ref ManifestError. | ||
| 98 | */ | ||
| 99 | [[nodiscard]] std::expected<std::vector<DriftRecord>, ManifestError> parse_drift_report(std::string_view text); | ||
| 100 | |||
| 101 | /** | ||
| 102 | * @brief Writes a drift report to a file via @ref serialize_drift_report. | ||
| 103 | * @param path Destination file path (UTF-8). | ||
| 104 | * @param entries The drift entries to serialize. | ||
| 105 | * @return true on success, false if the file could not be opened or written. | ||
| 106 | * @note The write is not atomic: it truncates @p path in place, so a crash or power loss mid-write can leave a | ||
| 107 | * partial manifest. That is acceptable here because the manifest is a regenerable diagnostic/diff | ||
| 108 | * artifact (offsets are re-healed every session, never loaded as load-bearing state); a torn file is | ||
| 109 | * reported as MalformedLine / MissingHeader on the next read and overwritten. Do not route load-bearing | ||
| 110 | * data through this path without first making the write atomic (temp file + replace). | ||
| 111 | */ | ||
| 112 | [[nodiscard]] bool write_drift_report_to_file(const std::string &path, std::span<const DriftEntry> entries); | ||
| 113 | |||
| 114 | /** | ||
| 115 | * @brief Reads and parses a drift manifest file. | ||
| 116 | * @param path Source file path (UTF-8). | ||
| 117 | * @return The parsed records, or a @ref ManifestError: @ref ManifestError::FileOpenFailed when the file cannot | ||
| 118 | * be opened, or a parse error (@ref ManifestError::MissingHeader / @ref ManifestError::MalformedLine) | ||
| 119 | * when the file is present but its contents are corrupt. An opened-but-empty file reports | ||
| 120 | * MissingHeader. | ||
| 121 | */ | ||
| 122 | [[nodiscard]] std::expected<std::vector<DriftRecord>, ManifestError> | ||
| 123 | read_drift_report_from_file(const std::string &path); | ||
| 124 | } // namespace Rtti | ||
| 125 | } // namespace DetourModKit | ||
| 126 | |||
| 127 | #endif // DETOURMODKIT_DRIFT_MANIFEST_HPP | ||
| 128 |