yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
native_save_receipt.cc
Go to the documentation of this file.
2
3#include "absl/status/status.h"
4
5#if defined(YAZE_WITH_JSON)
6
7#include <algorithm>
8#include <array>
9#include <exception>
10#include <fstream>
11#include <limits>
12#include <map>
13#include <optional>
14#include <set>
15#include <string_view>
16#include <utility>
17
18#include "absl/strings/ascii.h"
19#include "absl/strings/str_cat.h"
21#include "nlohmann/json.hpp"
22#include "util/macro.h"
23#include "util/platform_paths.h"
24
25namespace yaze::core {
26namespace {
27
28namespace fs = std::filesystem;
29using Json = nlohmann::json;
30constexpr char kKind[] = "yaze.native_save.v1";
31constexpr char kContextKind[] = "yaze.native_save_context.v1";
32constexpr uint64_t kMaxRomBytes = 64 * 1024 * 1024;
33constexpr uint64_t kMaxReceiptBytes = 1024 * 1024;
34constexpr size_t kMaxArtifacts = 256;
35
36bool IsIdentifier(std::string_view value, bool role = false) {
37 if (value.empty() || value.size() > 128 ||
38 (role && (value.front() < 'a' || value.front() > 'z'))) {
39 return false;
40 }
41 return std::all_of(value.begin(), value.end(), [role](char c) {
42 return (c >= 'a' && c <= 'z') || (!role && c >= 'A' && c <= 'Z') ||
43 (c >= '0' && c <= '9') || c == '.' || c == '_' || c == ':' ||
44 c == '-';
45 });
46}
47
48bool IsSha256(std::string_view value) {
49 return value.size() == 64 &&
50 std::all_of(value.begin(), value.end(), [](char c) {
51 return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f');
52 });
53}
54
55bool IsOperationId(std::string_view value) {
56 return !value.empty() && value.size() <= 128 &&
57 std::all_of(value.begin(), value.end(),
58 [](char c) { return c >= 0x20 && c <= 0x7E; });
59}
60
61std::string PathText(const fs::path& path) {
62 const auto bytes = path.generic_u8string();
63 return std::string(bytes.begin(), bytes.end());
64}
65
66bool IsAbsolutePath(const fs::path& path) {
67 return !path.empty() && path.is_absolute() &&
68 path.string().find('\0') == std::string::npos;
69}
70
71bool AreBindingErrorsValid(const std::vector<std::string>& errors) {
72 return errors.size() <= kMaxArtifacts &&
73 std::all_of(errors.begin(), errors.end(), [](const auto& error) {
74 return !error.empty() && error.size() <= 1024 &&
75 error.find('\0') == std::string::npos;
76 });
77}
78
79absl::StatusOr<fs::path> ResolveRegularFile(const fs::path& path) {
80 std::error_code error;
81 const auto status = fs::symlink_status(path, error);
82 if (error || !fs::is_regular_file(status)) {
83 return absl::FailedPreconditionError(
84 absl::StrCat("Native save input must be a regular, non-symlink file: ",
85 path.string()));
86 }
87 auto resolved = fs::canonical(path, error);
88 if (error) {
89 return absl::FailedPreconditionError(
90 absl::StrCat("Cannot resolve native save input: ", path.string()));
91 }
92 return resolved;
93}
94
95absl::StatusOr<uint64_t> FileSize(const fs::path& path) {
96 std::error_code error;
97 const auto size = fs::file_size(path, error);
98 if (error || size > std::numeric_limits<uint64_t>::max()) {
99 return absl::FailedPreconditionError(
100 absl::StrCat("Cannot measure native save input: ", path.string()));
101 }
102 return static_cast<uint64_t>(size);
103}
104
105absl::StatusOr<std::string> ReadBoundedFile(const fs::path& path,
106 uint64_t maximum) {
107 ASSIGN_OR_RETURN(const auto size, FileSize(path));
108 if (size > maximum) {
109 return absl::InvalidArgumentError("Native save input exceeds size limit");
110 }
111 std::ifstream input(path, std::ios::binary);
112 if (!input.is_open()) {
113 return absl::FailedPreconditionError("Cannot read native save input");
114 }
115 std::string content;
116 std::array<char, 64 * 1024> buffer;
117 while (input) {
118 input.read(buffer.data(), buffer.size());
119 const auto count = static_cast<uint64_t>(input.gcount());
120 if (count > maximum - content.size()) {
121 return absl::InvalidArgumentError("Native save input exceeds size limit");
122 }
123 content.append(buffer.data(), static_cast<size_t>(count));
124 }
125 if (!input.eof() || input.bad()) {
126 return absl::DataLossError("Failed while reading native save input");
127 }
128 return content;
129}
130
131absl::StatusOr<std::optional<std::string>> ReadOptionalOutput(
132 const fs::path& path) {
133 std::error_code error;
134 const auto status = fs::symlink_status(path, error);
135 if (error == std::errc::no_such_file_or_directory ||
136 (!error && status.type() == fs::file_type::not_found)) {
137 return std::nullopt;
138 }
139 ASSIGN_OR_RETURN(const auto resolved, ResolveRegularFile(path));
140 ASSIGN_OR_RETURN(auto content, ReadBoundedFile(resolved, kMaxReceiptBytes));
141 return std::optional<std::string>(std::move(content));
142}
143
144bool IsProducerArtifact(const Json& value) {
145 return value.is_object() && value.size() == 4 &&
146 value.value("role", Json()) == "producer" && value.contains("path") &&
147 value["path"].is_string() &&
148 IsAbsolutePath(fs::u8path(value["path"].get<std::string>())) &&
149 value.contains("sha256") && value["sha256"].is_string() &&
150 IsSha256(value["sha256"].get_ref<const std::string&>()) &&
151 value.contains("bytes") && value["bytes"].is_number_unsigned();
152}
153
154absl::Status ValidateContext(const NativeSaveContext& context) {
156 const auto expected,
157 GetNativeSaveContext(context.editing_source_path, context.project_path));
158 if (context.path != expected.path ||
159 context.editing_source_path != expected.editing_source_path ||
160 context.project_path != expected.project_path) {
161 return absl::InvalidArgumentError("Native save context is not current");
162 }
163 return absl::OkStatus();
164}
165
166absl::Status ValidatePreviousContext(const Json& value,
167 const NativeSaveContext& context) {
168 if (!value.is_object() || value.size() != 7 ||
169 value.value("kind", Json()) != kContextKind ||
170 value.value("editing_source_path", Json()) !=
171 context.editing_source_path ||
172 !value.contains("project_path") || !value["project_path"].is_string() ||
173 !value.contains("state") || !value["state"].is_string() ||
174 !value.contains("rom_path") || !value["rom_path"].is_string() ||
175 !IsAbsolutePath(fs::u8path(value["rom_path"].get<std::string>())) ||
176 !value.contains("producer") || !IsProducerArtifact(value["producer"]) ||
177 !value.contains("native_commit")) {
178 return absl::FailedPreconditionError("Invalid prior native save context");
179 }
180 const auto project_path = value["project_path"].get<std::string>();
181 if (!project_path.empty() && !IsAbsolutePath(fs::u8path(project_path))) {
182 return absl::FailedPreconditionError("Invalid native save context project");
183 }
184 if (value["state"] == "committed") {
185 const auto& commit = value["native_commit"];
186 if (!commit.is_object() || commit.size() != 3 ||
187 !commit.contains("receipt_path") ||
188 !commit["receipt_path"].is_string() ||
189 !IsAbsolutePath(
190 fs::u8path(commit["receipt_path"].get<std::string>())) ||
191 !commit.contains("generation") ||
192 !commit["generation"].is_number_unsigned() ||
193 commit["generation"].get<uint64_t>() == 0 ||
194 !commit.contains("operation_id") ||
195 !commit["operation_id"].is_string() ||
196 !IsOperationId(commit["operation_id"].get_ref<const std::string&>())) {
197 return absl::FailedPreconditionError(
198 "Invalid committed native save context");
199 }
200 } else if (value["state"] != "rebound" || !value["native_commit"].is_null()) {
201 return absl::FailedPreconditionError("Invalid retired native save context");
202 }
203 return absl::OkStatus();
204}
205
206absl::Status ValidatePreviousReceipt(const Json& receipt,
207 const fs::path& receipt_path) {
208 if (!receipt.is_object() || receipt.size() != 8 ||
209 receipt.value("kind", Json()) != kKind ||
210 !receipt.contains("generation") ||
211 !receipt["generation"].is_number_unsigned() ||
212 receipt["generation"].get<uint64_t>() == 0 ||
213 !receipt.contains("operation_id") ||
214 !receipt["operation_id"].is_string() ||
215 !IsOperationId(receipt["operation_id"].get_ref<const std::string&>()) ||
216 (receipt.value("operation", Json()) != "save" &&
217 receipt.value("operation", Json()) != "save_as") ||
218 receipt.value("receipt_path", Json()) != PathText(receipt_path) ||
219 !receipt.contains("bindings_complete") ||
220 !receipt["bindings_complete"].is_boolean() ||
221 !receipt.contains("binding_errors") ||
222 !receipt["binding_errors"].is_array() || !receipt.contains("artifacts") ||
223 !receipt["artifacts"].is_array() ||
224 receipt["artifacts"].size() > kMaxArtifacts) {
225 return absl::FailedPreconditionError("Invalid prior native save receipt");
226 }
227 std::vector<std::string> errors;
228 for (const auto& error : receipt["binding_errors"]) {
229 if (!error.is_string()) {
230 return absl::FailedPreconditionError("Invalid prior native save errors");
231 }
232 errors.push_back(error.get<std::string>());
233 }
234 if (!AreBindingErrorsValid(errors) ||
235 receipt["bindings_complete"] != errors.empty()) {
236 return absl::FailedPreconditionError("Invalid prior native save errors");
237 }
238 std::set<std::string> roles;
239 std::map<std::string, const Json*> paths;
240 for (const auto& artifact : receipt["artifacts"]) {
241 if (!artifact.is_object() || artifact.size() != 4 ||
242 !artifact.contains("role") || !artifact["role"].is_string() ||
243 !artifact.contains("path") || !artifact["path"].is_string() ||
244 !artifact.contains("sha256") || !artifact["sha256"].is_string() ||
245 !artifact.contains("bytes") ||
246 !artifact["bytes"].is_number_unsigned()) {
247 return absl::FailedPreconditionError(
248 "Invalid prior native save artifact");
249 }
250 const auto& role = artifact["role"].get_ref<const std::string&>();
251 const fs::path path = fs::u8path(artifact["path"].get<std::string>());
252 const auto& hash = artifact["sha256"].get_ref<const std::string&>();
253 const auto [existing, inserted] =
254 paths.emplace(absl::AsciiStrToLower(path.generic_string()), &artifact);
255 const bool conflicting_path =
256 !inserted && ((*existing->second)["path"] != artifact["path"] ||
257 (*existing->second)["sha256"] != artifact["sha256"] ||
258 (*existing->second)["bytes"] != artifact["bytes"]);
259 if (!IsIdentifier(role, true) || !roles.insert(role).second ||
260 !IsAbsolutePath(path) || path != path.lexically_normal() ||
261 conflicting_path || !IsSha256(hash)) {
262 return absl::FailedPreconditionError("Invalid prior native save binding");
263 }
264 }
265 if (!roles.contains("rom") || !roles.contains("producer")) {
266 return absl::FailedPreconditionError("Prior native save roles are missing");
267 }
268 return absl::OkStatus();
269}
270
271struct ArtifactBinding {
272 NativeSaveArtifact artifact;
273 fs::path resolved;
274 uint64_t bytes;
275};
276
277absl::Status VerifyBinding(const ArtifactBinding& binding) {
278 ASSIGN_OR_RETURN(const auto resolved,
279 ResolveRegularFile(binding.artifact.path));
280 if (resolved != binding.resolved) {
281 return absl::AbortedError("Native save input path changed");
282 }
283 ASSIGN_OR_RETURN(const auto size, FileSize(resolved));
284 ASSIGN_OR_RETURN(const auto hash, ComputeSourceArtifactFileSha256(resolved));
285 ASSIGN_OR_RETURN(const auto final_resolved,
286 ResolveRegularFile(binding.artifact.path));
287 ASSIGN_OR_RETURN(const auto final_size, FileSize(final_resolved));
288 if (final_resolved != resolved || size != binding.bytes ||
289 final_size != binding.bytes || hash != binding.artifact.expected_sha256) {
290 return absl::AbortedError("Native save artifact changed after save");
291 }
292 return absl::OkStatus();
293}
294
295} // namespace
296
297absl::StatusOr<NativeSaveContext> GetNativeSaveContext(
298 const std::string& editing_source_path, const std::string& project_path) {
299 if (!IsAbsolutePath(fs::u8path(editing_source_path)) ||
300 (!project_path.empty() && !IsAbsolutePath(fs::u8path(project_path)))) {
301 return absl::InvalidArgumentError(
302 "Native save editing-source and project paths must be absolute");
303 }
304 std::error_code error;
305 const auto origin =
306 fs::weakly_canonical(fs::u8path(editing_source_path), error);
307 if (error) {
308 return absl::FailedPreconditionError(
309 "Cannot resolve native editing source");
310 }
311 const auto origin_text = PathText(origin);
312 ASSIGN_OR_RETURN(const auto app_data,
314 const auto directory = fs::absolute(app_data, error) / "native-saves";
315 if (error) {
316 return absl::FailedPreconditionError(
317 "Cannot resolve native save directory");
318 }
319 fs::create_directories(directory, error);
320 if (error || !fs::is_directory(fs::symlink_status(directory, error))) {
321 return absl::FailedPreconditionError("Cannot create native save directory");
322 }
323 const auto path = fs::canonical(directory, error) /
324 (ComputeSourceArtifactSha256(origin_text) + ".json");
325 if (error) {
326 return absl::FailedPreconditionError("Cannot resolve native save context");
327 }
328 std::string normalized_project;
329 if (!project_path.empty()) {
330 normalized_project =
331 PathText(fs::weakly_canonical(fs::u8path(project_path), error));
332 if (error) {
333 return absl::FailedPreconditionError(
334 "Cannot resolve native save project");
335 }
336 }
337 return NativeSaveContext{path, origin_text, std::move(normalized_project)};
338}
339
340absl::Status RetireNativeSaveContext(const NativeSaveContext& context,
341 const std::string& state) {
342 if (state != "rebound" && state != "session_changed") {
343 return absl::InvalidArgumentError("Invalid native save retirement state");
344 }
345 RETURN_IF_ERROR(ValidateContext(context));
346 ASSIGN_OR_RETURN(const auto existing, ReadOptionalOutput(context.path));
347 if (!existing.has_value()) {
348 return absl::OkStatus();
349 }
350 ASSIGN_OR_RETURN(auto lock,
352 ASSIGN_OR_RETURN(const auto before, ReadOptionalOutput(context.path));
353 if (!before.has_value()) {
354 return absl::AbortedError("Native save context disappeared");
355 }
357 ValidatePreviousContext(Json::parse(*before, nullptr, false), context));
358 const auto previous = Json::parse(*before);
359 const Json retired = {{"kind", kContextKind},
360 {"editing_source_path", context.editing_source_path},
361 {"state", "rebound"},
362 {"project_path", context.project_path},
363 {"rom_path", previous["rom_path"]},
364 {"producer", previous["producer"]},
365 {"native_commit", nullptr}};
366 std::string text;
367 try {
368 text = retired.dump(2) + "\n";
369 } catch (const Json::exception&) {
370 return absl::InvalidArgumentError(
371 "Native save context must be valid UTF-8");
372 }
374 *lock, {{context.path, before, text}},
375 ComputeSourceArtifactSha256(*before), [&]() -> absl::Status {
376 ASSIGN_OR_RETURN(const auto resolved, ResolveRegularFile(context.path));
377 if (resolved != context.path) {
378 return absl::AbortedError("Native save context path changed");
379 }
380 return absl::OkStatus();
381 });
382}
383
384absl::StatusOr<NativeSaveReceipt> PublishNativeSaveReceipt(
385 const NativeSaveReceiptRequest& request) {
386 if (!IsAbsolutePath(request.receipt_path) ||
387 !IsOperationId(request.operation_id) ||
388 (request.operation != "save" && request.operation != "save_as") ||
389 request.artifacts.size() > kMaxArtifacts ||
390 !AreBindingErrorsValid(request.binding_errors)) {
391 return absl::InvalidArgumentError("Invalid native save receipt request");
392 }
393 std::set<std::string> roles;
394 std::vector<fs::path> inputs;
395 std::map<fs::path, std::string> input_hashes;
396 std::vector<ArtifactBinding> bindings;
397 for (const auto& artifact : request.artifacts) {
398 if (!IsIdentifier(artifact.role, true) ||
399 !roles.insert(artifact.role).second || !IsAbsolutePath(artifact.path) ||
400 !IsSha256(artifact.expected_sha256)) {
401 return absl::InvalidArgumentError("Invalid native save artifact binding");
402 }
403 ASSIGN_OR_RETURN(auto resolved, ResolveRegularFile(artifact.path));
404 ASSIGN_OR_RETURN(const auto size, FileSize(resolved));
405 if (artifact.role == "rom" && size > kMaxRomBytes) {
406 return absl::InvalidArgumentError("Native save ROM exceeds size limit");
407 }
408 const auto [existing, inserted] =
409 input_hashes.emplace(resolved, artifact.expected_sha256);
410 if (!inserted && existing->second != artifact.expected_sha256) {
411 return absl::InvalidArgumentError("Native save input hashes conflict");
412 }
413 if (inserted) {
414 inputs.push_back(resolved);
415 }
416 bindings.push_back({artifact, std::move(resolved), size});
417 }
418 if (!roles.contains("rom") || !roles.contains("producer")) {
419 return absl::InvalidArgumentError("Native save requires rom and producer");
420 }
421 std::vector<fs::path> outputs{request.receipt_path};
422 if (request.context.has_value()) {
423 RETURN_IF_ERROR(ValidateContext(*request.context));
424 outputs.push_back(request.context->path);
425 }
426
427 std::error_code error;
428 const auto receipt_path = fs::weakly_canonical(request.receipt_path, error);
429 if (error) {
430 return absl::FailedPreconditionError("Cannot resolve native save receipt");
431 }
433 auto lock,
435 outputs, {"native save receipt", "native save receipt"}, inputs));
436 const auto receipt_after_lock =
437 fs::weakly_canonical(request.receipt_path, error);
438 if (error || receipt_after_lock != receipt_path) {
439 return absl::AbortedError("Native save receipt path changed");
440 }
441 for (const auto& binding : bindings) {
442 RETURN_IF_ERROR(VerifyBinding(binding));
443 }
444 std::sort(bindings.begin(), bindings.end(),
445 [](const auto& left, const auto& right) {
446 return left.artifact.role < right.artifact.role;
447 });
448 Json artifacts = Json::array();
449 const ArtifactBinding* rom = nullptr;
450 Json producer;
451 for (const auto& binding : bindings) {
452 artifacts.push_back({{"role", binding.artifact.role},
453 {"path", PathText(binding.resolved)},
454 {"sha256", binding.artifact.expected_sha256},
455 {"bytes", binding.bytes}});
456 if (binding.artifact.role == "rom") {
457 rom = &binding;
458 } else if (binding.artifact.role == "producer") {
459 producer = artifacts.back();
460 }
461 }
462 ASSIGN_OR_RETURN(auto rom_bytes,
463 ReadBoundedFile(rom->resolved, kMaxRomBytes));
464 if (ComputeSourceArtifactSha256(rom_bytes) != rom->artifact.expected_sha256) {
465 return absl::AbortedError("Native save ROM changed after save");
466 }
467
468 std::optional<std::string> before;
469 std::optional<std::string> context_before;
470 std::optional<Json> context_previous;
471 if (request.context.has_value()) {
472 ASSIGN_OR_RETURN(context_before, ReadOptionalOutput(request.context->path));
473 if (context_before.has_value()) {
474 context_previous = Json::parse(*context_before, nullptr, false);
476 ValidatePreviousContext(*context_previous, *request.context));
477 }
478 }
479 uint64_t generation = 1;
480 Json receipt = {{"kind", kKind},
481 {"generation", generation},
482 {"operation_id", request.operation_id},
483 {"operation", request.operation},
484 {"receipt_path", PathText(receipt_path)},
485 {"bindings_complete", request.binding_errors.empty()},
486 {"binding_errors", request.binding_errors},
487 {"artifacts", std::move(artifacts)}};
488 bool replay = false;
489 const bool exists = fs::exists(receipt_path, error);
490 if (error) {
491 return absl::FailedPreconditionError("Cannot inspect native save receipt");
492 }
493 if (exists) {
494 ASSIGN_OR_RETURN(auto resolved, ResolveRegularFile(receipt_path));
495 ASSIGN_OR_RETURN(before, ReadBoundedFile(resolved, kMaxReceiptBytes));
496 const auto previous = Json::parse(*before, nullptr, false);
497 RETURN_IF_ERROR(ValidatePreviousReceipt(previous, receipt_path));
498 generation = previous["generation"].get<uint64_t>();
499 if (previous["operation_id"] == request.operation_id) {
500 replay = true;
501 if (previous["operation"] != request.operation ||
502 previous["artifacts"] != receipt["artifacts"] ||
503 previous["binding_errors"] != receipt["binding_errors"]) {
504 return absl::AbortedError("Native save operation_id was reused");
505 }
506 } else if (generation == std::numeric_limits<uint64_t>::max()) {
507 return absl::OutOfRangeError("Native save generation overflow");
508 } else {
509 ++generation;
510 }
511 receipt["generation"] = generation;
512 }
513 std::string text;
514 try {
515 text = receipt.dump(2) + "\n";
516 } catch (const Json::exception&) {
517 return absl::InvalidArgumentError("Native save paths must be valid UTF-8");
518 }
519 if (text.size() > kMaxReceiptBytes) {
520 return absl::InvalidArgumentError("Native save receipt exceeds size limit");
521 }
522 // Idempotent retries preserve the original receipt bytes, including spacing.
523 if (before.has_value() && Json::parse(*before) == receipt) {
524 text = *before;
525 }
526 std::optional<std::string> context_text;
527 if (request.context.has_value()) {
528 const auto& context = *request.context;
529 const Json committed = {
530 {"kind", kContextKind},
531 {"editing_source_path", context.editing_source_path},
532 {"state", "committed"},
533 {"project_path", context.project_path},
534 {"rom_path", PathText(rom->resolved)},
535 {"producer", producer},
536 {"native_commit",
537 {{"receipt_path", PathText(receipt_path)},
538 {"generation", generation},
539 {"operation_id", request.operation_id}}}};
540 if (replay &&
541 (!context_previous.has_value() || *context_previous != committed)) {
542 return absl::AbortedError(
543 "Native save retry cannot reactivate a retired context");
544 }
545 try {
546 context_text = committed.dump(2) + "\n";
547 } catch (const Json::exception&) {
548 return absl::InvalidArgumentError(
549 "Native save context must be valid UTF-8");
550 }
551 if (replay) {
552 context_text = context_before;
553 }
554 }
555 std::vector<SourceArtifactUpdate> updates;
556 updates.push_back({rom->resolved, rom_bytes, std::move(rom_bytes)});
557 updates.push_back({receipt_path, std::move(before), text});
558 if (request.context.has_value()) {
559 updates.push_back({request.context->path, context_before, *context_text});
560 }
562 *lock, std::move(updates), rom->artifact.expected_sha256, [&]() {
563 for (const auto& binding : bindings) {
564 RETURN_IF_ERROR(VerifyBinding(binding));
565 }
566 ASSIGN_OR_RETURN(const auto resolved_receipt,
567 ResolveRegularFile(request.receipt_path));
568 if (resolved_receipt != receipt_path) {
569 return absl::AbortedError("Native save receipt path changed");
570 }
571 ASSIGN_OR_RETURN(const auto readback,
572 ReadBoundedFile(receipt_path, kMaxReceiptBytes));
573 if (readback != text) {
574 return absl::DataLossError("Native save receipt readback changed");
575 }
576 if (request.context.has_value()) {
577 ASSIGN_OR_RETURN(const auto resolved_context,
578 ResolveRegularFile(request.context->path));
579 if (resolved_context != request.context->path) {
580 return absl::AbortedError("Native save context path changed");
581 }
582 ASSIGN_OR_RETURN(const auto context_readback,
583 ReadBoundedFile(resolved_context, kMaxReceiptBytes));
584 if (context_readback != *context_text) {
585 return absl::DataLossError("Native save context readback changed");
586 }
587 }
588 return absl::OkStatus();
589 }));
590 return NativeSaveReceipt{generation, std::move(text)};
591}
592
593} // namespace yaze::core
594
595#else
596
597namespace yaze::core {
598
599absl::StatusOr<NativeSaveContext> GetNativeSaveContext(const std::string&,
600 const std::string&) {
601 return absl::UnimplementedError("Native save contexts require JSON support");
602}
603
605 const std::string&) {
606 return absl::UnimplementedError("Native save contexts require JSON support");
607}
608
609absl::StatusOr<NativeSaveReceipt> PublishNativeSaveReceipt(
611 return absl::UnimplementedError("Native save receipts require JSON support");
612}
613
614} // namespace yaze::core
615
616#endif // defined(YAZE_WITH_JSON)
static Json parse(const std::string &)
Definition json.h:36
static Json array()
Definition json.h:35
static absl::StatusOr< std::filesystem::path > GetAppDataDirectory()
Get the user-specific application data directory for YAZE.
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
absl::Status PublishSourceArtifacts(const SourceArtifactPublicationLock &lock, std::vector< SourceArtifactUpdate > updates, std::string_view expected_primary_sha256, SourceArtifactReadbackValidator readback_validator)
absl::Status RetireNativeSaveContext(const NativeSaveContext &, const std::string &)
std::string ComputeSourceArtifactSha256(std::string_view content)
absl::StatusOr< std::string > ComputeSourceArtifactFileSha256(const fs::path &path)
absl::StatusOr< NativeSaveContext > GetNativeSaveContext(const std::string &, const std::string &)
absl::StatusOr< std::unique_ptr< SourceArtifactPublicationLock > > AcquireSourceArtifactPublicationLock(const std::vector< fs::path > &targets, const SourceArtifactPublisherLabels &labels, const std::vector< fs::path > &read_only_targets)
absl::StatusOr< NativeSaveReceipt > PublishNativeSaveReceipt(const NativeSaveReceiptRequest &)
Rom * rom()
Get the current ROM instance.
absl::StatusOr< std::string > ReadBoundedFile(const std::filesystem::path &path, size_t limit)
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22