272 const std::string project_path = *parser.
GetString(
"project");
273 std::vector<CheckResult> checks;
274 bool any_fail =
false;
281 bool exists = fs::exists(project_path, ec);
284 {
"path_exists",
"fail",
285 absl::StrFormat(
"Path does not exist: %s", project_path)});
288 checks.push_back({
"path_exists",
"pass", project_path});
295 std::string resolved_path = project_path;
296 bool is_bundle =
false;
298 fs::path fsp(project_path);
299 std::string ext = fsp.extension().string();
302 std::string bundle_root =
304 if (!bundle_root.empty()) {
305 resolved_path = bundle_root;
307 }
else if (ext ==
".yazeproj") {
309 resolved_path = project_path;
311 }
else if (ext !=
".yaze" && ext !=
".zsproj") {
313 {
"format_recognized",
"fail",
314 absl::StrFormat(
"Unrecognized project format: %s", ext)});
320 {
"format_recognized",
"pass",
321 is_bundle ?
"yazeproj bundle" : absl::StrFormat(
"file (%s)", ext)});
328 if (is_bundle && !any_fail) {
329 fs::path bundle_dir(resolved_path);
330 fs::path project_yaze = bundle_dir /
"project.yaze";
332 if (fs::exists(project_yaze, ec) && !ec) {
334 {
"bundle_project_yaze",
"pass",
"project.yaze found in bundle root"});
336 checks.push_back({
"bundle_project_yaze",
"warn",
337 "project.yaze missing — will be auto-created on open"});
345 bool parse_ok =
false;
347 auto status = proj.
Open(resolved_path);
351 {
"project_parses",
"pass", absl::StrFormat(
"name=%s", proj.
name)});
354 {
"project_parses",
"fail",
355 absl::StrFormat(
"Parse failed: %s", std::string(status.message()))});
364 const fs::path project_config =
365 is_bundle ? fs::path(resolved_path) /
"project.yaze"
366 : fs::path(resolved_path);
367 const auto abs_paths = ListAbsolutePathsInProjectFile(project_config);
368 if (!abs_paths.empty()) {
369 std::string detail =
"Absolute paths reduce portability: ";
370 for (
size_t i = 0; i < abs_paths.size(); ++i) {
373 detail += abs_paths[i];
375 checks.push_back({
"path_portability",
"warn", detail});
377 checks.push_back({
"path_portability",
"pass",
"All paths are relative"});
385 const std::string manifest_abs =
388 if (!fs::exists(manifest_abs, ec) || ec) {
390 {
"hack_manifest_ready",
"fail",
391 absl::StrFormat(
"Configured hack manifest not found: %s",
396 {
"hack_manifest_ready",
"fail",
397 absl::StrFormat(
"Configured hack manifest failed to load: %s",
401 checks.push_back({
"hack_manifest_ready",
"pass",
402 absl::StrFormat(
"Loaded configured hack manifest: %s",
413 if (fs::exists(rom_abs, ec) && !ec) {
414 auto fsize = fs::file_size(rom_abs, ec);
417 {
"rom_accessible",
"warn",
418 absl::StrFormat(
"ROM exists but size unreadable: %s", rom_abs)});
420 checks.push_back({
"rom_accessible",
"pass",
421 absl::StrFormat(
"%s (%zu bytes)", rom_abs, fsize)});
424 checks.push_back({
"rom_accessible",
"fail",
425 absl::StrFormat(
"ROM not found: %s (resolved: %s)",
429 }
else if (parse_ok) {
430 checks.push_back({
"rom_accessible",
"warn",
"No ROM path in project"});
436 if (parser.
HasFlag(
"check-rom-hash") && parse_ok) {
437 std::string raw_expected;
438 std::string bundle_hash_source;
439 bool hash_metadata_ready =
true;
442 fs::path manifest_path = fs::path(resolved_path) /
"manifest.json";
444 if (!fs::exists(manifest_path, ec) || ec) {
445 checks.push_back({
"rom_hash_check",
"warn",
446 "No manifest.json in bundle (hash unavailable)"});
447 hash_metadata_ready =
false;
449 std::ifstream mf(manifest_path);
450 auto manifest = nlohmann::json::parse(mf,
nullptr,
false);
451 if (manifest.is_discarded()) {
453 {
"rom_hash_check",
"fail",
"manifest.json parse failed"});
455 hash_metadata_ready =
false;
457 const BundleHashMetadata metadata = ParseBundleHashMetadata(manifest);
458 if (!metadata.error.empty()) {
459 checks.push_back({
"rom_hash_check",
"fail", metadata.error});
461 hash_metadata_ready =
false;
462 }
else if (!metadata.available) {
464 {
"rom_hash_check",
"warn",
465 "No usable romChecksum or rom_sha1 digest in manifest.json"});
466 hash_metadata_ready =
false;
468 raw_expected = metadata.expected;
469 bundle_hash_source = metadata.source;
475 if (raw_expected.empty()) {
476 checks.push_back({
"rom_hash_check",
"warn",
477 "No expected_hash in standalone project"});
478 hash_metadata_ready =
false;
482 if (hash_metadata_ready) {
483 const std::string expected = NormalizeHash(raw_expected);
486 std::string algorithm;
487 bool legacy_match =
false;
495 }
else if ((expected.size() == 8 || expected.size() == 40) &&
496 IsHexHash(expected)) {
497 algorithm = expected.size() == 8 ?
"CRC32" :
"SHA1";
501 const auto load_status = loaded_rom.
LoadFromFile(rom_abs, load_options);
502 if (!load_status.ok()) {
504 {
"rom_hash_check",
"fail",
505 absl::StrFormat(
"Cannot load ROM for hashing: %s (%s)", rom_abs,
506 load_status.message())});
508 hash_metadata_ready =
false;
509 }
else if (expected.size() == 8) {
511 loaded_rom.
data(), loaded_rom.
size());
512 actual = identity.crc32;
520 {
"rom_hash_check",
"fail",
521 "Standalone expected_hash must be an 8-character CRC32 or "
522 "40-character SHA1 hexadecimal digest"});
524 hash_metadata_ready =
false;
527 if (hash_metadata_ready) {
528 if (actual.empty()) {
530 {
"rom_hash_check",
"fail",
531 absl::StrFormat(
"Cannot read ROM for hashing: %s", rom_abs)});
533 }
else if (legacy_match) {
535 {
"rom_hash_check",
"pass",
537 "Legacy Yaze CRC32 match: %s; standard CRC32: %s", expected,
539 }
else if (NormalizeHash(actual) == expected) {
540 const std::string detail =
541 is_bundle ? absl::StrFormat(
"%s match (%s): %s", algorithm,
542 bundle_hash_source, actual)
543 : absl::StrFormat(
"%s match: %s", algorithm, actual);
544 checks.push_back({
"rom_hash_check",
"pass", detail});
547 {
"rom_hash_check",
"fail",
548 absl::StrFormat(
"%s mismatch: expected=%s actual=%s", algorithm,
559 bool overall_ok = !any_fail;
560 int pass_count = 0, warn_count = 0, fail_count = 0;
561 for (
const auto& chk : checks) {
562 if (chk.status ==
"pass")
564 else if (chk.status ==
"warn")
570 formatter.
AddField(
"ok", overall_ok);
572 overall_ok ? std::string(
"pass") : std::string(
"fail"));
573 formatter.
AddField(
"project_path", resolved_path);
574 formatter.
AddField(
"is_bundle", is_bundle);
575 formatter.
AddField(
"pass_count", pass_count);
576 formatter.
AddField(
"warn_count", warn_count);
577 formatter.
AddField(
"fail_count", fail_count);
580 for (
const auto& chk : checks) {
582 formatter.
AddField(
"name", chk.name);
583 formatter.
AddField(
"status", chk.status);
584 formatter.
AddField(
"detail", chk.detail);
592 if (
auto rp = parser.
GetString(
"report"); rp.has_value() && !rp->empty()) {
594 nlohmann::json report;
595 report[
"ok"] = overall_ok;
596 report[
"status"] = overall_ok ?
"pass" :
"fail";
597 report[
"project_path"] = resolved_path;
598 report[
"is_bundle"] = is_bundle;
599 report[
"pass_count"] = pass_count;
600 report[
"warn_count"] = warn_count;
601 report[
"fail_count"] = fail_count;
602 nlohmann::json checks_json = nlohmann::json::array();
603 for (
const auto& chk : checks) {
604 checks_json.push_back(
605 {{
"name", chk.name}, {
"status", chk.status}, {
"detail", chk.detail}});
607 report[
"checks"] = std::move(checks_json);
609 std::ofstream report_file(
610 *rp, std::ios::out | std::ios::binary | std::ios::trunc);
611 if (!report_file.is_open()) {
612 return absl::PermissionDeniedError(absl::StrFormat(
613 "project-bundle-verify: cannot open report file: %s", *rp));
615 report_file << report.dump(2) <<
"\n";
616 if (!report_file.good()) {
617 return absl::InternalError(absl::StrFormat(
618 "project-bundle-verify: failed writing report: %s", *rp));
623 return absl::FailedPreconditionError(absl::StrFormat(
624 "project-bundle-verify: %d check(s) failed", fail_count));
626 return absl::OkStatus();
bool equals(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, BinaryPredicate p)