cache_file_reader.cpp 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. #include <Windows.h>
  2. #include <cstdint>
  3. #include <cstring>
  4. #include "../internal.h"
  5. #include "cache_payload_reader.h"
  6. namespace sunrise::state::build_data::cache {
  7. namespace {
  8. /** @return True when every required domain is nonempty. */
  9. [[nodiscard]] bool required_domains_present(const records::DomainCounts& counts) noexcept {
  10. return counts.named != 0 && counts.items != 0 && counts.collectibles != 0
  11. && counts.materialRequirementSets != 0 && counts.socketPlugRules != 0
  12. && counts.socketPlugPools != 0 && counts.inventoryBuckets != 0
  13. && counts.socketEntryLists != 0 && counts.progressions != 0 && counts.scenarios != 0
  14. && counts.rosterGroups != 0;
  15. }
  16. /** @return True when every count fits the output storage. */
  17. [[nodiscard]] bool counts_fit(const records::DomainCounts& counts,
  18. records::MutableDomains output) noexcept {
  19. return counts.named <= output.named.size() && counts.items <= output.items.size()
  20. && counts.collectibles <= output.collectibles.size()
  21. && counts.materialRequirementSets <= output.materialRequirementSets.size()
  22. && counts.itemDetails <= output.itemDetails.size()
  23. && counts.socketPlugRules <= output.socketPlugRules.size()
  24. && counts.socketPlugPools <= output.socketPlugPools.size()
  25. && counts.socketPlugMembers <= output.socketPlugMembers.size()
  26. && counts.inventoryBuckets <= output.inventoryBuckets.size()
  27. && counts.socketEntryLists <= output.socketEntryLists.size()
  28. && counts.socketEntryTables <= output.socketEntryTables.size()
  29. && counts.abilityBuckets <= output.abilityBuckets.size()
  30. && counts.progressions <= output.progressions.size()
  31. && counts.scenarios <= output.scenarios.size()
  32. && counts.rosterGroups <= output.rosterGroups.size()
  33. && counts.spawnStems <= output.spawnStems.size()
  34. && counts.spawnNameHashes <= output.spawnNameHashes.size()
  35. && counts.spawnPoints <= output.spawnPoints.size()
  36. && counts.hashNames <= output.hashNames.size()
  37. && counts.vendorIndex <= output.vendorIndex.size()
  38. && counts.vendorDefinitions <= output.vendorDefinitions.size()
  39. && counts.vendorSaleRows <= output.vendorSaleRows.size()
  40. && counts.vendorInstalledRows <= output.vendorInstalledRows.size();
  41. }
  42. /** @return The header's row counts, as platform sizes. */
  43. [[nodiscard]] records::DomainCounts counts_of(const records::Header& header) noexcept {
  44. return {
  45. header.namedCount,
  46. header.itemCount,
  47. header.collectibleCount,
  48. header.materialRequirementSetCount,
  49. header.itemDetailCount,
  50. header.socketPlugRuleCount,
  51. header.socketPlugPoolCount,
  52. header.socketPlugMemberCount,
  53. header.inventoryBucketCount,
  54. header.socketEntryListCount,
  55. header.socketEntryTableCount,
  56. header.abilityBucketCount,
  57. header.progressionCount,
  58. header.scenarioCount,
  59. header.rosterGroupCount,
  60. header.spawnStemCount,
  61. header.spawnNameHashCount,
  62. header.spawnPointCount,
  63. header.hashNameCount,
  64. header.vendorIndexCount,
  65. header.vendorDefinitionCount,
  66. header.vendorSaleRowCount,
  67. header.vendorInstalledRowCount,
  68. };
  69. }
  70. /**
  71. * A cache written by any other format is out of date, so a version bump needs no edit here.
  72. * Listing them one by one left a bumped version unknown, and a valid cache read as corrupt.
  73. * A newer file is another build's cache rather than a damaged one, so it rebuilds the same way.
  74. * Reading it as corrupt instead failed the whole boot until the file was deleted by hand, which
  75. * is what downgrading the module did.
  76. * @param version Cache prefix version.
  77. * @return True when the cache was not written by the current format.
  78. */
  79. [[nodiscard]] bool stale_format(std::uint32_t version) noexcept {
  80. return version != records::kCacheFormatVersion;
  81. }
  82. /** @return The pending status, or invalid when the file fails to close. */
  83. [[nodiscard]] LoadStatus close_with(HANDLE file, LoadStatus status) noexcept {
  84. return CloseHandle(file) != FALSE ? status : LoadStatus::invalid;
  85. }
  86. } // namespace
  87. /** Reads the PE identity and ties it to the configured-equipment hash. */
  88. bool current_build_identity(std::uint64_t configuredEquipmentHash,
  89. BuildIdentity& identity) noexcept {
  90. identity = {};
  91. const HMODULE module = GetModuleHandleW(nullptr);
  92. if (module == nullptr) {
  93. return false;
  94. }
  95. const auto* image = reinterpret_cast<const std::byte*>(module);
  96. IMAGE_DOS_HEADER dos{};
  97. std::memcpy(&dos, image, sizeof dos);
  98. if (dos.e_magic != IMAGE_DOS_SIGNATURE || dos.e_lfanew <= 0) {
  99. return false;
  100. }
  101. IMAGE_NT_HEADERS64 nt{};
  102. std::memcpy(&nt, image + dos.e_lfanew, sizeof nt);
  103. if (nt.Signature != IMAGE_NT_SIGNATURE
  104. || nt.OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC
  105. || nt.OptionalHeader.SizeOfImage == 0) {
  106. return false;
  107. }
  108. identity.imageTimestamp = nt.FileHeader.TimeDateStamp;
  109. identity.imageSize = nt.OptionalHeader.SizeOfImage;
  110. identity.configuredEquipmentHash = configuredEquipmentHash;
  111. return true;
  112. }
  113. /** Loads every build-bound domain and commits the counts only after the file closes. */
  114. LoadStatus load(const wchar_t* path,
  115. const BuildIdentity& expectedBuild,
  116. records::MutableDomains output,
  117. records::DomainCounts& counts) noexcept {
  118. counts = {};
  119. read::clear(output);
  120. if (path == nullptr || expectedBuild.imageSize == 0 || output.constants == nullptr) {
  121. return LoadStatus::invalid;
  122. }
  123. const HANDLE file = CreateFileW(path,
  124. GENERIC_READ,
  125. FILE_SHARE_READ,
  126. nullptr,
  127. OPEN_EXISTING,
  128. FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN,
  129. nullptr);
  130. if (file == INVALID_HANDLE_VALUE) {
  131. const DWORD error = GetLastError();
  132. return error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND ? LoadStatus::missing
  133. : LoadStatus::invalid;
  134. }
  135. LARGE_INTEGER actualSize{};
  136. records::Prefix prefix{};
  137. if (GetFileSizeEx(file, &actualSize) == FALSE || actualSize.QuadPart <= 0
  138. || !read::read_value(file, prefix) || prefix.magic != records::kCacheMagic) {
  139. return close_with(file, LoadStatus::invalid);
  140. }
  141. if (stale_format(prefix.version)) {
  142. return close_with(file, LoadStatus::stale);
  143. }
  144. LARGE_INTEGER beginning{};
  145. records::Header header{};
  146. if (SetFilePointerEx(file, beginning, nullptr, FILE_BEGIN) == FALSE
  147. || !read::read_value(file, header) || header.magic != records::kCacheMagic
  148. || header.version != records::kCacheFormatVersion) {
  149. return close_with(file, LoadStatus::invalid);
  150. }
  151. const BuildIdentity cachedBuild{
  152. header.imageTimestamp,
  153. header.imageSize,
  154. header.configuredEquipmentHash,
  155. };
  156. if (!(cachedBuild == expectedBuild)) {
  157. return close_with(file, LoadStatus::stale);
  158. }
  159. const records::DomainCounts pendingCounts = counts_of(header);
  160. std::uint64_t expectedSize = 0;
  161. std::uint64_t checksum = 0;
  162. bool valid = required_domains_present(pendingCounts) && counts_fit(pendingCounts, output)
  163. && read::expected_size(pendingCounts, expectedSize)
  164. && static_cast<std::uint64_t>(actualSize.QuadPart) == expectedSize
  165. && read::read_payload(file, header.constants, pendingCounts, output, checksum)
  166. && checksum == header.payloadChecksum;
  167. const LoadStatus status = close_with(file, valid ? LoadStatus::loaded : LoadStatus::invalid);
  168. if (status != LoadStatus::loaded) {
  169. // Counts and rows commit together only after the file handle closes cleanly.
  170. read::clear(output);
  171. return status;
  172. }
  173. counts = pendingCounts;
  174. // Header scalars commit with the counts, on the same clean-close path as the record arrays.
  175. *output.constants = header.constants;
  176. return LoadStatus::loaded;
  177. }
  178. } // namespace sunrise::state::build_data::cache