6#include "absl/status/status.h"
7#include "absl/strings/str_format.h"
20 for (
size_t i = 0; i < msg.
Data.size(); ++i) {
21 uint8_t
byte = msg.
Data[i];
23 if (
byte >= 0x67 &&
byte <= 0x80) {
25 if (!cmd.has_value()) {
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);
34 }
else if (cmd->HasArgument) {
36 if (i + 1 >= msg.
Data.size()) {
38 finding.
id =
"missing_command_arg";
40 finding.
message = absl::StrFormat(
41 "Control code [%s] missing argument in message %d", cmd->Token,
44 absl::StrFormat(
"Message %d offset %zu", msg.
ID, i);
59 if (msg.
Data.empty()) {
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);
73 finding.
id =
"missing_terminator";
76 absl::StrFormat(
"Message %d missing 0x7F terminator (ends with 0x%02X)",
78 finding.
location = absl::StrFormat(
"Message %d", msg.
ID);
87 const std::vector<editor::DictionaryEntry>& dict,
89 for (
size_t i = 0; i < msg.
Data.size(); ++i) {
90 uint8_t
byte = msg.
Data[i];
93 if (dict_idx >=
static_cast<int>(dict.size())) {
95 finding.
id =
"invalid_dict_ref";
97 finding.
message = absl::StrFormat(
98 "Invalid dictionary reference [D:%02X] in message %d (max valid: "
100 dict_idx, msg.
ID,
static_cast<int>(dict.size()) - 1);
101 finding.
location = absl::StrFormat(
"Message %d offset %zu", msg.
ID, i);
115 const int kCorruptionThreshold = 10;
117 for (uint8_t
byte : msg.
Data) {
121 }
else if (
byte == 0xFF) {
129 if (zero_run >= kCorruptionThreshold) {
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);
142 if (ff_run >= kCorruptionThreshold) {
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);
161 for (
const auto& warning : warnings) {
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);
178 bool verbose = args.
HasFlag(
"verbose");
183 return absl::InvalidArgumentError(
"ROM not loaded");
189 constexpr size_t kDictionaryBankEnd = 0x78000;
190 if (rom->
size() < kDictionaryBankEnd) {
192 finding.
id =
"dictionary_out_of_bounds";
195 absl::StrFormat(
"ROM is 0x%zX bytes; the dictionary tables need 0x%zX",
196 rom->
size(), kDictionaryBankEnd);
197 finding.
location =
"Dictionary Tables";
208 std::cout << finding.
message <<
"\n";
210 return absl::OkStatus();
212 std::vector<editor::DictionaryEntry> dictionary;
215 }
catch (
const std::exception& e) {
217 finding.
id =
"dictionary_build_failed";
220 absl::StrFormat(
"Failed to build dictionary: %s", e.what());
221 finding.
location =
"Dictionary Tables";
228 return absl::OkStatus();
232 std::vector<editor::MessageData> messages;
234 uint8_t* mutable_data =
const_cast<uint8_t*
>(rom->
data());
235 const int scan_end =
static_cast<int>(std::min<size_t>(
240 }
catch (
const std::exception& e) {
242 finding.
id =
"message_scan_failed";
244 finding.
message = absl::StrFormat(
"Failed to scan messages: %s", e.what());
245 finding.
location =
"Message Data Region";
254 for (
const auto& msg : messages) {
255 if (msg.Data.empty()) {
264 ValidateControlCodes(msg, report);
265 ValidateDictionaryRefs(msg, dictionary, report);
266 CheckCorruptionPatterns(msg, report);
267 ValidateLineWidths(msg, report);
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()));
287 for (
const auto& finding : report.
findings) {
294 if (!formatter.
IsJson()) {
296 std::cout <<
"╔════════════════════════════════════════════════════════════"
298 std::cout <<
"║ MESSAGE DOCTOR "
300 std::cout <<
"╠════════════════════════════════════════════════════════════"
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 <<
"╠════════════════════════════════════════════════════════════"
310 std::cout << absl::StrFormat(
311 "║ Findings: %d total (%d errors, %d warnings, %d info)%-8s ║\n",
315 std::cout << absl::StrFormat(
"║ Fixable Issues: %-44d ║\n",
318 std::cout <<
"╚════════════════════════════════════════════════════════════"
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";
327 std::cout <<
"\nUse --verbose to see detailed findings.\n";
331 std::cout <<
"\n \033[1;32mNo critical issues found.\033[0m\n";
336 return absl::OkStatus();
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
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.
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)
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 suggested_action
std::string FormatJson() const
Format finding as JSON object string.
DiagnosticSeverity severity
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
std::string ContentsParsed