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;
494 }
else if ((expected.size() == 8 || expected.size() == 40) &&
495 IsHexHash(expected)) {
496 algorithm = expected.size() == 8 ?
"CRC32" :
"SHA1";
500 const auto load_status = loaded_rom.
LoadFromFile(rom_abs, load_options);
501 if (!load_status.ok()) {
503 {
"rom_hash_check",
"fail",
504 absl::StrFormat(
"Cannot load ROM for hashing: %s (%s)", rom_abs,
505 load_status.message())});
507 hash_metadata_ready =
false;
508 }
else if (expected.size() == 8) {
515 {
"rom_hash_check",
"fail",
516 "Standalone expected_hash must be an 8-character CRC32 or "
517 "40-character SHA1 hexadecimal digest"});
519 hash_metadata_ready =
false;
522 if (hash_metadata_ready) {
523 if (actual.empty()) {
525 {
"rom_hash_check",
"fail",
526 absl::StrFormat(
"Cannot read ROM for hashing: %s", rom_abs)});
528 }
else if (NormalizeHash(actual) == expected) {
529 const std::string detail =
530 is_bundle ? absl::StrFormat(
"%s match (%s): %s", algorithm,
531 bundle_hash_source, actual)
532 : absl::StrFormat(
"%s match: %s", algorithm, actual);
533 checks.push_back({
"rom_hash_check",
"pass", detail});
536 {
"rom_hash_check",
"fail",
537 absl::StrFormat(
"%s mismatch: expected=%s actual=%s", algorithm,
548 bool overall_ok = !any_fail;
549 int pass_count = 0, warn_count = 0, fail_count = 0;
550 for (
const auto& chk : checks) {
551 if (chk.status ==
"pass")
553 else if (chk.status ==
"warn")
559 formatter.
AddField(
"ok", overall_ok);
561 overall_ok ? std::string(
"pass") : std::string(
"fail"));
562 formatter.
AddField(
"project_path", resolved_path);
563 formatter.
AddField(
"is_bundle", is_bundle);
564 formatter.
AddField(
"pass_count", pass_count);
565 formatter.
AddField(
"warn_count", warn_count);
566 formatter.
AddField(
"fail_count", fail_count);
569 for (
const auto& chk : checks) {
571 formatter.
AddField(
"name", chk.name);
572 formatter.
AddField(
"status", chk.status);
573 formatter.
AddField(
"detail", chk.detail);
581 if (
auto rp = parser.
GetString(
"report"); rp.has_value() && !rp->empty()) {
583 nlohmann::json report;
584 report[
"ok"] = overall_ok;
585 report[
"status"] = overall_ok ?
"pass" :
"fail";
586 report[
"project_path"] = resolved_path;
587 report[
"is_bundle"] = is_bundle;
588 report[
"pass_count"] = pass_count;
589 report[
"warn_count"] = warn_count;
590 report[
"fail_count"] = fail_count;
591 nlohmann::json checks_json = nlohmann::json::array();
592 for (
const auto& chk : checks) {
593 checks_json.push_back(
594 {{
"name", chk.name}, {
"status", chk.status}, {
"detail", chk.detail}});
596 report[
"checks"] = std::move(checks_json);
598 std::ofstream report_file(
599 *rp, std::ios::out | std::ios::binary | std::ios::trunc);
600 if (!report_file.is_open()) {
601 return absl::PermissionDeniedError(absl::StrFormat(
602 "project-bundle-verify: cannot open report file: %s", *rp));
604 report_file << report.dump(2) <<
"\n";
605 if (!report_file.good()) {
606 return absl::InternalError(absl::StrFormat(
607 "project-bundle-verify: failed writing report: %s", *rp));
612 return absl::FailedPreconditionError(absl::StrFormat(
613 "project-bundle-verify: %d check(s) failed", fail_count));
615 return absl::OkStatus();
bool equals(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, BinaryPredicate p)