yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
rom.cc
Go to the documentation of this file.
1#include "rom.h"
2
3#include <algorithm>
4#include <atomic>
5#include <cerrno>
6#include <chrono>
7#include <cstddef>
8#include <cstdint>
9#include <cstring>
10#include <ctime>
11#include <filesystem>
12#include <fstream>
13#include <iostream>
14#include <new>
15#include <string>
16#include <system_error>
17#include <vector>
18
19#include "absl/cleanup/cleanup.h"
20#include "absl/status/status.h"
21#include "absl/status/statusor.h"
22#include "absl/strings/str_cat.h"
23#include "absl/strings/str_format.h"
24#include "absl/strings/string_view.h"
27#include "rom/write_fence.h"
28#include "util/hex.h"
29#include "util/log.h"
30#include "util/macro.h"
31
32#ifdef __EMSCRIPTEN__
33#include <emscripten.h>
35#endif
36
37#if defined(_WIN32)
38#include <windows.h>
39#else
40#include <fcntl.h>
41#include <unistd.h>
42#endif
43
44namespace yaze {
45
46namespace {
47
48// ============================================================================
49// Copier Header Detection and Removal
50// ============================================================================
51
53constexpr size_t kCopierHeaderAlignment = 0x8000;
54constexpr size_t kLoRomInternalHeader = 0x7FC0;
55constexpr size_t kHiRomInternalHeader = 0xFFC0;
56
57// A plausible SNES internal header: map mode byte of the form 0x2X and a
58// checksum/complement pair that XORs to 0xFFFF.
59bool HasInternalHeaderAt(const uint8_t* data, size_t size, size_t offset) {
60 if (offset + 0x20 > size) {
61 return false;
62 }
63 const uint8_t map_mode = data[offset + 0x15];
64 const uint16_t complement = data[offset + 0x1C] | (data[offset + 0x1D] << 8);
65 const uint16_t checksum = data[offset + 0x1E] | (data[offset + 0x1F] << 8);
66 return (map_mode & 0xE0) == 0x20 && (complement ^ checksum) == 0xFFFF;
67}
68
69bool HasInternalHeader(const uint8_t* data, size_t size, size_t base) {
70 return HasInternalHeaderAt(data, size, base + kLoRomInternalHeader) ||
71 HasInternalHeaderAt(data, size, base + kHiRomInternalHeader);
72}
73
74void MaybeStripCopierHeader(std::vector<uint8_t>& rom_data,
75 std::vector<uint8_t>& header,
76 CopierHeaderReason& reason) {
77 header.clear();
78 reason = DetectCopierHeader(rom_data.data(), rom_data.size());
79 if (reason == CopierHeaderReason::kNone) {
80 return;
81 }
82 header.assign(rom_data.begin(), rom_data.begin() + kCopierHeaderSize);
83 rom_data.erase(rom_data.begin(), rom_data.begin() + kCopierHeaderSize);
84 LOG_INFO("Rom", "Stripped copier header (%s); ROM size 0x%zX",
85 CopierHeaderReasonName(reason), rom_data.size());
86}
87
88std::string MakeSafeTimestamp(std::time_t now_c) {
89 std::string timestamp = std::ctime(&now_c);
90 timestamp.erase(std::remove(timestamp.begin(), timestamp.end(), '\n'),
91 timestamp.end());
92 std::replace(timestamp.begin(), timestamp.end(), ' ', '_');
93
94 // Keep backup/save-new filenames valid across platforms (especially
95 // Windows, where ':' and several other characters are not allowed).
96 for (char& ch : timestamp) {
97 switch (ch) {
98 case '<':
99 case '>':
100 case ':':
101 case '"':
102 case '/':
103 case '\\':
104 case '|':
105 case '?':
106 case '*':
107 ch = '-';
108 break;
109 default:
110 break;
111 }
112 }
113 return timestamp;
114}
115
116absl::StatusOr<std::filesystem::path> GetAvailableBackupPath(
117 const std::filesystem::path& requested_path) {
118 std::filesystem::path candidate = requested_path;
119 for (int suffix = 1; suffix <= 1000; ++suffix) {
120 std::error_code exists_ec;
121 const bool exists = std::filesystem::exists(candidate, exists_ec);
122 if (exists_ec) {
123 return absl::InternalError(absl::StrCat(
124 "Could not inspect required ROM backup path: ", candidate.string(),
125 ": ", exists_ec.message()));
126 }
127 if (!exists) {
128 return candidate;
129 }
130 candidate = requested_path.string() + "_" + std::to_string(suffix);
131 }
132
133 return absl::ResourceExhaustedError(
134 "Could not allocate a unique required ROM backup path");
135}
136
137#if !defined(__EMSCRIPTEN__)
138void BestEffortFsyncFile(const std::filesystem::path& path);
139void BestEffortFsyncParentDir(const std::filesystem::path& file_path);
140#endif
141
143 const std::filesystem::path& source_path,
144 const std::filesystem::path& requested_backup_path) {
145 auto backup_path_or = GetAvailableBackupPath(requested_backup_path);
146 if (!backup_path_or.ok()) {
147 return backup_path_or.status();
148 }
149
150 const std::filesystem::path backup_path = *backup_path_or;
151 std::filesystem::path temp_path = backup_path;
152 temp_path += ".tmp";
153
154 std::error_code copy_ec;
155 const bool copied = std::filesystem::copy_file(
156 source_path, temp_path, std::filesystem::copy_options::none, copy_ec);
157 if (!copied || copy_ec) {
158 std::error_code cleanup_ec;
159 std::filesystem::remove(temp_path, cleanup_ec);
160 return absl::FailedPreconditionError(absl::StrCat(
161 "Could not create required ROM backup: ", source_path.string(), " -> ",
162 backup_path.string(), ": ",
163 copy_ec ? copy_ec.message() : "copy did not complete"));
164 }
165
166#if !defined(__EMSCRIPTEN__)
167 BestEffortFsyncFile(temp_path);
168#endif
169
170 std::error_code rename_ec;
171 std::filesystem::rename(temp_path, backup_path, rename_ec);
172 if (rename_ec) {
173 std::error_code cleanup_ec;
174 std::filesystem::remove(temp_path, cleanup_ec);
175 return absl::FailedPreconditionError(absl::StrCat(
176 "Could not finalize required ROM backup: ", backup_path.string(), ": ",
177 rename_ec.message()));
178 }
179
180#if !defined(__EMSCRIPTEN__)
181 BestEffortFsyncParentDir(backup_path);
182#endif
183
184 return absl::OkStatus();
185}
186
187// Match the exclusive staging contract used by CLI artifact publication.
188// Keep the name independent of the destination basename so long ROM filenames
189// still fit the filesystem's component limit. Exclusive creation, rather than
190// the nonce alone, establishes ownership; never reopen this path to write it.
191std::atomic<bool> g_fail_rom_staging_for_testing{false};
192
193absl::StatusOr<std::filesystem::path> WriteExclusiveRomTemp(
194 const std::filesystem::path& target_path,
195 const std::vector<uint8_t>& bytes) {
196 if (g_fail_rom_staging_for_testing.load(std::memory_order_relaxed)) {
197 return absl::InternalError(
198 "Could not create temp ROM file: staging failure injected for testing");
199 }
200 static std::atomic<uint64_t> sequence{0};
201 for (int attempt = 0; attempt < 100; ++attempt) {
202 const auto tick = static_cast<uint64_t>(
203 std::chrono::steady_clock::now().time_since_epoch().count());
204 const auto id = sequence.fetch_add(1, std::memory_order_relaxed);
205 const auto temp_path =
206 target_path.parent_path() /
207 absl::StrFormat(".yaze-rom-%016x-%016x.tmp", tick, id);
208#if defined(_WIN32)
209 HANDLE file =
210 CreateFileW(temp_path.wstring().c_str(), GENERIC_WRITE, 0, nullptr,
211 CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr);
212 if (file == INVALID_HANDLE_VALUE) {
213 const DWORD error = GetLastError();
214 if (error == ERROR_FILE_EXISTS || error == ERROR_ALREADY_EXISTS) {
215 continue;
216 }
217 return absl::InternalError(absl::StrCat(
218 "Could not create temp ROM file: ", temp_path.string(), ": ",
219 std::error_code(static_cast<int>(error), std::system_category())
220 .message()));
221 }
222#else
223 int file = open(temp_path.c_str(), O_WRONLY | O_CREAT | O_EXCL, 0666);
224 if (file < 0) {
225 const int error = errno;
226 if (error == EEXIST) {
227 continue;
228 }
229 return absl::InternalError(absl::StrCat(
230 "Could not create temp ROM file: ", temp_path.string(), ": ",
231 std::error_code(error, std::generic_category()).message()));
232 }
233#endif
234 // Only installed after successful exclusive creation. On failure we may
235 // remove our staging file, never a pre-existing destination or .tmp file.
236 absl::Cleanup cleanup = [&] {
237#if defined(_WIN32)
238 if (file != INVALID_HANDLE_VALUE) {
239 (void)CloseHandle(file);
240 }
241#else
242 if (file >= 0) {
243 (void)close(file);
244 }
245#endif
246 std::error_code ec;
247 std::filesystem::remove(temp_path, ec);
248 };
249
250 size_t offset = 0;
251 while (offset < bytes.size()) {
252 const size_t chunk = std::min<size_t>(bytes.size() - offset, 1024 * 1024);
253#if defined(_WIN32)
254 DWORD written = 0;
255 const bool ok = WriteFile(file, bytes.data() + offset,
256 static_cast<DWORD>(chunk), &written, nullptr);
257 const int error =
258 ok ? ERROR_WRITE_FAULT : static_cast<int>(GetLastError());
259 if (!ok || written == 0) {
260 return absl::InternalError(absl::StrCat(
261 "Error while writing ROM file: ", temp_path.string(), ": ",
262 std::error_code(error, std::system_category()).message()));
263 }
264#else
265 const ssize_t written = write(file, bytes.data() + offset, chunk);
266 if (written < 0 && errno == EINTR) {
267 continue;
268 }
269 if (written <= 0) {
270 const int error = written < 0 ? errno : EIO;
271 return absl::InternalError(absl::StrCat(
272 "Error while writing ROM file: ", temp_path.string(), ": ",
273 std::error_code(error, std::generic_category()).message()));
274 }
275#endif
276 offset += static_cast<size_t>(written);
277 }
278
279#if defined(_WIN32)
280 if (!FlushFileBuffers(file)) {
281 const int error = static_cast<int>(GetLastError());
282 return absl::InternalError(absl::StrCat(
283 "Could not flush temp ROM file: ", temp_path.string(), ": ",
284 std::error_code(error, std::system_category()).message()));
285 }
286 const bool closed = CloseHandle(file);
287 const int close_error = closed ? 0 : static_cast<int>(GetLastError());
288 file = INVALID_HANDLE_VALUE;
289 const auto close_category = &std::system_category();
290#else
291#if !defined(__EMSCRIPTEN__)
292 int flush_result;
293 do {
294 flush_result = fsync(file);
295 } while (flush_result != 0 && errno == EINTR);
296 if (flush_result != 0) {
297 const int error = errno;
298 return absl::InternalError(absl::StrCat(
299 "Could not flush temp ROM file: ", temp_path.string(), ": ",
300 std::error_code(error, std::generic_category()).message()));
301 }
302#endif
303 const int close_result = close(file);
304 const int close_error = close_result == 0 ? 0 : errno;
305 file = -1;
306 const auto close_category = &std::generic_category();
307#endif
308 if (close_error != 0) {
309 return absl::InternalError(absl::StrCat(
310 "Could not close temp ROM file: ", temp_path.string(), ": ",
311 std::error_code(close_error, *close_category).message()));
312 }
313 std::move(cleanup).Cancel();
314 return temp_path;
315 }
316 return absl::ResourceExhaustedError(
317 "Could not allocate a unique temp ROM file");
318}
319
320#ifdef __EMSCRIPTEN__
321inline void MaybeBroadcastChange(uint32_t offset,
322 const std::vector<uint8_t>& old_bytes,
323 const std::vector<uint8_t>& new_bytes) {
324 if (new_bytes.empty())
325 return;
326 auto& collab = app::platform::GetWasmCollaborationInstance();
327 if (!collab.IsConnected() || collab.IsApplyingRemoteChange()) {
328 return;
329 }
330 (void)collab.BroadcastChange(offset, old_bytes, new_bytes);
331}
332#endif
333
334#if !defined(__EMSCRIPTEN__)
335void BestEffortFsyncFile(const std::filesystem::path& path) {
336#if defined(_WIN32)
337 // FlushFileBuffers requires GENERIC_WRITE access.
338 HANDLE handle =
339 CreateFileW(path.wstring().c_str(), GENERIC_WRITE,
340 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
341 nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
342 if (handle == INVALID_HANDLE_VALUE) {
343 return;
344 }
345 (void)FlushFileBuffers(handle);
346 (void)CloseHandle(handle);
347#else
348 int fd = open(path.c_str(), O_RDONLY);
349 if (fd < 0) {
350 return;
351 }
352 (void)fsync(fd);
353 (void)close(fd);
354#endif
355}
356
357void BestEffortFsyncParentDir(const std::filesystem::path& file_path) {
358#if defined(_WIN32)
359 (void)file_path;
360 // Best-effort only; Windows directory fsync is not portable here.
361#else
362 std::filesystem::path dir_path = file_path.parent_path();
363 if (dir_path.empty()) {
364 dir_path = ".";
365 }
366 int fd = open(dir_path.c_str(), O_RDONLY);
367 if (fd < 0) {
368 return;
369 }
370 (void)fsync(fd);
371 (void)close(fd);
372#endif
373}
374#endif // !defined(__EMSCRIPTEN__)
375
376} // namespace
377
378CopierHeaderReason DetectCopierHeader(const uint8_t* data, size_t size) {
379 if (data == nullptr || size <= kCopierHeaderSize) {
381 }
382 const size_t remainder = size % kCopierHeaderAlignment;
383 if (remainder == 0) {
385 }
386 const bool header_in_place = HasInternalHeader(data, size, 0);
387 const bool header_after_skip =
388 HasInternalHeader(data, size, kCopierHeaderSize);
389 if (remainder == kCopierHeaderSize) {
390 // Standard layout. Still keep a headerless image with 0x200 trailing
391 // bytes, recognizable by an internal header valid only in place.
392 return header_in_place && !header_after_skip
395 }
396 // Nonstandard sizes (trailing data after the ROM) need positive evidence.
397 return header_after_skip && !header_in_place
400}
401
403 switch (reason) {
405 return "none";
407 return "size_alignment";
409 return "internal_header_offset";
410 }
411 return "none";
412}
413
415 g_fail_rom_staging_for_testing.store(fail, std::memory_order_relaxed);
416}
417
419 auto result = rom::InspectLyraLayout(rom_data_.data(), rom_data_.size());
421 result.presence == rom::LyraLayoutPresence::kAbsent) {
422 result.presence = rom::LyraLayoutPresence::kInvalid;
423 result.write_supported = false;
424 result.error = "Loaded Lyra descriptor was removed or damaged";
425 }
426 return result;
427}
428
429absl::Status Rom::CheckLayoutWritable() const {
430 const auto layout = InspectLyraLayout();
431 if (layout.presence == rom::LyraLayoutPresence::kAbsent) {
432 return absl::OkStatus();
433 }
434 return absl::FailedPreconditionError(
435 layout.presence == rom::LyraLayoutPresence::kValid
436 ? "Lyra overworld ROM layout revision 1 is inspection-only; save "
437 "refused"
438 : absl::StrCat("Invalid Lyra overworld ROM layout: ", layout.error,
439 "; save refused"));
440}
441
442Rom::Rom(const Rom& other)
443 : size_(other.size_),
444 title_(other.title_),
445 filename_(other.filename_),
446 short_name_(other.short_name_),
447 rom_data_(other.rom_data_),
448 copier_header_(other.copier_header_),
449 copier_header_reason_(other.copier_header_reason_),
450 lyra_attributed_at_load_(other.lyra_attributed_at_load_),
451 resource_label_manager_(other.resource_label_manager_),
452 dirty_(other.dirty_),
453 object_tile_revision_(other.object_tile_revision_) {
454 // Write fences are non-owning, instance-local transaction state. A new Rom
455 // must never inherit pointers owned by active scopes on `other`.
456}
457
458Rom& Rom::operator=(const Rom& other) {
459 if (this == &other) {
460 return *this;
461 }
462
464 size_ = other.size_;
465 title_ = other.title_;
466 filename_ = other.filename_;
467 short_name_ = other.short_name_;
468 rom_data_ = other.rom_data_;
473 dirty_ = other.dirty_;
474
475 // Write fences are non-owning, instance-local transaction state. Preserve
476 // this Rom's active scopes rather than importing pointers owned by `other`.
477
481 return *this;
482}
483
485 // Rom historically has copy-on-rvalue assignment semantics. Keep the source
486 // intact while ensuring in-place session replacement uses the same strictly
487 // monotonic destination generation as lvalue assignment.
488 return operator=(static_cast<const Rom&>(other));
489}
490
491absl::Status Rom::LoadFromFile(const std::string& filename,
492 const LoadOptions& options) {
493 if (filename.empty()) {
494 return absl::InvalidArgumentError(
495 "Could not load ROM: parameter `filename` is empty.");
496 }
497
498#ifdef __EMSCRIPTEN__
500 std::ifstream test_file(filename_, std::ios::binary);
501 if (!test_file.is_open()) {
502 return absl::NotFoundError(absl::StrCat(
503 "ROM file does not exist or cannot be opened: ", filename_));
504 }
505 test_file.seekg(0, std::ios::end);
506 size_ = test_file.tellg();
507 test_file.close();
508
509 if (size_ < 32768) {
510 return absl::InvalidArgumentError(absl::StrFormat(
511 "ROM file too small (%zu bytes), minimum is 32KB", size_));
512 }
513#else
514 if (!std::filesystem::exists(filename)) {
515 return absl::NotFoundError(
516 absl::StrCat("ROM file does not exist: ", filename));
517 }
518 filename_ = std::filesystem::absolute(filename).string();
519#endif
520 short_name_ = filename_.substr(filename_.find_last_of("/\\") + 1);
521
522 std::ifstream file(filename_, std::ios::binary);
523 if (!file.is_open()) {
524 return absl::NotFoundError(
525 absl::StrCat("Could not open ROM file: ", filename_));
526 }
527
528#ifndef __EMSCRIPTEN__
529 try {
530 size_ = std::filesystem::file_size(filename_);
531 if (size_ < 32768) {
532 return absl::InvalidArgumentError(absl::StrFormat(
533 "ROM file too small (%zu bytes), minimum is 32KB", size_));
534 }
535 } catch (...) {
536 file.seekg(0, std::ios::end);
537 size_ = file.tellg();
538 }
539#endif
540
541 // ALttP ROMs are <= 4MB (with an SMC header ~4MB+512); reject anything far
542 // larger so a mis-selected file cannot force a huge up-front allocation.
543 constexpr size_t kMaxRomSize = 16 * 1024 * 1024; // 16 MB
544 if (size_ > kMaxRomSize) {
545 return absl::InvalidArgumentError(absl::StrFormat(
546 "ROM file too large (%zu bytes), maximum is 16MB", size_));
547 }
548
549 try {
550 rom_data_.resize(size_);
551 file.seekg(0, std::ios::beg);
552 file.read(reinterpret_cast<char*>(rom_data_.data()), size_);
553 } catch (const std::bad_alloc& e) {
554 return absl::ResourceExhaustedError(absl::StrFormat(
555 "Failed to allocate memory for ROM (%zu bytes)", size_));
556 }
557
558 file.close();
559
560 copier_header_.clear();
562 if (options.strip_header) {
563 MaybeStripCopierHeader(rom_data_, copier_header_, copier_header_reason_);
564 }
565 size_ = rom_data_.size();
566
570
571 if (options.load_resource_labels) {
572 resource_label_manager_.LoadLabels(absl::StrFormat("%s.labels", filename));
573 }
574
575 // Parse SNES Header for Title
576 if (rom_data_.size() >= 0x8000) {
577 // Check LoROM (0x7FC0) vs HiROM (0xFFC0)
578 // Simple heuristic: Z3 is LoROM
579 size_t header_offset = 0x7FC0;
580 if (rom_data_.size() >= 0x10000) {
581 // Compute checksums to verify?
582 // For now default to LoROM
583 }
584
585 if (header_offset + 21 <= rom_data_.size()) {
586 char buffer[22] = {0};
587 for (int i = 0; i < 21; ++i) {
589 buffer[i] = (c >= 32 && c <= 126) ? c : ' ';
590 }
591 title_ = std::string(buffer);
592 // Trim trailing spaces safely
593 auto last_non_space = title_.find_last_not_of(' ');
594 if (last_non_space == std::string::npos) {
595 title_.clear();
596 } else {
597 title_.erase(last_non_space + 1);
598 }
599 }
600 }
601
603 return absl::OkStatus();
604}
605
606absl::Status Rom::LoadFromData(const std::vector<uint8_t>& data,
607 const LoadOptions& options) {
608 if (data.empty()) {
609 return absl::InvalidArgumentError(
610 "Could not load ROM: parameter `data` is empty.");
611 }
612 rom_data_ = data;
613
614 copier_header_.clear();
616 if (options.strip_header) {
617 MaybeStripCopierHeader(rom_data_, copier_header_, copier_header_reason_);
618 }
619 size_ = rom_data_.size();
620
624
625 // Parse SNES Header for Title
626 if (rom_data_.size() >= 0x8000) {
627 size_t header_offset = 0x7FC0;
628 if (header_offset + 21 <= rom_data_.size()) {
629 char buffer[22] = {0};
630 for (int i = 0; i < 21; ++i) {
632 buffer[i] = (c >= 32 && c <= 126) ? c : ' ';
633 }
634 title_ = std::string(buffer);
635 auto last_non_space = title_.find_last_not_of(' ');
636 if (last_non_space == std::string::npos) {
637 title_.clear();
638 } else {
639 title_.erase(last_non_space + 1);
640 }
641 }
642 }
643
645 return absl::OkStatus();
646}
647
648absl::Status Rom::SaveToFile(const SaveSettings& settings) {
650 if (rom_data_.empty()) {
651 return absl::InternalError("ROM data is empty.");
652 }
653
654 std::string filename = settings.filename;
655 if (filename.empty()) {
657 }
658
659 // Backup modes must reason about the actual destination, including the
660 // timestamped path selected by save_new.
661 if (settings.save_new) {
662 auto now = std::chrono::system_clock::now();
663 auto now_c = std::chrono::system_clock::to_time_t(now);
664 auto filename_no_ext = filename.substr(0, filename.find_last_of("."));
665 filename =
666 absl::StrCat(filename_no_ext, "_", MakeSafeTimestamp(now_c), ".sfc");
667 }
668
669 if (settings.require_backup) {
670 const std::filesystem::path target_path(filename);
671 std::error_code exists_ec;
672 const bool target_exists = std::filesystem::exists(target_path, exists_ec);
673 if (exists_ec) {
674 return absl::FailedPreconditionError(absl::StrCat(
675 "Could not inspect required ROM backup target: ", filename, ": ",
676 exists_ec.message()));
677 }
678
679 // A strict backup protects the file that is about to be replaced, not the
680 // ROM's original load path. A new target has no previous bytes to protect.
681 if (target_exists) {
682 auto now = std::chrono::system_clock::now();
683 auto now_c = std::chrono::system_clock::to_time_t(now);
684 std::string backup_filename =
685 absl::StrCat(filename, "_backup_", MakeSafeTimestamp(now_c));
686 RETURN_IF_ERROR(CreateRequiredBackup(target_path, backup_filename));
687 }
688 } else if (settings.backup) {
689 try {
690 const std::filesystem::path target_path(filename);
691 // Best-effort backups protect the file about to be replaced. A new
692 // destination has no previous bytes to preserve.
693 if (std::filesystem::exists(target_path)) {
694 auto now = std::chrono::system_clock::now();
695 auto now_c = std::chrono::system_clock::to_time_t(now);
696 std::string backup_filename =
697 absl::StrCat(filename, "_backup_", MakeSafeTimestamp(now_c));
698 std::filesystem::copy(
699 target_path, backup_filename,
700 std::filesystem::copy_options::overwrite_existing);
701 }
702 } catch (const std::filesystem::filesystem_error& e) {
703 LOG_WARN("Rom", "Could not create backup: %s", e.what());
704 }
705 }
706
707 // A copier header stripped at load goes back in front of the data, so the
708 // file keeps its layout (dropping it would shift every file offset).
709 std::vector<uint8_t> headered;
710 if (!copier_header_.empty()) {
711 headered.reserve(copier_header_.size() + rom_data_.size());
712 headered.insert(headered.end(), copier_header_.begin(),
713 copier_header_.end());
714 headered.insert(headered.end(), rom_data_.begin(), rom_data_.end());
715 }
716 const std::vector<uint8_t>& file_bytes =
718
719 // Write only through an exclusively created staging handle, then replace
720 // the destination. A pre-existing .tmp file/link must never be truncated.
721 const std::filesystem::path target_path(filename);
722 ASSIGN_OR_RETURN(const std::filesystem::path temp_path,
723 WriteExclusiveRomTemp(target_path, file_bytes));
724
725 std::error_code rename_ec;
726 std::filesystem::rename(temp_path, target_path, rename_ec);
727#if defined(_WIN32)
728 // Windows may reject std::filesystem::rename when the destination exists.
729 // Replace it without deleting the original first, so a failed replacement
730 // does not leave the target missing.
731 if (rename_ec) {
732 if (MoveFileExW(temp_path.wstring().c_str(), target_path.wstring().c_str(),
734 rename_ec.clear();
735 } else {
736 rename_ec = std::error_code(static_cast<int>(GetLastError()),
737 std::system_category());
738 }
739 }
740#endif
741 if (rename_ec) {
742 std::error_code rm_ec;
743 std::filesystem::remove(temp_path, rm_ec);
744 return absl::InternalError(absl::StrCat(
745 "Failed to move temp ROM into place: ", rename_ec.message()));
746 }
747
748#if !defined(__EMSCRIPTEN__)
749 // Best-effort fsync the parent dir so the rename is durable.
750 BestEffortFsyncParentDir(target_path);
751#endif
752
753 dirty_ = false;
754 return absl::OkStatus();
755}
756
758 if (fence == nullptr) {
759 return;
760 }
761 write_fence_stack_.push_back(fence);
762}
763
765 if (fence == nullptr) {
766 return;
767 }
768 if (!write_fence_stack_.empty() && write_fence_stack_.back() == fence) {
769 write_fence_stack_.pop_back();
770 return;
771 }
772
773 // Defensive: avoid leaving a stale fence active if call sites mismatch.
774 for (auto it = write_fence_stack_.rbegin(); it != write_fence_stack_.rend();
775 ++it) {
776 if (*it == fence) {
777 write_fence_stack_.erase(std::next(it).base());
778 LOG_WARN("Rom", "Popped non-top write fence (mismatched scope)");
779 return;
780 }
781 }
782 LOG_WARN("Rom", "PopWriteFence called for unknown fence");
783}
784
785absl::StatusOr<uint8_t> Rom::ReadByte(int offset) const {
786 if (offset < 0 || offset >= static_cast<int>(rom_data_.size())) {
787 return absl::OutOfRangeError(absl::StrFormat(
788 "Offset %d out of range (size: %d)", offset, rom_data_.size()));
789 }
790 return rom_data_[offset];
791}
792
793absl::StatusOr<uint16_t> Rom::ReadWord(int offset) const {
794 if (offset < 0 || offset + 1 >= static_cast<int>(rom_data_.size())) {
795 return absl::OutOfRangeError("Offset out of range");
796 }
797 return (uint16_t)(rom_data_[offset] | (rom_data_[offset + 1] << 8));
798}
799
800absl::StatusOr<uint32_t> Rom::ReadLong(int offset) const {
801 if (offset < 0 || offset + 2 >= static_cast<int>(rom_data_.size())) {
802 return absl::OutOfRangeError("Offset out of range");
803 }
804 return (uint32_t)(rom_data_[offset] | (rom_data_[offset + 1] << 8) |
805 (rom_data_[offset + 2] << 16));
806}
807
808absl::StatusOr<std::vector<uint8_t>> Rom::ReadByteVector(
809 uint32_t offset, uint32_t length) const {
810 if (offset + length > static_cast<uint32_t>(rom_data_.size())) {
811 return absl::OutOfRangeError("Offset and length out of range");
812 }
813 std::vector<uint8_t> result;
814 result.reserve(length);
815 for (uint32_t i = offset; i < offset + length; i++) {
816 result.push_back(rom_data_[i]);
817 }
818 return result;
819}
820
821absl::StatusOr<gfx::Tile16> Rom::ReadTile16(uint32_t tile16_id,
822 uint32_t tile16_ptr) {
823 // Skip 8 bytes per tile.
824 auto tpos = tile16_ptr + (tile16_id * 0x08);
825 gfx::Tile16 tile16 = {};
828 tpos += 2;
831 tpos += 2;
834 tpos += 2;
837 return tile16;
838}
839
840absl::Status Rom::WriteTile16(int tile16_id, uint32_t tile16_ptr,
841 const gfx::Tile16& tile) {
842 auto tpos = tile16_ptr + (tile16_id * 0x08);
844 tpos += 2;
846 tpos += 2;
848 tpos += 2;
850 return absl::OkStatus();
851}
852
853absl::Status Rom::WriteByte(int addr, uint8_t value) {
855 if (addr < 0 || addr >= static_cast<int>(rom_data_.size())) {
856 return absl::OutOfRangeError("Address out of range");
857 }
858 for (auto* fence : write_fence_stack_) {
859 RETURN_IF_ERROR(fence->Check(static_cast<uint32_t>(addr), 1, "WriteByte"));
860 }
861 const uint8_t old_val = rom_data_[addr];
862 rom_data_[addr] = value;
863 dirty_ = true;
864#ifdef __EMSCRIPTEN__
865 MaybeBroadcastChange(addr, {old_val}, {value});
866#endif
867 for (auto* fence : write_fence_stack_) {
868 fence->RecordWrite(static_cast<uint32_t>(addr), 1);
869 }
870 return absl::OkStatus();
871}
872
873absl::Status Rom::WriteWord(int addr, uint16_t value) {
875 if (addr < 0 || addr + 1 >= static_cast<int>(rom_data_.size())) {
876 return absl::OutOfRangeError("Address out of range");
877 }
878 for (auto* fence : write_fence_stack_) {
879 RETURN_IF_ERROR(fence->Check(static_cast<uint32_t>(addr), 2, "WriteWord"));
880 }
881 const uint8_t old0 = rom_data_[addr];
882 const uint8_t old1 = rom_data_[addr + 1];
883 rom_data_[addr] = (uint8_t)(value & 0xFF);
884 rom_data_[addr + 1] = (uint8_t)((value >> 8) & 0xFF);
885 dirty_ = true;
886#ifdef __EMSCRIPTEN__
888 {static_cast<uint8_t>(value & 0xFF),
889 static_cast<uint8_t>((value >> 8) & 0xFF)});
890#endif
891 for (auto* fence : write_fence_stack_) {
892 fence->RecordWrite(static_cast<uint32_t>(addr), 2);
893 }
894 return absl::OkStatus();
895}
896
897absl::Status Rom::WriteShort(int addr, uint16_t value) {
898 return WriteWord(addr, value);
899}
900
901absl::Status Rom::WriteLong(uint32_t addr, uint32_t value) {
903 if (addr + 2 >= static_cast<uint32_t>(rom_data_.size())) {
904 return absl::OutOfRangeError("Address out of range");
905 }
906 for (auto* fence : write_fence_stack_) {
907 RETURN_IF_ERROR(fence->Check(addr, 3, "WriteLong"));
908 }
909 const uint8_t old0 = rom_data_[addr];
910 const uint8_t old1 = rom_data_[addr + 1];
911 const uint8_t old2 = rom_data_[addr + 2];
912 rom_data_[addr] = (uint8_t)(value & 0xFF);
913 rom_data_[addr + 1] = (uint8_t)((value >> 8) & 0xFF);
914 rom_data_[addr + 2] = (uint8_t)((value >> 16) & 0xFF);
915 dirty_ = true;
916#ifdef __EMSCRIPTEN__
918 {static_cast<uint8_t>(value & 0xFF),
919 static_cast<uint8_t>((value >> 8) & 0xFF),
920 static_cast<uint8_t>((value >> 16) & 0xFF)});
921#endif
922 for (auto* fence : write_fence_stack_) {
923 fence->RecordWrite(addr, 3);
924 }
925 return absl::OkStatus();
926}
927
928absl::Status Rom::WriteVector(int addr, std::vector<uint8_t> data) {
930 if (addr < 0) {
931 return absl::OutOfRangeError("Address out of range");
932 }
933 if (addr + static_cast<int>(data.size()) >
934 static_cast<int>(rom_data_.size())) {
935 return absl::OutOfRangeError("Address out of range");
936 }
937 for (auto* fence : write_fence_stack_) {
938 RETURN_IF_ERROR(fence->Check(static_cast<uint32_t>(addr),
939 static_cast<uint32_t>(data.size()),
940 "WriteVector"));
941 }
942 std::vector<uint8_t> old_data;
943 old_data.reserve(data.size());
944 for (int i = 0; i < static_cast<int>(data.size()); i++) {
945 old_data.push_back(rom_data_[addr + i]);
946 rom_data_[addr + i] = data[i];
947 }
948 dirty_ = true;
949#ifdef __EMSCRIPTEN__
950 MaybeBroadcastChange(addr, old_data, data);
951#endif
952 for (auto* fence : write_fence_stack_) {
953 fence->RecordWrite(static_cast<uint32_t>(addr),
954 static_cast<uint32_t>(data.size()));
955 }
956 return absl::OkStatus();
957}
958
959absl::Status Rom::WriteColor(uint32_t address, const gfx::SnesColor& color) {
960 uint16_t bgr = ((color.snes() >> 10) & 0x1F) | ((color.snes() & 0x1F) << 10) |
961 (color.snes() & 0x7C00);
962 return WriteWord(address, bgr);
963}
964
965absl::Status Rom::WriteHelper(const WriteAction& action) {
966 if (std::holds_alternative<uint8_t>(action.value)) {
967 return WriteByte(action.address, std::get<uint8_t>(action.value));
968 } else if (std::holds_alternative<uint16_t>(action.value) ||
969 std::holds_alternative<short>(action.value)) {
970 return WriteShort(action.address, std::get<uint16_t>(action.value));
971 } else if (std::holds_alternative<std::vector<uint8_t>>(action.value)) {
972 return WriteVector(action.address,
973 std::get<std::vector<uint8_t>>(action.value));
974 } else if (std::holds_alternative<gfx::SnesColor>(action.value)) {
975 return WriteColor(action.address, std::get<gfx::SnesColor>(action.value));
976 }
977 return absl::InvalidArgumentError("Invalid write argument type");
978}
979
980} // namespace yaze
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:49
absl::StatusOr< std::vector< uint8_t > > ReadByteVector(uint32_t offset, uint32_t length) const
Definition rom.cc:808
void PushWriteFence(rom::WriteFence *fence)
Definition rom.cc:757
absl::Status LoadFromFile(const std::string &filename, const LoadOptions &options=LoadOptions::Defaults())
Definition rom.cc:491
std::vector< uint8_t > rom_data_
Definition rom.h:252
absl::Status CheckLayoutWritable() const
Definition rom.cc:429
absl::StatusOr< gfx::Tile16 > ReadTile16(uint32_t tile16_id, uint32_t tile16_ptr)
Definition rom.cc:821
absl::Status WriteColor(uint32_t address, const gfx::SnesColor &color)
Definition rom.cc:959
auto filename() const
Definition rom.h:207
void PopWriteFence(rom::WriteFence *fence)
Definition rom.cc:764
absl::Status WriteByte(int addr, uint8_t value)
Definition rom.cc:853
rom::LyraLayoutInspection InspectLyraLayout() const
Definition rom.cc:418
absl::StatusOr< uint8_t > ReadByte(int offset) const
Definition rom.cc:785
absl::Status WriteTile16(int tile16_id, uint32_t tile16_ptr, const gfx::Tile16 &tile)
Definition rom.cc:840
const auto & vector() const
Definition rom.h:205
std::vector< uint8_t > copier_header_
Definition rom.h:255
absl::Status WriteVector(int addr, std::vector< uint8_t > data)
Definition rom.cc:928
std::string title_
Definition rom.h:243
absl::Status SaveToFile(const SaveSettings &settings)
Definition rom.cc:648
absl::StatusOr< uint16_t > ReadWord(int offset) const
Definition rom.cc:793
static void SetStagingFailureForTesting(bool fail)
Definition rom.cc:414
CopierHeaderReason copier_header_reason_
Definition rom.h:256
Rom()=default
auto data() const
Definition rom.h:201
std::vector< rom::WriteFence * > write_fence_stack_
Definition rom.h:269
Rom & operator=(const Rom &other)
Definition rom.cc:458
bool dirty_
Definition rom.h:263
absl::Status LoadFromData(const std::vector< uint8_t > &data, const LoadOptions &options=LoadOptions::Defaults())
Definition rom.cc:606
void AdvanceObjectTileRevision()
Definition rom.h:197
std::string filename_
Definition rom.h:246
unsigned long size_
Definition rom.h:240
absl::Status WriteShort(int addr, uint16_t value)
Definition rom.cc:897
project::ResourceLabelManager resource_label_manager_
Definition rom.h:260
std::string short_name_
Definition rom.h:249
uint64_t object_tile_revision_
Definition rom.h:266
absl::Status WriteWord(int addr, uint16_t value)
Definition rom.cc:873
virtual absl::Status WriteHelper(const WriteAction &action)
Definition rom.cc:965
absl::Status WriteLong(uint32_t addr, uint32_t value)
Definition rom.cc:901
absl::StatusOr< uint32_t > ReadLong(int offset) const
Definition rom.cc:800
bool lyra_attributed_at_load_
Definition rom.h:257
SNES Color container.
Definition snes_color.h:110
constexpr uint16_t snes() const
Get SNES 15-bit color.
Definition snes_color.h:193
Tile composition of four 8x8 tiles.
Definition snes_tile.h:142
#define LOG_WARN(category, format,...)
Definition log.h:138
#define LOG_INFO(category, format,...)
Definition log.h:136
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
std::string MakeSafeTimestamp(std::time_t now_c)
Definition rom.cc:88
std::atomic< bool > g_fail_rom_staging_for_testing
Definition rom.cc:191
absl::Status CreateRequiredBackup(const std::filesystem::path &source_path, const std::filesystem::path &requested_backup_path)
Definition rom.cc:142
void BestEffortFsyncFile(const std::filesystem::path &path)
Definition rom.cc:335
constexpr size_t kLoRomInternalHeader
Definition rom.cc:54
bool HasInternalHeaderAt(const uint8_t *data, size_t size, size_t offset)
Definition rom.cc:59
absl::StatusOr< std::filesystem::path > WriteExclusiveRomTemp(const std::filesystem::path &target_path, const std::vector< uint8_t > &bytes)
Definition rom.cc:193
void BestEffortFsyncParentDir(const std::filesystem::path &file_path)
Definition rom.cc:357
constexpr size_t kCopierHeaderAlignment
SNES ROM images are multiples of 32 KB; a copier header adds 0x200.
Definition rom.cc:53
void MaybeStripCopierHeader(std::vector< uint8_t > &rom_data, std::vector< uint8_t > &header, CopierHeaderReason &reason)
Definition rom.cc:74
constexpr size_t kHiRomInternalHeader
Definition rom.cc:55
bool HasInternalHeader(const uint8_t *data, size_t size, size_t base)
Definition rom.cc:69
absl::StatusOr< std::filesystem::path > GetAvailableBackupPath(const std::filesystem::path &requested_path)
Definition rom.cc:116
uint16_t TileInfoToWord(TileInfo tile_info)
Definition snes_tile.cc:362
TileInfo WordToTileInfo(uint16_t word)
Definition snes_tile.cc:379
LyraLayoutInspection InspectLyraLayout(const uint8_t *image, size_t size)
CopierHeaderReason DetectCopierHeader(const uint8_t *data, size_t size)
Definition rom.cc:378
CopierHeaderReason
Why a copier header was recognized at the start of a ROM file.
Definition rom.h:29
constexpr size_t kCopierHeaderSize
Size of the SMC/SWC copier header that some ROM dumps start with.
Definition rom.h:26
const char * CopierHeaderReasonName(CopierHeaderReason reason)
Definition rom.cc:402
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
bool load_resource_labels
Definition rom.h:66
std::string filename
Definition rom.h:54
ValueType value
Definition rom.h:155
bool LoadLabels(const std::string &filename)
Definition project.cc:2824