activity_sdk_decode_plans.cpp 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. #include "activity_sdk_decode_plans.h"
  2. #include <algorithm>
  3. #include <array>
  4. #include <cstdio>
  5. #include <cstring>
  6. #include <memory>
  7. #include <new>
  8. #include <set>
  9. #include <string>
  10. #include <tuple>
  11. #include <utility>
  12. #include "../../../core/filesystem/temporary_sibling.h"
  13. #include "../../../core/logging/log.h"
  14. #include "../../../state/gameplay/external/rsat_decode_plans.h"
  15. #include "activity_sdk_decode_plans_internal.h"
  16. namespace sunrise::client::content::activity::sdk_generation::decode_plans {
  17. namespace {
  18. using namespace internal;
  19. /** Cache file identity: magic, version 2, the client build it was compiled against. */
  20. constexpr std::string_view kMagic = "SRRSATP2";
  21. constexpr std::uint32_t kVersion = 2;
  22. constexpr std::uint32_t kClientBuild = 86657;
  23. constexpr std::size_t kHeaderSize = 136;
  24. /** Identity of this builder; its text is hashed into the evidence. */
  25. constexpr std::string_view kGeneratorId = "sunrise-rsat-decode-plans-builder-1";
  26. constexpr std::wstring_view kTemporarySuffix = L".tmp";
  27. /** Per-pair refusals logged before the rest are only counted. */
  28. constexpr std::uint32_t kLoggedErrorLimit = 16;
  29. /** One plan row of the cache: component, selection, entry range, bitmap bits, active. */
  30. struct PlanRow final {
  31. std::uint32_t component{};
  32. std::uint32_t schema{};
  33. std::uint32_t first{};
  34. std::uint32_t count{};
  35. std::uint32_t bits{};
  36. std::uint32_t active{};
  37. };
  38. /** One package codec row of the cache. */
  39. struct ExtraRow final {
  40. std::uint32_t handle{};
  41. std::uint32_t original{};
  42. std::uint32_t serialized{};
  43. std::uint32_t flags{};
  44. std::uint32_t array{};
  45. std::uint32_t first{};
  46. std::uint32_t count{};
  47. };
  48. // Rows are copied as raw bytes, so their layouts must match the cache format exactly.
  49. static_assert(sizeof(PlanRow) == 24 && sizeof(PlanEntry) == 20 && sizeof(SchemaRow) == 16
  50. && sizeof(ExtraRow) == 28);
  51. template <typename T> void put(std::vector<std::byte>& bytes, const T& value) {
  52. const std::size_t at = bytes.size();
  53. bytes.resize(at + sizeof(T));
  54. std::memcpy(bytes.data() + at, &value, sizeof(T));
  55. }
  56. /** Appends one 40-byte field row in cache order. */
  57. void put_field(std::vector<std::byte>& bytes, const Field& field) {
  58. put(bytes, field.offset);
  59. put(bytes, field.alternate);
  60. put(bytes, field.bit);
  61. put(bytes, field.half);
  62. put(bytes, field.type);
  63. put(bytes, field.presence);
  64. put(bytes, field.parameter2);
  65. put(bytes, field.nested);
  66. put(bytes, field.bias);
  67. put(bytes, field.width);
  68. put(bytes, field.parameter3);
  69. put(bytes, field.parameter4);
  70. }
  71. void append_hex(std::string& text, std::span<const std::byte> bytes) {
  72. // Digests are lowercase hex in the evidence and the manifest.
  73. constexpr std::string_view digits = "0123456789abcdef";
  74. for (const std::byte value : bytes) {
  75. text.push_back(digits[static_cast<std::size_t>(value) >> 4U]);
  76. text.push_back(digits[static_cast<std::size_t>(value) & 0x0FU]);
  77. }
  78. }
  79. void append_handle(std::string& text, std::uint32_t handle) {
  80. std::array<char, 16> buffer{};
  81. const int written = std::snprintf(buffer.data(), buffer.size(), "%08X", handle);
  82. text.append(buffer.data(), written > 0 ? static_cast<std::size_t>(written) : 0U);
  83. }
  84. /** Compact JSON of the evidence in key order; its exact bytes are hashed into the fingerprint. */
  85. void append_evidence(std::string& text,
  86. const Digest& executable,
  87. const Digest& generator,
  88. const std::map<std::uint32_t, Digest>& packages) {
  89. text.append("{\"executableSha256\":\"");
  90. append_hex(text, executable);
  91. text.append("\",\"generatorSha256\":\"");
  92. append_hex(text, generator);
  93. text.append("\",\"packageSha256\":{");
  94. bool first = true;
  95. for (const auto& [handle, digest] : packages) {
  96. text.append(first ? "\"" : ",\"");
  97. first = false;
  98. append_handle(text, handle);
  99. text.append("\":\"");
  100. append_hex(text, digest);
  101. text.push_back('"');
  102. }
  103. text.append("}}");
  104. }
  105. /** Two-space indented manifest in key order. */
  106. void append_manifest(std::string& text,
  107. const Digest& sdk,
  108. const Digest& fingerprint,
  109. const Digest& executable,
  110. const Digest& generator,
  111. const Digest& payload,
  112. const std::map<std::uint32_t, Digest>& packages) {
  113. text.append("{\n \"build\": 86657,\n \"evidenceFingerprint\": \"");
  114. append_hex(text, fingerprint);
  115. text.append("\",\n \"executableSha256\": \"");
  116. append_hex(text, executable);
  117. text.append("\",\n \"format\": \"SRRSATP2\",\n \"generatorSha256\": \"");
  118. append_hex(text, generator);
  119. text.append("\",\n \"packageSha256\": {");
  120. bool first = true;
  121. for (const auto& [handle, digest] : packages) {
  122. text.append(first ? "\n \"" : ",\n \"");
  123. first = false;
  124. append_handle(text, handle);
  125. text.append("\": \"");
  126. append_hex(text, digest);
  127. text.push_back('"');
  128. }
  129. text.append(first ? "},\n \"payloadSha256\": \"" : "\n },\n \"payloadSha256\": \"");
  130. append_hex(text, payload);
  131. text.append("\",\n \"sdkBuildHash\": \"");
  132. append_hex(text, sdk);
  133. text.append("\"\n}\n");
  134. }
  135. /**
  136. * Writes one complete file through a sibling so a reader never sees a partial file.
  137. * @return False when the sibling could not be written or published.
  138. */
  139. [[nodiscard]] bool write_file(std::wstring_view finalPath,
  140. std::span<const std::byte> bytes) noexcept {
  141. try {
  142. const std::wstring final(finalPath);
  143. const std::wstring temporary = final + std::wstring(kTemporarySuffix);
  144. const HANDLE file = CreateFileW(temporary.c_str(),
  145. GENERIC_WRITE,
  146. 0,
  147. nullptr,
  148. CREATE_ALWAYS,
  149. FILE_ATTRIBUTE_NORMAL,
  150. nullptr);
  151. if (file == INVALID_HANDLE_VALUE) {
  152. return false;
  153. }
  154. DWORD written = 0;
  155. const bool complete =
  156. WriteFile(file, bytes.data(), static_cast<DWORD>(bytes.size()), &written, nullptr)
  157. != FALSE
  158. && written == bytes.size();
  159. if (CloseHandle(file) == FALSE || !complete) {
  160. (void)DeleteFileW(temporary.c_str());
  161. return false;
  162. }
  163. return core::path::publish_sibling(temporary.c_str(), final.c_str());
  164. } catch (...) {
  165. return false;
  166. }
  167. }
  168. /** Logs one refused pair; the cache leaves it out. */
  169. void log_pair_error(std::uint32_t component, std::uint32_t schema, const char* detail) noexcept {
  170. std::array<char, 160> line{};
  171. const int written = std::snprintf(line.data(),
  172. line.size(),
  173. "ev=activity_sdk_decoder_cache result=skip component=0x%08X "
  174. "selection=0x%08X detail=%s",
  175. component,
  176. schema,
  177. detail == nullptr ? "unknown" : detail);
  178. if (written > 0) {
  179. core::log::write(
  180. core::log::Channel::client,
  181. core::log::Level::warn,
  182. {line.data(), (std::min)(static_cast<std::size_t>(written), line.size() - 1U)});
  183. }
  184. }
  185. /** Reads every package codec reachable from the compiled metadata into sorted rows. */
  186. [[nodiscard]] bool collect_extras(Store& store,
  187. std::vector<ExtraRow>& rows,
  188. std::vector<Field>& fields,
  189. std::vector<SchemaRow>& schemas,
  190. std::vector<std::uint32_t>& fieldValues) {
  191. std::vector<std::uint32_t> pending;
  192. for (const auto& [handle, record] : store.entries()) {
  193. if (record.package) {
  194. pending.push_back(handle);
  195. }
  196. }
  197. std::map<std::uint32_t, Codec> extras;
  198. const char* error = nullptr;
  199. while (!pending.empty()) {
  200. const std::uint32_t handle = pending.back();
  201. pending.pop_back();
  202. if (extras.contains(handle)) {
  203. continue;
  204. }
  205. const Metadata* record = store.get(handle, error);
  206. if (record == nullptr) {
  207. return false;
  208. }
  209. if (!record->codec.present) {
  210. continue;
  211. }
  212. const Codec codec = record->codec;
  213. for (const Field& field : codec.fields) {
  214. if (field.type == kTypeNested && !store.image().slots().contains(field.nested)) {
  215. pending.push_back(field.nested);
  216. }
  217. }
  218. extras.emplace(handle, codec);
  219. }
  220. for (const auto& [handle, codec] : extras) {
  221. rows.push_back({handle,
  222. codec.original,
  223. codec.serialized,
  224. codec.flags,
  225. codec.array,
  226. static_cast<std::uint32_t>(fields.size()),
  227. static_cast<std::uint32_t>(codec.fields.size())});
  228. fields.insert(fields.end(), codec.fields.begin(), codec.fields.end());
  229. schemas.push_back({handle,
  230. codec.serialized,
  231. static_cast<std::uint32_t>(fieldValues.size()),
  232. static_cast<std::uint32_t>(codec.fields.size())});
  233. for (const Field& field : codec.fields) {
  234. fieldValues.push_back(field.bit);
  235. }
  236. }
  237. return true;
  238. }
  239. [[nodiscard]] bool cancelled(const Request& request) noexcept {
  240. return request.cancel != nullptr && request.cancel(request.cancelContext);
  241. }
  242. /** Compiles every pair; refused pairs are counted and left out. */
  243. [[nodiscard]] Status
  244. compile_pairs(const Request& request, Store& store, std::vector<Plan>& plans, Result& output) {
  245. std::set<std::pair<std::uint32_t, std::uint32_t>> pairs;
  246. for (const auto& descriptor : request.descriptors) {
  247. if (descriptor.schemaTag != 0 && descriptor.schemaTag != kAbsent) {
  248. pairs.emplace(descriptor.componentTag, descriptor.schemaTag);
  249. }
  250. }
  251. output.pairs = static_cast<std::uint32_t>(pairs.size());
  252. std::vector<std::byte> blob;
  253. for (const auto& [component, schemaTag] : pairs) {
  254. if (cancelled(request)) {
  255. return Status::cancelled;
  256. }
  257. const char* error = nullptr;
  258. Digest digest{};
  259. Plan plan{};
  260. plan.schemaTag = schemaTag;
  261. bool complete = store.read_package(schemaTag, kSelectionClass, blob);
  262. if (!complete) {
  263. error = "selection_class";
  264. } else if (!middleware::crypto::sha256::hash(blob, digest)) {
  265. return Status::hashFailed;
  266. } else {
  267. store.package_hashes()[schemaTag] = digest;
  268. complete = compile(store, component, blob, plan, error);
  269. }
  270. if (!complete) {
  271. if (output.errors < kLoggedErrorLimit) {
  272. log_pair_error(component, schemaTag, error);
  273. }
  274. ++output.errors;
  275. continue;
  276. }
  277. plans.push_back(std::move(plan));
  278. }
  279. return Status::ready;
  280. }
  281. /** Owns the package reader storage for one build. */
  282. struct ScratchOwner final {
  283. std::unique_ptr<reader::Scratch> value{new (std::nothrow) reader::Scratch()};
  284. ~ScratchOwner() noexcept {
  285. if (value != nullptr) {
  286. reader::close_files(*value);
  287. }
  288. }
  289. };
  290. /**
  291. * Compiles the plans, rows the codec index and writes the cache pair.
  292. * @param request Validated build inputs.
  293. * @param output Receives the counts as they are known, even on failure.
  294. * @return Status::ready when both files are on disk.
  295. */
  296. [[nodiscard]] Status run(const Request& request, Result& output) {
  297. Image image;
  298. const Status opened =
  299. image.open(std::wstring(request.executablePath).c_str(), request.executableModule);
  300. if (opened != Status::ready) {
  301. return opened;
  302. }
  303. output.slots = static_cast<std::uint32_t>(image.slots().size());
  304. ScratchOwner scratch;
  305. if (scratch.value == nullptr) {
  306. return Status::allocation;
  307. }
  308. Store store(image, *request.source, *scratch.value);
  309. std::vector<Plan> plans;
  310. const Status compiled = compile_pairs(request, store, plans, output);
  311. if (compiled != Status::ready) {
  312. return compiled;
  313. }
  314. std::vector<SchemaRow> schemas;
  315. std::vector<std::uint32_t> fieldValues;
  316. const char* error = nullptr;
  317. if (!build_codec_index(image, schemas, fieldValues, error)) {
  318. return Status::reflectionInvalid;
  319. }
  320. std::vector<ExtraRow> extraRows;
  321. std::vector<Field> extraFields;
  322. if (!collect_extras(store, extraRows, extraFields, schemas, fieldValues)) {
  323. return Status::packageRead;
  324. }
  325. std::sort(schemas.begin(), schemas.end(), [](const SchemaRow& one, const SchemaRow& other) {
  326. return std::tie(one.handle, one.size, one.first, one.count)
  327. < std::tie(other.handle, other.size, other.first, other.count);
  328. });
  329. std::vector<PlanRow> rows;
  330. std::vector<PlanEntry> entries;
  331. for (const Plan& plan : plans) {
  332. rows.push_back({plan.component,
  333. plan.schemaTag,
  334. static_cast<std::uint32_t>(entries.size()),
  335. static_cast<std::uint32_t>(plan.entries.size()),
  336. plan.bitmapBits,
  337. plan.active ? 1U : 0U});
  338. entries.insert(entries.end(), plan.entries.begin(), plan.entries.end());
  339. output.active += plan.active ? 1U : 0U;
  340. }
  341. output.plans = static_cast<std::uint32_t>(rows.size());
  342. output.entries = static_cast<std::uint32_t>(entries.size());
  343. output.schemas = static_cast<std::uint32_t>(schemas.size());
  344. output.fields = static_cast<std::uint32_t>(fieldValues.size());
  345. std::vector<std::byte> payload;
  346. for (const PlanRow& row : rows) {
  347. put(payload, row);
  348. }
  349. for (const PlanEntry& entry : entries) {
  350. put(payload, entry);
  351. }
  352. for (const SchemaRow& row : schemas) {
  353. put(payload, row);
  354. }
  355. for (const std::uint32_t value : fieldValues) {
  356. put(payload, value);
  357. }
  358. for (const ExtraRow& row : extraRows) {
  359. put(payload, row);
  360. }
  361. for (const Field& field : extraFields) {
  362. put_field(payload, field);
  363. }
  364. Digest executable{};
  365. Digest generator{};
  366. Digest payloadDigest{};
  367. Digest fingerprint{};
  368. const std::span<const std::byte> generatorText(
  369. reinterpret_cast<const std::byte*>(kGeneratorId.data()), kGeneratorId.size());
  370. if (!middleware::crypto::sha256::hash(image.file(), executable)
  371. || !middleware::crypto::sha256::hash(generatorText, generator)
  372. || !middleware::crypto::sha256::hash(payload, payloadDigest)) {
  373. return Status::hashFailed;
  374. }
  375. std::string evidence;
  376. append_evidence(evidence, executable, generator, store.package_hashes());
  377. if (!middleware::crypto::sha256::hash(
  378. std::span(reinterpret_cast<const std::byte*>(evidence.data()), evidence.size()),
  379. fingerprint)) {
  380. return Status::hashFailed;
  381. }
  382. Digest sdk{};
  383. std::copy(request.sdkBuildSha256.begin(), request.sdkBuildSha256.end(), sdk.begin());
  384. std::vector<std::byte> cache;
  385. cache.reserve(kHeaderSize + payload.size());
  386. cache.insert(cache.end(),
  387. reinterpret_cast<const std::byte*>(kMagic.data()),
  388. reinterpret_cast<const std::byte*>(kMagic.data()) + kMagic.size());
  389. put(cache, kVersion);
  390. put(cache, kClientBuild);
  391. cache.insert(cache.end(), sdk.begin(), sdk.end());
  392. cache.insert(cache.end(), fingerprint.begin(), fingerprint.end());
  393. cache.insert(cache.end(), payloadDigest.begin(), payloadDigest.end());
  394. put(cache, static_cast<std::uint32_t>(rows.size()));
  395. put(cache, static_cast<std::uint32_t>(entries.size()));
  396. put(cache, static_cast<std::uint32_t>(schemas.size()));
  397. put(cache, static_cast<std::uint32_t>(fieldValues.size()));
  398. put(cache, static_cast<std::uint32_t>(extraRows.size()));
  399. put(cache, static_cast<std::uint32_t>(extraFields.size()));
  400. cache.insert(cache.end(), payload.begin(), payload.end());
  401. std::string manifest;
  402. append_manifest(
  403. manifest, sdk, fingerprint, executable, generator, payloadDigest, store.package_hashes());
  404. const std::wstring manifestPath =
  405. std::wstring(request.cachePath)
  406. + std::wstring(state::gameplay::rsat_decode_plans::kManifestSuffix);
  407. if (!write_file(request.cachePath, cache)
  408. || !write_file(
  409. manifestPath,
  410. std::span(reinterpret_cast<const std::byte*>(manifest.data()), manifest.size()))) {
  411. return Status::writeFailed;
  412. }
  413. return Status::ready;
  414. }
  415. } // namespace
  416. /**
  417. * Checks the request, then runs the build with allocation failures mapped to a status.
  418. * @param request Executable, package source, descriptor pairs, SDK identity and output path.
  419. * @param output Receives the counts of the written cache.
  420. * @return Status::ready when both files are complete on disk.
  421. */
  422. Status build(const Request& request, Result& output) noexcept {
  423. output = {};
  424. if (request.source == nullptr || request.executablePath.empty() || request.cachePath.empty()
  425. || request.sdkBuildSha256.size() != Digest{}.size()) {
  426. return Status::invalidInput;
  427. }
  428. try {
  429. return run(request, output);
  430. } catch (...) {
  431. return Status::allocation;
  432. }
  433. }
  434. /** @return The stable log name of one build status. */
  435. const char* status_name(Status status) noexcept {
  436. switch (status) {
  437. case Status::ready:
  438. return "ready";
  439. case Status::cancelled:
  440. return "cancelled";
  441. case Status::invalidInput:
  442. return "invalid_input";
  443. case Status::executableUnavailable:
  444. return "executable_unavailable";
  445. case Status::executableInvalid:
  446. return "executable_invalid";
  447. case Status::reflectionInvalid:
  448. return "reflection_invalid";
  449. case Status::packageRead:
  450. return "package_read";
  451. case Status::hashFailed:
  452. return "hash_failed";
  453. case Status::writeFailed:
  454. return "write_failed";
  455. case Status::allocation:
  456. return "allocation";
  457. }
  458. return "unknown";
  459. }
  460. } // namespace sunrise::client::content::activity::sdk_generation::decode_plans