yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
message_source_sync.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <array>
5#include <cctype>
6#include <cstdint>
7#include <exception>
8#include <filesystem>
9#include <fstream>
10#include <iterator>
11#include <limits>
12#include <map>
13#include <memory>
14#include <optional>
15#include <string>
16#include <string_view>
17#include <system_error>
18#include <utility>
19#include <vector>
20
21#include "absl/status/status.h"
22#include "absl/strings/ascii.h"
23#include "absl/strings/match.h"
24#include "absl/strings/str_cat.h"
25#include "absl/strings/str_format.h"
28#include "nlohmann/json.hpp"
29#include "rom/snes.h"
30#include "util/macro.h"
31
32namespace yaze::editor {
33namespace {
34
35namespace fs = std::filesystem;
36using Json = nlohmann::json;
37
39 std::string text;
40 std::vector<uint8_t> bytes;
41};
42
43using ExpandedSourceBank = std::map<int, ExpandedSourceMessage>;
44
46 .subject = "message source",
47 .published_file = "published message file",
48};
49
50absl::StatusOr<std::string> ReadTextFile(const fs::path& path,
51 absl::string_view label) {
52 std::ifstream input(path, std::ios::binary);
53 if (!input.is_open()) {
54 return absl::NotFoundError(
55 absl::StrFormat("Cannot open %s: %s", label, path.string()));
56 }
57 std::string content{std::istreambuf_iterator<char>(input),
58 std::istreambuf_iterator<char>()};
59 if (!input.good() && !input.eof()) {
60 return absl::DataLossError(
61 absl::StrFormat("Failed while reading %s: %s", label, path.string()));
62 }
63 return content;
64}
65
66bool ReadBoundedNonnegativeInteger(const Json& value, uint64_t maximum,
67 uint64_t* parsed) {
68 if (value.is_number_unsigned()) {
69 const uint64_t candidate = value.get<uint64_t>();
70 if (candidate > maximum) {
71 return false;
72 }
73 *parsed = candidate;
74 return true;
75 }
76 if (!value.is_number_integer()) {
77 return false;
78 }
79 const int64_t candidate = value.get<int64_t>();
80 if (candidate < 0 || static_cast<uint64_t>(candidate) > maximum) {
81 return false;
82 }
83 *parsed = static_cast<uint64_t>(candidate);
84 return true;
85}
86
87absl::StatusOr<Json> ParseBundleDocument(std::string_view content,
88 absl::string_view label) {
89 Json document;
90 try {
91 document = Json::parse(content);
92 } catch (const std::exception& error) {
93 return absl::InvalidArgumentError(
94 absl::StrFormat("%s is not valid JSON: %s", label, error.what()));
95 }
96 if (!document.is_object()) {
97 return absl::InvalidArgumentError(
98 absl::StrFormat("%s must be a JSON object", label));
99 }
100 if (!document.contains("format") || !document["format"].is_string() ||
101 document["format"].get<std::string>() != "yaze-message-bundle") {
102 return absl::InvalidArgumentError(
103 absl::StrFormat("%s format must be 'yaze-message-bundle'", label));
104 }
105 uint64_t version = 0;
106 if (!document.contains("version") ||
108 document["version"],
109 static_cast<uint64_t>(std::numeric_limits<int>::max()), &version) ||
110 version != static_cast<uint64_t>(kMessageBundleVersion)) {
111 return absl::InvalidArgumentError(absl::StrFormat(
112 "%s version must be integer %d", label, kMessageBundleVersion));
113 }
114 if (!document.contains("messages") || !document["messages"].is_array()) {
115 return absl::InvalidArgumentError(
116 absl::StrFormat("%s must contain a messages array", label));
117 }
118 if (document.contains("counts")) {
119 uint64_t expanded_count = 0;
120 uint64_t vanilla_count = 0;
121 if (!document["counts"].is_object() ||
122 !document["counts"].contains("expanded") ||
124 document["counts"]["expanded"],
125 static_cast<uint64_t>(std::numeric_limits<int>::max()),
126 &expanded_count) ||
127 !document["counts"].contains("vanilla") ||
129 document["counts"]["vanilla"],
130 static_cast<uint64_t>(std::numeric_limits<int>::max()),
131 &vanilla_count)) {
132 return absl::InvalidArgumentError(absl::StrFormat(
133 "%s counts must use bounded non-negative integers", label));
134 }
135 }
136 return document;
137}
138
139absl::StatusOr<std::string> SelectEntryText(const Json& entry, int id,
140 absl::string_view label) {
141 // `text` is the editable canonical field. Rich exports may retain stale
142 // raw/parsed diagnostics beside a deliberately changed text value.
143 for (absl::string_view key : {"text", "raw", "parsed"}) {
144 const std::string key_string(key);
145 if (!entry.contains(key_string)) {
146 continue;
147 }
148 if (!entry[key_string].is_string()) {
149 return absl::InvalidArgumentError(
150 absl::StrFormat("%s expanded message %d field '%s' must be a string",
151 label, id, key));
152 }
153 return entry[key_string].get<std::string>();
154 }
155 return absl::InvalidArgumentError(absl::StrFormat(
156 "%s expanded message %d has no raw, text, or parsed string", label, id));
157}
158
159std::string NormalizeLegacyDictionaryTokens(std::string text) {
160 size_t search_from = 0;
161 while (true) {
162 const size_t token_start = text.find("[D:", search_from);
163 if (token_start == std::string::npos) {
164 break;
165 }
166 size_t value_start = token_start + 3;
167 if (value_start < text.size() && text[value_start] == '$') {
168 ++value_start;
169 }
170 const size_t token_end = text.find(']', value_start);
171 if (token_end == std::string::npos) {
172 break;
173 }
174 const size_t digit_count = token_end - value_start;
175 const bool valid_digits =
176 digit_count >= 1 && digit_count <= 2 &&
177 std::all_of(text.begin() + static_cast<std::ptrdiff_t>(value_start),
178 text.begin() + static_cast<std::ptrdiff_t>(token_end),
179 [](unsigned char c) { return std::isxdigit(c) != 0; });
180 if (!valid_digits) {
181 search_from = token_end + 1;
182 continue;
183 }
184 const int dictionary_id =
185 std::stoi(text.substr(value_start, digit_count), nullptr, 16);
186 const std::string normalized = absl::StrFormat("[D:%02X]", dictionary_id);
187 text.replace(token_start, token_end - token_start + 1, normalized);
188 search_from = token_start + normalized.size();
189 }
190 return text;
191}
192
193absl::StatusOr<ExpandedSourceBank> ParseExpandedBank(const Json& document,
194 int expected_count,
195 bool require_complete,
196 absl::string_view label) {
198 for (const Json& entry : document["messages"]) {
199 if (!entry.is_object()) {
200 return absl::InvalidArgumentError(
201 absl::StrFormat("%s message entry must be an object", label));
202 }
203 uint64_t id_value = 0;
204 if (!entry.contains("id") ||
206 entry["id"], static_cast<uint64_t>(expected_count - 1),
207 &id_value)) {
208 return absl::InvalidArgumentError(
209 absl::StrFormat("%s message entry ID must be an integer in [0, %d)",
210 label, expected_count));
211 }
212 const int id = static_cast<int>(id_value);
213 if (!entry.contains("bank") || !entry["bank"].is_string() ||
214 entry["bank"].get<std::string>() != "expanded") {
215 return absl::InvalidArgumentError(absl::StrFormat(
216 "%s message %d must declare bank 'expanded'", label, id));
217 }
218 if (bank.contains(id)) {
219 return absl::InvalidArgumentError(absl::StrFormat(
220 "%s contains duplicate expanded message ID %d", label, id));
221 }
222
223 std::string text;
224 ASSIGN_OR_RETURN(text, SelectEntryText(entry, id, label));
225 text = NormalizeLegacyDictionaryTokens(std::move(text));
226 if (absl::StrContains(text, "[BANK]")) {
227 return absl::InvalidArgumentError(absl::StrFormat(
228 "%s expanded message %d contains [BANK], which is only valid in "
229 "the vanilla message stream",
230 label, id));
231 }
232 MessageParseResult parsed;
233 try {
235 } catch (const std::exception& error) {
236 return absl::InvalidArgumentError(
237 absl::StrFormat("%s expanded message %d cannot be parsed safely: %s",
238 label, id, error.what()));
239 } catch (...) {
240 return absl::InvalidArgumentError(absl::StrFormat(
241 "%s expanded message %d cannot be parsed safely", label, id));
242 }
243 if (!parsed.ok()) {
244 return absl::InvalidArgumentError(
245 absl::StrFormat("%s expanded message %d is invalid: %s", label, id,
246 parsed.errors.front()));
247 }
248 if (!parsed.warnings.empty()) {
249 return absl::InvalidArgumentError(
250 absl::StrFormat("%s expanded message %d is not source-stable: %s",
251 label, id, parsed.warnings.front()));
252 }
253 bank.emplace(id, ExpandedSourceMessage{
254 .text = std::move(text),
255 .bytes = std::move(parsed.bytes),
256 });
257 }
258
259 if (bank.empty()) {
260 return absl::InvalidArgumentError(
261 absl::StrFormat("%s has no expanded messages", label));
262 }
263 if (require_complete) {
264 if (bank.size() != static_cast<size_t>(expected_count)) {
265 return absl::FailedPreconditionError(absl::StrFormat(
266 "%s must contain the complete expanded bank: expected %d entries, "
267 "found %zu",
268 label, expected_count, bank.size()));
269 }
270 for (int id = 0; id < expected_count; ++id) {
271 if (!bank.contains(id)) {
272 return absl::FailedPreconditionError(absl::StrFormat(
273 "%s is missing bank-local expanded message ID %d", label, id));
274 }
275 }
276 uint64_t expanded_count = 0;
277 uint64_t vanilla_count = 0;
278 if (!document.contains("counts") || !document["counts"].is_object() ||
279 !document["counts"].contains("expanded") ||
281 document["counts"]["expanded"],
282 static_cast<uint64_t>(std::numeric_limits<int>::max()),
283 &expanded_count) ||
284 expanded_count != static_cast<uint64_t>(expected_count) ||
285 !document["counts"].contains("vanilla") ||
287 document["counts"]["vanilla"],
288 static_cast<uint64_t>(std::numeric_limits<int>::max()),
289 &vanilla_count) ||
290 vanilla_count != 0) {
291 return absl::FailedPreconditionError(
292 absl::StrFormat("%s counts must declare vanilla=0 and expanded=%d",
293 label, expected_count));
294 }
295 }
296 return bank;
297}
298
300 Json document;
301 document["format"] = "yaze-message-bundle";
302 document["version"] = kMessageBundleVersion;
303 document["counts"] = {{"vanilla", 0}, {"expanded", bank.size()}};
304 document["messages"] = Json::array();
305 for (const auto& [id, message] : bank) {
306 document["messages"].push_back(
307 {{"id", id}, {"bank", "expanded"}, {"text", message.text}});
308 }
309 return document.dump(2) + "\n";
310}
311
312std::string RenderAsmBody(const ExpandedSourceBank& bank,
313 int first_expanded_id) {
314 std::string output;
315 for (const auto& [local_id, message] : bank) {
316 absl::StrAppend(&output, absl::StrFormat("Message_%03X:\n",
317 first_expanded_id + local_id));
318 std::vector<uint8_t> terminated = message.bytes;
319 terminated.push_back(kMessageTerminator);
320 for (size_t offset = 0; offset < terminated.size(); offset += 16) {
321 absl::StrAppend(&output, " db ");
322 const size_t line_end = std::min(terminated.size(), offset + 16);
323 for (size_t index = offset; index < line_end; ++index) {
324 if (index != offset) {
325 absl::StrAppend(&output, ", ");
326 }
327 absl::StrAppend(&output, absl::StrFormat("$%02X", terminated[index]));
328 }
329 absl::StrAppend(&output, "\n");
330 }
331 absl::StrAppend(&output, "\n");
332 }
333 absl::StrAppend(&output, "db $FF\n");
334 return output;
335}
336
337std::string RenderAsmInclude(const ExpandedSourceBank& bank,
338 int first_expanded_id,
339 std::string_view source_sha256) {
340 const std::string body = RenderAsmBody(bank, first_expanded_id);
341 return absl::StrFormat(
342 "; Source bundle SHA-256: %s\n"
343 "; Generated ASM body SHA-256: %s\n"
344 "; Generated by yaze message-source-sync. Do not edit.\n\n",
345 std::string(source_sha256),
347 body;
348}
349
350bool IsCanonicalMappedLoRomAddress(uint32_t address) {
351 const uint8_t bank = static_cast<uint8_t>((address >> 16) & 0xFFu);
352 return bank != 0x7E && bank != 0x7F && (address & 0xFFFFu) >= 0x8000u &&
353 PcToSnes(SnesToPc(address)) == address;
354}
355
356absl::StatusOr<size_t> ValidateLayoutAndGetCapacity(
357 const core::MessageLayout& layout) {
358 if (layout.expanded_count <= 0 ||
359 layout.last_expanded_id < layout.first_expanded_id) {
360 return absl::FailedPreconditionError(
361 "Hack manifest must define a non-empty expanded message range");
362 }
363 const int range_count =
364 static_cast<int>(layout.last_expanded_id - layout.first_expanded_id) + 1;
365 if (layout.expanded_count != range_count) {
366 return absl::FailedPreconditionError(absl::StrFormat(
367 "Hack manifest expanded range is not contiguous: first=0x%03X, "
368 "last=0x%03X implies %d messages, but count=%d",
369 layout.first_expanded_id, layout.last_expanded_id, range_count,
370 layout.expanded_count));
371 }
374 return absl::FailedPreconditionError(
375 "Hack manifest expanded data range must use canonical mapped LoROM "
376 "addresses");
377 }
378 const uint32_t start_pc = SnesToPc(layout.data_start);
379 const uint32_t end_pc = SnesToPc(layout.data_end);
380 if (end_pc < start_pc) {
381 return absl::FailedPreconditionError(
382 "Hack manifest expanded data range ends before it starts");
383 }
384 return static_cast<size_t>(end_pc - start_pc) + 1;
385}
386
387bool IsWithinRoot(const fs::path& root, const fs::path& candidate) {
388 const fs::path relative = candidate.lexically_relative(root);
389 if (relative.empty() || relative.is_absolute()) {
390 return false;
391 }
392 const auto first = relative.begin();
393 return first != relative.end() && *first != "..";
394}
395
396absl::StatusOr<fs::path> ResolveProjectTarget(
397 const fs::path& project_root, const std::string& configured_path,
398 bool must_exist, absl::string_view label) {
399 const fs::path relative(configured_path);
400 if (relative.empty() || relative.is_absolute() || relative.has_root_name()) {
401 return absl::InvalidArgumentError(
402 absl::StrFormat("%s must be a non-empty project-relative path", label));
403 }
404
405 const fs::path joined = (project_root / relative).lexically_normal();
406 std::error_code status_ec;
407 const auto link_status = fs::symlink_status(joined, status_ec);
408 if (status_ec != std::errc::no_such_file_or_directory && status_ec) {
409 return absl::FailedPreconditionError(
410 absl::StrFormat("Cannot inspect %s path %s: %s", label, joined.string(),
411 status_ec.message()));
412 }
413 if (!status_ec && fs::is_symlink(link_status)) {
414 return absl::FailedPreconditionError(absl::StrFormat(
415 "%s may not be a symbolic link: %s", label, joined.string()));
416 }
417
418 std::error_code canonical_ec;
419 fs::path resolved = must_exist ? fs::canonical(joined, canonical_ec)
420 : fs::weakly_canonical(joined, canonical_ec);
421 if (canonical_ec) {
422 return absl::FailedPreconditionError(
423 absl::StrFormat("Cannot resolve %s path %s: %s", label, joined.string(),
424 canonical_ec.message()));
425 }
426 resolved = resolved.lexically_normal();
427 if (!IsWithinRoot(project_root, resolved)) {
428 return absl::PermissionDeniedError(
429 absl::StrFormat("%s escapes project root %s: %s", label,
430 project_root.string(), resolved.string()));
431 }
432
433 std::error_code parent_ec;
434 const auto parent_status = fs::status(resolved.parent_path(), parent_ec);
435 if (parent_ec || !fs::is_directory(parent_status)) {
436 return absl::FailedPreconditionError(
437 absl::StrFormat("%s parent directory must already exist: %s", label,
438 resolved.parent_path().string()));
439 }
440 if (must_exist) {
441 std::error_code file_ec;
442 if (!fs::is_regular_file(resolved, file_ec) || file_ec) {
443 return absl::FailedPreconditionError(absl::StrFormat(
444 "%s must be an existing regular file: %s", label, resolved.string()));
445 }
446 } else if (!status_ec && fs::exists(link_status) &&
447 !fs::is_regular_file(link_status)) {
448 return absl::FailedPreconditionError(
449 absl::StrFormat("%s must be a regular file or a new path: %s", label,
450 resolved.string()));
451 }
452 return resolved;
453}
454
455absl::StatusOr<fs::path> CanonicalProjectRoot(
456 const project::YazeProject& project) {
457 if (project.filepath.empty()) {
458 return absl::FailedPreconditionError(
459 "Project has no descriptor path; open the project before source sync");
460 }
461 std::error_code ec;
462 fs::path descriptor = fs::absolute(project.filepath, ec);
463 if (ec) {
464 return absl::FailedPreconditionError(
465 absl::StrFormat("Cannot resolve project descriptor %s: %s",
466 project.filepath, ec.message()));
467 }
468 fs::path root = fs::canonical(descriptor.parent_path(), ec);
469 if (ec) {
470 return absl::FailedPreconditionError(
471 absl::StrFormat("Cannot resolve project root for %s: %s",
472 project.filepath, ec.message()));
473 }
474 return root.lexically_normal();
475}
476
477std::string LowercasePath(const fs::path& path) {
478 return absl::AsciiStrToLower(path.generic_string());
479}
480
481absl::Status ValidateDistinctTargets(const fs::path& bundle_path,
482 const fs::path& include_path) {
483 if (bundle_path == include_path ||
484 LowercasePath(bundle_path) == LowercasePath(include_path)) {
485 return absl::InvalidArgumentError(
486 "Canonical bundle and generated ASM include paths must be distinct");
487 }
488 std::error_code exists_ec;
489 const bool include_exists = fs::exists(include_path, exists_ec);
490 if (exists_ec) {
491 return absl::FailedPreconditionError(absl::StrFormat(
492 "Cannot inspect generated ASM include path: %s", exists_ec.message()));
493 }
494 if (include_exists) {
495 std::error_code equivalent_ec;
496 if (fs::equivalent(bundle_path, include_path, equivalent_ec)) {
497 return absl::InvalidArgumentError(
498 "Canonical bundle and generated ASM include paths alias each other");
499 }
500 if (equivalent_ec) {
501 return absl::FailedPreconditionError(
502 absl::StrFormat("Cannot compare source publication paths: %s",
503 equivalent_ec.message()));
504 }
505 }
506 return absl::OkStatus();
507}
508
509absl::StatusOr<std::string> NormalizeExpectedSha256(std::string hash) {
510 if (hash.size() != 64 ||
511 !std::all_of(hash.begin(), hash.end(),
512 [](unsigned char c) { return std::isxdigit(c) != 0; })) {
513 return absl::InvalidArgumentError(
514 "--expected-source-sha256 must be exactly 64 hexadecimal characters");
515 }
516 return absl::AsciiStrToLower(std::move(hash));
517}
518
519} // namespace
520
521std::string ComputeMessageSourceSha256(std::string_view content) {
522 return core::ComputeSourceArtifactSha256(content);
523}
524
525absl::StatusOr<MessageSourceSyncResult> SyncMessageSource(
526 const project::YazeProject& project, const fs::path& incoming_bundle_path,
527 const MessageSourceSyncOptions& options) {
528 if (!project.hack_manifest.loaded()) {
529 return absl::FailedPreconditionError(
530 "Project must load a hack manifest before message source sync");
531 }
532#if defined(__EMSCRIPTEN__)
533 if (options.write) {
534 return absl::FailedPreconditionError(
535 "message-source-sync --write is unavailable in browser builds because "
536 "durable atomic filesystem publication cannot be guaranteed");
537 }
538#endif
539 const core::MessageLayout& layout = project.hack_manifest.message_layout();
540 if (!layout.source.has_value()) {
541 return absl::FailedPreconditionError(
542 "Hack manifest does not define messages.source");
543 }
544
545 size_t capacity = 0;
546 ASSIGN_OR_RETURN(capacity, ValidateLayoutAndGetCapacity(layout));
547 fs::path project_root;
548 ASSIGN_OR_RETURN(project_root, CanonicalProjectRoot(project));
549
550 fs::path canonical_bundle_path;
552 canonical_bundle_path,
553 ResolveProjectTarget(project_root, layout.source->canonical_bundle_path,
554 /*must_exist=*/true, "Canonical message bundle"));
555 fs::path asm_include_path;
556 ASSIGN_OR_RETURN(asm_include_path,
557 ResolveProjectTarget(
558 project_root, layout.source->generated_asm_include_path,
559 /*must_exist=*/false, "Generated message ASM include"));
561 ValidateDistinctTargets(canonical_bundle_path, asm_include_path));
562 const std::vector<fs::path> publication_targets = {canonical_bundle_path,
563 asm_include_path};
565 publication_targets, kMessageSourcePublisherLabels));
566
567 std::error_code incoming_ec;
568 const fs::path resolved_incoming =
569 fs::canonical(incoming_bundle_path, incoming_ec);
570 if (incoming_ec || !fs::is_regular_file(resolved_incoming, incoming_ec) ||
571 incoming_ec) {
572 return absl::NotFoundError(absl::StrFormat(
573 "Incoming message bundle must be an existing regular file: %s",
574 incoming_bundle_path.string()));
575 }
576
577 std::string expected_sha;
578 std::unique_ptr<core::SourceArtifactPublicationLock> write_locks;
579 if (options.write) {
580 if (options.expected_source_sha256.empty()) {
581 return absl::InvalidArgumentError(
582 "--write requires --expected-source-sha256");
583 }
584 ASSIGN_OR_RETURN(expected_sha,
585 NormalizeExpectedSha256(options.expected_source_sha256));
586 ASSIGN_OR_RETURN(write_locks,
588 publication_targets, kMessageSourcePublisherLabels));
589 }
590
591 std::string canonical_before;
592 ASSIGN_OR_RETURN(canonical_before, ReadTextFile(canonical_bundle_path,
593 "canonical message bundle"));
594 const std::string source_sha_before =
595 core::ComputeSourceArtifactSha256(canonical_before);
596 Json canonical_document;
598 canonical_document,
599 ParseBundleDocument(canonical_before, "Canonical message bundle"));
600 ExpandedSourceBank merged_bank;
602 merged_bank,
603 ParseExpandedBank(canonical_document, layout.expanded_count,
604 /*require_complete=*/true, "Canonical message bundle"));
605 const std::string expected_current_asm = RenderAsmInclude(
606 merged_bank, layout.first_expanded_id, source_sha_before);
607
608 std::string incoming_content;
609 ASSIGN_OR_RETURN(incoming_content,
610 ReadTextFile(resolved_incoming, "incoming message bundle"));
611 Json incoming_document;
613 incoming_document,
614 ParseBundleDocument(incoming_content, "Incoming message bundle"));
615 ExpandedSourceBank incoming_bank;
617 incoming_bank,
618 ParseExpandedBank(incoming_document, layout.expanded_count,
619 /*require_complete=*/false, "Incoming message bundle"));
620 for (auto& [id, message] : incoming_bank) {
621 merged_bank[id] = std::move(message);
622 }
623
624 size_t encoded_size = 1; // Final $FF.
625 for (const auto& entry : merged_bank) {
626 encoded_size += entry.second.bytes.size() + 1; // Per-message $7F.
627 }
628 if (encoded_size > capacity) {
629 return absl::ResourceExhaustedError(absl::StrFormat(
630 "Expanded message source needs %zu bytes including terminators, but "
631 "manifest capacity is %zu bytes [0x%06X, 0x%06X]",
632 encoded_size, capacity, layout.data_start, layout.data_end));
633 }
634
635 const std::string canonical_after = SerializeCanonicalBundle(merged_bank);
636 const std::string proposed_source_sha =
637 core::ComputeSourceArtifactSha256(canonical_after);
638 const std::string asm_after = RenderAsmInclude(
639 merged_bank, layout.first_expanded_id, proposed_source_sha);
640
641 std::optional<std::string> asm_before;
642 std::error_code include_exists_ec;
643 if (fs::exists(asm_include_path, include_exists_ec)) {
644 std::string existing_include;
646 existing_include,
647 ReadTextFile(asm_include_path, "generated message ASM include"));
648 asm_before = std::move(existing_include);
649 } else if (include_exists_ec) {
650 return absl::FailedPreconditionError(
651 absl::StrFormat("Cannot inspect generated message ASM include: %s",
652 include_exists_ec.message()));
653 }
654 if (asm_before.has_value() && *asm_before != expected_current_asm) {
655 return absl::FailedPreconditionError(absl::StrFormat(
656 "Generated message ASM include drifted from the current canonical "
657 "bundle: %s",
658 asm_include_path.string()));
659 }
660
662 .canonical_bundle_path = canonical_bundle_path,
663 .generated_asm_include_path = asm_include_path,
664 .first_expanded_id = layout.first_expanded_id,
665 .last_expanded_id = layout.last_expanded_id,
666 .expanded_count = layout.expanded_count,
667 .incoming_updates = static_cast<int>(incoming_bank.size()),
668 .encoded_size = encoded_size,
669 .capacity = capacity,
670 .source_sha256_before = source_sha_before,
671 .proposed_source_sha256 = proposed_source_sha,
672 .changed = canonical_before != canonical_after ||
673 !asm_before.has_value() || *asm_before != asm_after,
674 };
675 if (!options.write) {
676 return result;
677 }
678 if (expected_sha != source_sha_before) {
679 return absl::AbortedError(absl::StrFormat(
680 "Canonical message source SHA-256 CAS failed: expected %s, got %s",
681 expected_sha, source_sha_before));
682 }
683 if (!result.changed) {
684 return result;
685 }
686
687 std::vector<core::SourceArtifactUpdate> updates;
688 updates.push_back(core::SourceArtifactUpdate{
689 .target = canonical_bundle_path,
690 .before = canonical_before,
691 .after = canonical_after,
692 });
693 updates.push_back(core::SourceArtifactUpdate{
694 .target = asm_include_path,
695 .before = asm_before,
696 .after = asm_after,
697 });
699 *write_locks, std::move(updates), expected_sha, [&]() -> absl::Status {
700 // Reopen the canonical bundle through the strict source parser, not
701 // just as bytes, before reporting a successful two-file publication.
702 std::string reopened_canonical;
703 ASSIGN_OR_RETURN(reopened_canonical,
704 ReadTextFile(canonical_bundle_path,
705 "published canonical message bundle"));
706 Json reopened_document;
708 reopened_document,
709 ParseBundleDocument(reopened_canonical,
710 "Published canonical message bundle"));
711 ExpandedSourceBank reopened_bank;
713 reopened_bank,
714 ParseExpandedBank(reopened_document, layout.expanded_count,
715 /*require_complete=*/true,
716 "Published canonical message bundle"));
717 if (core::ComputeSourceArtifactSha256(reopened_canonical) !=
718 proposed_source_sha) {
719 return absl::DataLossError(
720 "Published canonical message bundle SHA-256 readback failed");
721 }
722 return absl::OkStatus();
723 }));
724 result.wrote = true;
725 return result;
726}
727
728} // namespace yaze::editor
static Json parse(const std::string &)
Definition json.h:36
static Json array()
Definition json.h:35
const MessageLayout & message_layout() const
bool loaded() const
Check if the manifest has been loaded.
#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)
std::string ComputeSourceArtifactSha256(std::string_view content)
absl::Status ValidateSourceArtifactPublicationTargets(const std::vector< fs::path > &targets, const SourceArtifactPublisherLabels &labels)
absl::StatusOr< std::unique_ptr< SourceArtifactPublicationLock > > AcquireSourceArtifactPublicationLock(const std::vector< fs::path > &targets, const SourceArtifactPublisherLabels &labels)
std::map< int, ExpandedSourceMessage > ExpandedSourceBank
absl::Status ValidateDistinctTargets(const fs::path &bundle_path, const fs::path &include_path)
absl::StatusOr< fs::path > CanonicalProjectRoot(const project::YazeProject &project)
absl::StatusOr< Json > ParseBundleDocument(std::string_view content, absl::string_view label)
bool IsWithinRoot(const fs::path &root, const fs::path &candidate)
bool ReadBoundedNonnegativeInteger(const Json &value, uint64_t maximum, uint64_t *parsed)
absl::StatusOr< std::string > NormalizeExpectedSha256(std::string hash)
absl::StatusOr< std::string > ReadTextFile(const fs::path &path, absl::string_view label)
const core::SourceArtifactPublisherLabels kMessageSourcePublisherLabels
absl::StatusOr< ExpandedSourceBank > ParseExpandedBank(const Json &document, int expected_count, bool require_complete, absl::string_view label)
absl::StatusOr< std::string > SelectEntryText(const Json &entry, int id, absl::string_view label)
absl::StatusOr< size_t > ValidateLayoutAndGetCapacity(const core::MessageLayout &layout)
std::string SerializeCanonicalBundle(const ExpandedSourceBank &bank)
std::string RenderAsmBody(const ExpandedSourceBank &bank, int first_expanded_id)
std::string RenderAsmInclude(const ExpandedSourceBank &bank, int first_expanded_id, std::string_view source_sha256)
absl::StatusOr< fs::path > ResolveProjectTarget(const fs::path &project_root, const std::string &configured_path, bool must_exist, absl::string_view label)
Editors are the view controllers for the application.
constexpr int kMessageBundleVersion
std::string ComputeMessageSourceSha256(std::string_view content)
constexpr uint8_t kMessageTerminator
MessageParseResult ParseMessageToDataWithDiagnostics(std::string_view str)
absl::StatusOr< MessageSourceSyncResult > SyncMessageSource(const project::YazeProject &project, const fs::path &incoming_bundle_path, const MessageSourceSyncOptions &options)
uint32_t PcToSnes(uint32_t addr)
Definition snes.h:17
uint32_t SnesToPc(uint32_t addr) noexcept
Definition snes.h:8
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
Message range information for the expanded message system.
std::optional< Source > source
std::vector< uint8_t > bytes
std::vector< std::string > errors
std::vector< std::string > warnings
Modern project structure with comprehensive settings consolidation.
Definition project.h:172
core::HackManifest hack_manifest
Definition project.h:212