yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
rom_file_manager.cc
Go to the documentation of this file.
1#include "rom_file_manager.h"
2
3#include <algorithm>
4#include <chrono>
5#include <ctime>
6#include <filesystem>
7#include <fstream>
8#include <unordered_set>
9
10#include "absl/strings/str_format.h"
12#include "rom/rom.h"
13#include "util/file_util.h"
14#include "util/log.h"
15#include "zelda3/game_data.h"
16
17namespace yaze::editor {
18
19namespace {
20
21std::time_t ToTimeT(const std::filesystem::file_time_type& ftime) {
22 using namespace std::chrono;
23 auto sctp = time_point_cast<system_clock::duration>(
24 ftime - std::filesystem::file_time_type::clock::now() +
25 system_clock::now());
26 return system_clock::to_time_t(sctp);
27}
28
29std::string DayKey(std::time_t timestamp) {
30 std::tm local_tm{};
31#ifdef _WIN32
32 localtime_s(&local_tm, &timestamp);
33#else
34 localtime_r(&timestamp, &local_tm);
35#endif
36 char buffer[16];
37 std::strftime(buffer, sizeof(buffer), "%Y-%m-%d", &local_tm);
38 return std::string(buffer);
39}
40
41} // namespace
42
44 : toast_manager_(toast_manager) {}
45
46absl::Status RomFileManager::LoadRom(Rom* rom, const std::string& filename) {
47 if (!rom) {
48 return absl::InvalidArgumentError("ROM pointer cannot be null");
49 }
50 if (filename.empty()) {
51 return absl::InvalidArgumentError("No filename provided");
52 }
53 return LoadRomFromFile(rom, filename);
54}
55
56absl::Status RomFileManager::SaveRom(Rom* rom) {
57 if (!IsRomLoaded(rom)) {
58 return absl::FailedPreconditionError("No ROM loaded to save");
59 }
60
62 auto backup_status = CreateBackup(rom);
63 if (!backup_status.ok()) {
64 if (toast_manager_) {
66 absl::StrFormat("Backup failed: %s", backup_status.message()),
68 }
69 return backup_status;
70 }
71 }
72
73 Rom::SaveSettings settings;
74 settings.backup = false;
75 settings.save_new = false;
76
77 auto status = rom->SaveToFile(settings);
78 if (!status.ok() && toast_manager_) {
80 absl::StrFormat("Failed to save ROM: %s", status.message()),
82 } else if (toast_manager_) {
83 toast_manager_->Show("ROM saved successfully", ToastType::kSuccess);
84 }
85 return status;
86}
87
88absl::Status RomFileManager::SaveRomAs(Rom* rom, const std::string& filename) {
89 if (!IsRomLoaded(rom)) {
90 return absl::FailedPreconditionError("No ROM loaded to save");
91 }
92 if (filename.empty()) {
93 return absl::InvalidArgumentError("No filename provided for save as");
94 }
95
96 // Save As must not overwrite or back up the source ROM. If the requested
97 // target already exists, back up that target before replacing it.
98 if (backup_before_save_ && std::filesystem::exists(filename)) {
99 const std::string source_filename = rom->filename();
100 rom->set_filename(filename);
101 auto backup_status = CreateBackup(rom);
102 rom->set_filename(source_filename);
103 if (!backup_status.ok()) {
104 if (toast_manager_) {
106 absl::StrFormat("Backup failed: %s", backup_status.message()),
108 }
109 return backup_status;
110 }
111 }
112
113 Rom::SaveSettings settings;
114 settings.backup = false;
115 settings.save_new = false;
116 settings.filename = filename;
117 // settings.z3_save = true; // Deprecated
118
119 auto status = rom->SaveToFile(settings);
120 if (!status.ok() && toast_manager_) {
122 absl::StrFormat("Failed to save ROM as: %s", status.message()),
124 } else if (toast_manager_) {
125 toast_manager_->Show(absl::StrFormat("ROM saved as: %s", filename),
127 }
128 if (status.ok()) {
129 rom->set_filename(filename);
130 }
131 return status;
132}
133
135 const std::string& filename) {
136 if (!rom) {
137 return absl::InvalidArgumentError("ROM pointer cannot be null");
138 }
139 if (filename.empty()) {
140 return absl::InvalidArgumentError("No filename provided");
141 }
142
143 std::string extension = std::filesystem::path(filename).extension().string();
144
145 if (extension == ".yaze" || extension == ".yazeproj" ||
146 extension == ".zsproj") {
147 return absl::UnimplementedError("Project file loading not yet implemented");
148 }
149
150 return LoadRom(rom, filename);
151}
152
154 if (!IsRomLoaded(rom)) {
155 return absl::FailedPreconditionError("No ROM loaded to backup");
156 }
157
158 const std::string source_filename = rom->filename();
159 if (source_filename.empty()) {
160 return absl::InvalidArgumentError("ROM has no filename to backup");
161 }
162
163 // Safety: create backups from the on-disk ROM file, not the in-memory buffer.
164 // This ensures the backup represents the last saved state before overwrite.
165 std::string backup_filename = GenerateBackupFilename(source_filename);
166
167 std::error_code ec;
168 std::filesystem::copy_file(source_filename, backup_filename,
169 std::filesystem::copy_options::overwrite_existing,
170 ec);
171 if (ec) {
172 auto status = absl::InternalError(
173 absl::StrFormat("Failed to create backup: %s", ec.message()));
174 if (toast_manager_) {
176 absl::StrFormat("Backup failed: %s", status.message()),
178 }
179 return status;
180 }
181
182 if (toast_manager_) {
183 toast_manager_->Show(absl::StrFormat("Backup created: %s", backup_filename),
185 }
186
187 auto prune_status = PruneBackups(source_filename);
188 if (!prune_status.ok()) {
189 LOG_WARN("RomFileManager", "Backup prune failed: %s",
190 prune_status.message().data());
191 }
192
193 return absl::OkStatus();
194}
195
197 if (!IsRomLoaded(rom)) {
198 return absl::FailedPreconditionError("No valid ROM to validate");
199 }
200
201 if (rom->size() < 512 * 1024 || rom->size() > 8 * 1024 * 1024) {
202 return absl::InvalidArgumentError("ROM size is outside expected range");
203 }
204 if (rom->title().empty()) {
205 return absl::InvalidArgumentError("ROM title is empty or invalid");
206 }
207
208 if (toast_manager_) {
209 toast_manager_->Show("ROM validation passed", ToastType::kSuccess);
210 }
211 return absl::OkStatus();
212}
213
215 return rom && rom->is_loaded();
216}
217
218std::string RomFileManager::GetRomFilename(Rom* rom) const {
219 if (!IsRomLoaded(rom)) {
220 return "";
221 }
222 return rom->filename();
223}
224
226 const std::string& filename) {
227 if (!rom) {
228 return absl::InvalidArgumentError("ROM pointer cannot be null");
229 }
230 if (!IsValidRomFile(filename)) {
231 return absl::InvalidArgumentError(
232 absl::StrFormat("Invalid ROM file: %s", filename));
233 }
234
235 auto status = rom->LoadFromFile(filename);
236 if (!status.ok()) {
237 if (toast_manager_) {
239 absl::StrFormat("Failed to load ROM: %s", status.message()),
241 }
242 return status;
243 }
244
245 // IMPORTANT: Game data loading is now decoupled and should be handled
246 // by the caller (EditorManager) or a higher-level orchestration layer
247 // that manages both the generic Rom and the Zelda3::GameData.
248 // This class is strictly for generic ROM file operations.
249
250 if (toast_manager_) {
251 toast_manager_->Show(absl::StrFormat("ROM loaded: %s", rom->title()),
253 }
254 return absl::OkStatus();
255}
256
258 const std::string& original_filename) const {
259 std::filesystem::path path(original_filename);
260 std::string stem = path.stem().string();
261 std::string extension = path.extension().string();
262
263 auto now = std::chrono::system_clock::now();
264 auto time_t = std::chrono::system_clock::to_time_t(now);
265 const auto ms_since_epoch =
266 std::chrono::duration_cast<std::chrono::milliseconds>(
267 now.time_since_epoch())
268 .count();
269 const auto ms_part = ms_since_epoch % 1000;
270
271 std::filesystem::path backup_dir = GetBackupDirectory(original_filename);
272
273 std::string filename = absl::StrFormat(
274 "%s_backup_%lld_%03lld%s", stem, static_cast<long long>(time_t),
275 static_cast<long long>(ms_part), extension);
276 return (backup_dir / filename).string();
277}
278
280 const std::string& original_filename) const {
281 std::filesystem::path path(original_filename);
282 std::filesystem::path backup_dir = path.parent_path();
283 if (!backup_folder_.empty()) {
284 backup_dir = std::filesystem::path(backup_folder_);
285 }
286 std::error_code ec;
287 std::filesystem::create_directories(backup_dir, ec);
288 return backup_dir;
289}
290
291std::vector<RomFileManager::BackupEntry> RomFileManager::ListBackups(
292 const std::string& rom_filename) const {
293 std::vector<BackupEntry> backups;
294 if (rom_filename.empty()) {
295 return backups;
296 }
297
298 std::filesystem::path rom_path(rom_filename);
299 const std::string stem = rom_path.stem().string();
300 const std::string extension = rom_path.extension().string();
301 const std::string prefix = stem + "_backup_";
302 std::filesystem::path backup_dir = GetBackupDirectory(rom_filename);
303
304 std::error_code ec;
305 for (const auto& entry :
306 std::filesystem::directory_iterator(backup_dir, ec)) {
307 if (ec || !entry.is_regular_file()) {
308 continue;
309 }
310 const auto path = entry.path();
311 if (path.extension() != extension) {
312 continue;
313 }
314 const std::string filename = path.filename().string();
315 if (filename.rfind(prefix, 0) != 0) {
316 continue;
317 }
318
319 BackupEntry backup;
320 backup.path = path.string();
321 backup.filename = filename;
322 backup.size_bytes = entry.file_size(ec);
323 auto ftime = entry.last_write_time(ec);
324 if (!ec) {
325 backup.timestamp = ToTimeT(ftime);
326 }
327 backups.push_back(std::move(backup));
328 }
329
330 std::sort(backups.begin(), backups.end(),
331 [](const BackupEntry& a, const BackupEntry& b) {
332 return a.timestamp > b.timestamp;
333 });
334 return backups;
335}
336
338 const std::string& rom_filename) const {
339 if (backup_retention_count_ <= 0) {
340 return absl::OkStatus();
341 }
342
343 auto backups = ListBackups(rom_filename);
344 if (backups.size() <= static_cast<size_t>(backup_retention_count_)) {
345 return absl::OkStatus();
346 }
347
348 std::unordered_set<std::string> keep_paths;
349 const auto now = std::chrono::system_clock::now();
350 const auto keep_daily_days =
351 std::chrono::hours(24 * std::max(1, backup_keep_daily_days_));
352
353 if (backup_keep_daily_) {
354 std::unordered_set<std::string> seen_days;
355 for (const auto& backup : backups) {
356 if (backup.timestamp == 0) {
357 continue;
358 }
359 const auto backup_time =
360 std::chrono::system_clock::from_time_t(backup.timestamp);
361 if (now - backup_time > keep_daily_days) {
362 continue;
363 }
364 std::string day = DayKey(backup.timestamp);
365 if (seen_days.insert(day).second) {
366 keep_paths.insert(backup.path);
367 }
368 }
369 }
370
371 for (size_t i = 0;
372 i < backups.size() &&
373 keep_paths.size() < static_cast<size_t>(backup_retention_count_);
374 ++i) {
375 keep_paths.insert(backups[i].path);
376 }
377
378 for (const auto& backup : backups) {
379 if (keep_paths.count(backup.path) > 0) {
380 continue;
381 }
382 std::error_code remove_ec;
383 std::filesystem::remove(backup.path, remove_ec);
384 if (remove_ec) {
385 LOG_WARN("RomFileManager", "Failed to delete backup: %s",
386 backup.path.c_str());
387 }
388 }
389
390 return absl::OkStatus();
391}
392
393bool RomFileManager::IsValidRomFile(const std::string& filename) const {
394 if (filename.empty()) {
395 return false;
396 }
397
398 std::error_code ec;
399 if (!std::filesystem::exists(filename, ec) || ec) {
400 return false;
401 }
402
403 auto file_size = std::filesystem::file_size(filename, ec);
404 if (ec) {
405 return false;
406 }
407 // Zelda 3 ROMs are 1MB (0x100000 = 1,048,576 bytes), possibly with 512-byte
408 // SMC header. Allow ROMs from 512KB to 8MB to be safe.
409 if (file_size < 512 * 1024 || file_size > 8 * 1024 * 1024) {
410 return false;
411 }
412
413 return true;
414}
415
416} // namespace yaze::editor
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:28
absl::Status LoadFromFile(const std::string &filename, const LoadOptions &options=LoadOptions::Defaults())
Definition rom.cc:227
auto filename() const
Definition rom.h:157
absl::Status SaveToFile(const SaveSettings &settings)
Definition rom.cc:371
auto size() const
Definition rom.h:150
auto set_filename(std::string_view name)
Definition rom.h:158
bool is_loaded() const
Definition rom.h:144
auto title() const
Definition rom.h:149
std::string GenerateBackupFilename(const std::string &original_filename) const
absl::Status PruneBackups(const std::string &rom_filename) const
absl::Status OpenRomOrProject(Rom *rom, const std::string &filename)
std::vector< BackupEntry > ListBackups(const std::string &rom_filename) const
absl::Status ValidateRom(Rom *rom)
RomFileManager(ToastManager *toast_manager)
std::string GetRomFilename(Rom *rom) const
absl::Status CreateBackup(Rom *rom)
absl::Status LoadRom(Rom *rom, const std::string &filename)
absl::Status SaveRom(Rom *rom)
absl::Status LoadRomFromFile(Rom *rom, const std::string &filename)
bool IsValidRomFile(const std::string &filename) const
absl::Status SaveRomAs(Rom *rom, const std::string &filename)
bool IsRomLoaded(Rom *rom) const
std::filesystem::path GetBackupDirectory(const std::string &original_filename) const
void Show(const std::string &message, ToastType type=ToastType::kInfo, float ttl_seconds=3.0f)
#define LOG_WARN(category, format,...)
Definition log.h:107
std::time_t ToTimeT(const std::filesystem::file_time_type &ftime)
Editors are the view controllers for the application.
std::string filename
Definition rom.h:33