yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
message_doctor_commands.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <iostream>
5
6#include "absl/status/status.h"
7#include "absl/strings/str_format.h"
10#include "rom/rom.h"
11
12namespace yaze {
13namespace cli {
14
15namespace {
16
17// Validate control codes in message data
19 DiagnosticReport& report) {
20 for (size_t i = 0; i < msg.Data.size(); ++i) {
21 uint8_t byte = msg.Data[i];
22 // Control codes are in range 0x67-0x80
23 if (byte >= 0x67 && byte <= 0x80) {
24 auto cmd = editor::FindMatchingCommand(byte);
25 if (!cmd.has_value()) {
26 DiagnosticFinding finding;
27 finding.id = "unknown_control_code";
29 finding.message = absl::StrFormat(
30 "Unknown control code 0x%02X in message %d", byte, msg.ID);
31 finding.location = absl::StrFormat("Message %d offset %zu", msg.ID, i);
32 finding.fixable = false;
33 report.AddFinding(finding);
34 } else if (cmd->HasArgument) {
35 // Check if command argument follows when required
36 if (i + 1 >= msg.Data.size()) {
37 DiagnosticFinding finding;
38 finding.id = "missing_command_arg";
40 finding.message = absl::StrFormat(
41 "Control code [%s] missing argument in message %d", cmd->Token,
42 msg.ID);
43 finding.location =
44 absl::StrFormat("Message %d offset %zu", msg.ID, i);
45 finding.fixable = false;
46 report.AddFinding(finding);
47 } else {
48 // Skip the argument byte
49 ++i;
50 }
51 }
52 }
53 }
54}
55
56// Check for missing terminators
58 DiagnosticReport& report) {
59 if (msg.Data.empty()) {
60 DiagnosticFinding finding;
61 finding.id = "empty_message";
63 finding.message = absl::StrFormat("Message %d is empty", msg.ID);
64 finding.location = absl::StrFormat("Message %d", msg.ID);
65 finding.fixable = false;
66 report.AddFinding(finding);
67 return;
68 }
69
70 // Check that message ends with terminator (0x7F)
71 if (msg.Data.back() != editor::kMessageTerminator) {
72 DiagnosticFinding finding;
73 finding.id = "missing_terminator";
75 finding.message =
76 absl::StrFormat("Message %d missing 0x7F terminator (ends with 0x%02X)",
77 msg.ID, msg.Data.back());
78 finding.location = absl::StrFormat("Message %d", msg.ID);
79 finding.suggested_action = "Add 0x7F terminator to end of message";
80 finding.fixable = true;
81 report.AddFinding(finding);
82 }
83}
84
85// Validate dictionary references
87 const std::vector<editor::DictionaryEntry>& dict,
88 DiagnosticReport& report) {
89 for (size_t i = 0; i < msg.Data.size(); ++i) {
90 uint8_t byte = msg.Data[i];
91 if (byte >= editor::DICTOFF) {
92 int dict_idx = byte - editor::DICTOFF;
93 if (dict_idx >= static_cast<int>(dict.size())) {
94 DiagnosticFinding finding;
95 finding.id = "invalid_dict_ref";
97 finding.message = absl::StrFormat(
98 "Invalid dictionary reference [D:%02X] in message %d (max valid: "
99 "%02X)",
100 dict_idx, msg.ID, static_cast<int>(dict.size()) - 1);
101 finding.location = absl::StrFormat("Message %d offset %zu", msg.ID, i);
102 finding.fixable = false;
103 report.AddFinding(finding);
104 }
105 }
106 }
107}
108
109// Check for common corruption patterns
111 DiagnosticReport& report) {
112 // Check for large runs of 0x00 or 0xFF
113 int zero_run = 0;
114 int ff_run = 0;
115 const int kCorruptionThreshold = 10;
116
117 for (uint8_t byte : msg.Data) {
118 if (byte == 0x00) {
119 zero_run++;
120 ff_run = 0;
121 } else if (byte == 0xFF) {
122 ff_run++;
123 zero_run = 0;
124 } else {
125 zero_run = 0;
126 ff_run = 0;
127 }
128
129 if (zero_run >= kCorruptionThreshold) {
130 DiagnosticFinding finding;
131 finding.id = "possible_corruption_zeros";
133 finding.message = absl::StrFormat(
134 "Large block of zeros (%d bytes) in message %d", zero_run, msg.ID);
135 finding.location = absl::StrFormat("Message %d", msg.ID);
136 finding.suggested_action = "Check if message data is corrupted";
137 finding.fixable = false;
138 report.AddFinding(finding);
139 break; // Only report once per message
140 }
141
142 if (ff_run >= kCorruptionThreshold) {
143 DiagnosticFinding finding;
144 finding.id = "possible_corruption_ff";
146 finding.message = absl::StrFormat(
147 "Large block of 0xFF (%d bytes) in message %d", ff_run, msg.ID);
148 finding.location = absl::StrFormat("Message %d", msg.ID);
149 finding.suggested_action = "Check if message data is corrupted or erased";
150 finding.fixable = false;
151 report.AddFinding(finding);
152 break; // Only report once per message
153 }
154 }
155}
156
157// Validate line widths in messages
159 DiagnosticReport& report) {
161 for (const auto& warning : warnings) {
162 DiagnosticFinding finding;
163 finding.id = "line_too_wide";
165 finding.message = absl::StrFormat("Message %d: %s", msg.ID, warning);
166 finding.location = absl::StrFormat("Message %d", msg.ID);
167 finding.suggested_action = "Shorten line or add a line break command";
168 finding.fixable = true;
169 report.AddFinding(finding);
170 }
171}
172
173} // namespace
174
176 Rom* rom, const resources::ArgumentParser& args,
177 resources::OutputFormatter& formatter) {
178 bool verbose = args.HasFlag("verbose");
179
180 DiagnosticReport report;
181
182 if (!rom || !rom->is_loaded()) {
183 return absl::InvalidArgumentError("ROM not loaded");
184 }
185
186 // 1. Build Dictionary. BuildDictionaryEntries does not bound its reads; the
187 // pointer table and every entry it can address lie below the end of bank
188 // $0E, so a ROM at least that long keeps the scan inside the buffer.
189 constexpr size_t kDictionaryBankEnd = 0x78000;
190 if (rom->size() < kDictionaryBankEnd) {
191 DiagnosticFinding finding;
192 finding.id = "dictionary_out_of_bounds";
194 finding.message =
195 absl::StrFormat("ROM is 0x%zX bytes; the dictionary tables need 0x%zX",
196 rom->size(), kDictionaryBankEnd);
197 finding.location = "Dictionary Tables";
198 finding.fixable = false;
199 report.AddFinding(finding);
200
201 formatter.AddField("total_findings", report.TotalFindings());
202 formatter.AddField("critical_count", report.critical_count);
203 if (formatter.IsJson()) {
204 formatter.BeginArray("findings");
205 formatter.AddArrayItem(finding.FormatJson());
206 formatter.EndArray();
207 } else {
208 std::cout << finding.message << "\n";
209 }
210 return absl::OkStatus();
211 }
212 std::vector<editor::DictionaryEntry> dictionary;
213 try {
214 dictionary = editor::BuildDictionaryEntries(rom);
215 } catch (const std::exception& e) {
216 DiagnosticFinding finding;
217 finding.id = "dictionary_build_failed";
219 finding.message =
220 absl::StrFormat("Failed to build dictionary: %s", e.what());
221 finding.location = "Dictionary Tables";
222 finding.fixable = false;
223 report.AddFinding(finding);
224
225 // Cannot proceed without dictionary
226 formatter.AddField("total_findings", report.TotalFindings());
227 formatter.AddField("critical_count", report.critical_count);
228 return absl::OkStatus();
229 }
230
231 // 2. Scan Messages
232 std::vector<editor::MessageData> messages;
233 try {
234 uint8_t* mutable_data = const_cast<uint8_t*>(rom->data());
235 const int scan_end = static_cast<int>(std::min<size_t>(
236 rom->size(), static_cast<size_t>(editor::kTextDataEnd)));
237 messages =
238 editor::ReadAllTextData(mutable_data, editor::kTextData, scan_end);
239
240 } catch (const std::exception& e) {
241 DiagnosticFinding finding;
242 finding.id = "message_scan_failed";
244 finding.message = absl::StrFormat("Failed to scan messages: %s", e.what());
245 finding.location = "Message Data Region";
246 finding.fixable = false;
247 report.AddFinding(finding);
248 }
249
250 // 3. Analyze Each Message
251 int valid_count = 0;
252 int empty_count = 0;
253
254 for (const auto& msg : messages) {
255 if (msg.Data.empty()) {
256 empty_count++;
257 continue;
258 }
259
260 // Run all validations
261 // Note: ValidateTerminators is skipped because ReadAllTextData strips
262 // the 0x7F terminator from stored Data (it's used as the delimiter).
263 // Messages returned by the parser are guaranteed to be properly terminated.
264 ValidateControlCodes(msg, report);
265 ValidateDictionaryRefs(msg, dictionary, report);
266 CheckCorruptionPatterns(msg, report);
267 ValidateLineWidths(msg, report);
268
269 valid_count++;
270 }
271
272 // 4. Output results
273 formatter.AddField("messages_scanned", static_cast<int>(messages.size()));
274 formatter.AddField("valid_messages", valid_count);
275 formatter.AddField("empty_messages", empty_count);
276 formatter.AddField("dictionary_entries", static_cast<int>(dictionary.size()));
277 formatter.AddField("total_findings", report.TotalFindings());
278 formatter.AddField("critical_count", report.critical_count);
279 formatter.AddField("error_count", report.error_count);
280 formatter.AddField("warning_count", report.warning_count);
281 formatter.AddField("info_count", report.info_count);
282 formatter.AddField("fixable_count", report.fixable_count);
283
284 // Output findings array for JSON
285 if (formatter.IsJson()) {
286 formatter.BeginArray("findings");
287 for (const auto& finding : report.findings) {
288 formatter.AddArrayItem(finding.FormatJson());
289 }
290 formatter.EndArray();
291 }
292
293 // Text output
294 if (!formatter.IsJson()) {
295 std::cout << "\n";
296 std::cout << "╔════════════════════════════════════════════════════════════"
297 "═══╗\n";
298 std::cout << "║ MESSAGE DOCTOR "
299 " ║\n";
300 std::cout << "╠════════════════════════════════════════════════════════════"
301 "═══╣\n";
302 std::cout << absl::StrFormat("║ Messages Scanned: %-42d ║\n",
303 static_cast<int>(messages.size()));
304 std::cout << absl::StrFormat("║ Valid Messages: %-44d ║\n", valid_count);
305 std::cout << absl::StrFormat("║ Empty Messages: %-44d ║\n", empty_count);
306 std::cout << absl::StrFormat("║ Dictionary Entries: %-40d ║\n",
307 static_cast<int>(dictionary.size()));
308 std::cout << "╠════════════════════════════════════════════════════════════"
309 "═══╣\n";
310 std::cout << absl::StrFormat(
311 "║ Findings: %d total (%d errors, %d warnings, %d info)%-8s ║\n",
312 report.TotalFindings(), report.error_count, report.warning_count,
313 report.info_count, "");
314 if (report.fixable_count > 0) {
315 std::cout << absl::StrFormat("║ Fixable Issues: %-44d ║\n",
316 report.fixable_count);
317 }
318 std::cout << "╚════════════════════════════════════════════════════════════"
319 "═══╝\n";
320
321 if (verbose && !report.findings.empty()) {
322 std::cout << "\n=== Detailed Findings ===\n";
323 for (const auto& finding : report.findings) {
324 std::cout << " " << finding.FormatText() << "\n";
325 }
326 } else if (!verbose && report.HasProblems()) {
327 std::cout << "\nUse --verbose to see detailed findings.\n";
328 }
329
330 if (!report.HasProblems()) {
331 std::cout << "\n \033[1;32mNo critical issues found.\033[0m\n";
332 }
333 std::cout << "\n";
334 }
335
336 return absl::OkStatus();
337}
338
339} // namespace cli
340} // 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:52
auto data() const
Definition rom.h:213
auto size() const
Definition rom.h:212
bool is_loaded() const
Definition rom.h:199
absl::Status Execute(Rom *rom, const resources::ArgumentParser &args, resources::OutputFormatter &formatter) override
Execute the command business logic.
Utility for parsing common CLI argument patterns.
bool HasFlag(const std::string &name) const
Check if a flag is present.
Utility for consistent output formatting across commands.
void BeginArray(const std::string &key)
Begin an array.
void AddArrayItem(const std::string &item)
Add an item to current array.
void AddField(const std::string &key, const std::string &value)
Add a key-value pair.
bool IsJson() const
Check if using JSON format.
void ValidateDictionaryRefs(const editor::MessageData &msg, const std::vector< editor::DictionaryEntry > &dict, DiagnosticReport &report)
void CheckCorruptionPatterns(const editor::MessageData &msg, DiagnosticReport &report)
void ValidateControlCodes(const editor::MessageData &msg, DiagnosticReport &report)
void ValidateTerminators(const editor::MessageData &msg, DiagnosticReport &report)
void ValidateLineWidths(const editor::MessageData &msg, DiagnosticReport &report)
constexpr int kTextData
std::vector< MessageData > ReadAllTextData(uint8_t *rom, int pos, int max_pos, bool allow_bank_switch)
constexpr uint8_t kMessageTerminator
std::vector< DictionaryEntry > BuildDictionaryEntries(Rom *rom)
constexpr uint8_t DICTOFF
std::optional< TextElement > FindMatchingCommand(uint8_t b)
std::vector< std::string > ValidateMessageLineWidths(const std::string &message)
constexpr int kTextDataEnd
A single diagnostic finding.
std::string FormatJson() const
Format finding as JSON object string.
Complete diagnostic report.
std::vector< DiagnosticFinding > findings
int TotalFindings() const
Get total finding count.
bool HasProblems() const
Check if report has any critical or error findings.
void AddFinding(const DiagnosticFinding &finding)
Add a finding and update counts.
std::vector< uint8_t > Data