205 const std::string project_path = *parser.
GetString(
"project");
206 std::vector<CheckResult> checks;
207 bool any_fail =
false;
214 bool exists = fs::exists(project_path, ec);
217 {
"path_exists",
"fail",
218 absl::StrFormat(
"Path does not exist: %s", project_path)});
221 checks.push_back({
"path_exists",
"pass", project_path});
228 std::string resolved_path = project_path;
229 bool is_bundle =
false;
231 fs::path fsp(project_path);
232 std::string ext = fsp.extension().string();
235 std::string bundle_root =
237 if (!bundle_root.empty()) {
238 resolved_path = bundle_root;
240 }
else if (ext ==
".yazeproj") {
242 resolved_path = project_path;
244 }
else if (ext !=
".yaze" && ext !=
".zsproj") {
246 {
"format_recognized",
"fail",
247 absl::StrFormat(
"Unrecognized project format: %s", ext)});
253 {
"format_recognized",
"pass",
254 is_bundle ?
"yazeproj bundle" : absl::StrFormat(
"file (%s)", ext)});
261 if (is_bundle && !any_fail) {
262 fs::path bundle_dir(resolved_path);
263 fs::path project_yaze = bundle_dir /
"project.yaze";
265 if (fs::exists(project_yaze, ec) && !ec) {
267 {
"bundle_project_yaze",
"pass",
"project.yaze found in bundle root"});
269 checks.push_back({
"bundle_project_yaze",
"warn",
270 "project.yaze missing — will be auto-created on open"});
278 bool parse_ok =
false;
280 auto status = proj.
Open(resolved_path);
284 {
"project_parses",
"pass", absl::StrFormat(
"name=%s", proj.
name)});
287 {
"project_parses",
"fail",
288 absl::StrFormat(
"Parse failed: %s", std::string(status.message()))});
297 const fs::path project_config =
298 is_bundle ? fs::path(resolved_path) /
"project.yaze"
299 : fs::path(resolved_path);
300 const auto abs_paths = ListAbsolutePathsInProjectFile(project_config);
301 if (!abs_paths.empty()) {
302 std::string detail =
"Absolute paths reduce portability: ";
303 for (
size_t i = 0; i < abs_paths.size(); ++i) {
306 detail += abs_paths[i];
308 checks.push_back({
"path_portability",
"warn", detail});
310 checks.push_back({
"path_portability",
"pass",
"All paths are relative"});
318 const std::string manifest_abs =
321 if (!fs::exists(manifest_abs, ec) || ec) {
323 {
"hack_manifest_ready",
"fail",
324 absl::StrFormat(
"Configured hack manifest not found: %s",
329 {
"hack_manifest_ready",
"fail",
330 absl::StrFormat(
"Configured hack manifest failed to load: %s",
334 checks.push_back({
"hack_manifest_ready",
"pass",
335 absl::StrFormat(
"Loaded configured hack manifest: %s",
346 if (fs::exists(rom_abs, ec) && !ec) {
347 auto fsize = fs::file_size(rom_abs, ec);
350 {
"rom_accessible",
"warn",
351 absl::StrFormat(
"ROM exists but size unreadable: %s", rom_abs)});
353 checks.push_back({
"rom_accessible",
"pass",
354 absl::StrFormat(
"%s (%zu bytes)", rom_abs, fsize)});
357 checks.push_back({
"rom_accessible",
"fail",
358 absl::StrFormat(
"ROM not found: %s (resolved: %s)",
362 }
else if (parse_ok) {
363 checks.push_back({
"rom_accessible",
"warn",
"No ROM path in project"});
369 if (parser.
HasFlag(
"check-rom-hash") && parse_ok) {
370 std::string raw_expected;
371 bool hash_metadata_ready =
true;
374 fs::path manifest_path = fs::path(resolved_path) /
"manifest.json";
376 if (!fs::exists(manifest_path, ec) || ec) {
377 checks.push_back({
"rom_hash_check",
"warn",
378 "No manifest.json in bundle (hash unavailable)"});
379 hash_metadata_ready =
false;
381 std::ifstream mf(manifest_path);
382 auto manifest = nlohmann::json::parse(mf,
nullptr,
false);
383 if (manifest.is_discarded()) {
385 {
"rom_hash_check",
"fail",
"manifest.json parse failed"});
387 hash_metadata_ready =
false;
389 raw_expected = manifest.value(
"rom_sha1", std::string{});
390 if (raw_expected.empty()) {
391 checks.push_back({
"rom_hash_check",
"warn",
392 "No rom_sha1 field in manifest.json"});
393 hash_metadata_ready =
false;
399 if (raw_expected.empty()) {
400 checks.push_back({
"rom_hash_check",
"warn",
401 "No expected_hash in standalone project"});
402 hash_metadata_ready =
false;
406 if (hash_metadata_ready) {
407 const std::string expected = NormalizeHash(raw_expected);
410 std::string algorithm;
417 if (expected.size() != 40 || !IsHexHash(expected)) {
419 {
"rom_hash_check",
"fail",
420 "manifest.json rom_sha1 must be 40 hexadecimal characters"});
422 hash_metadata_ready =
false;
426 }
else if ((expected.size() == 8 || expected.size() == 40) &&
427 IsHexHash(expected)) {
428 algorithm = expected.size() == 8 ?
"CRC32" :
"SHA1";
432 const auto load_status = loaded_rom.
LoadFromFile(rom_abs, load_options);
433 if (!load_status.ok()) {
435 {
"rom_hash_check",
"fail",
436 absl::StrFormat(
"Cannot load ROM for hashing: %s (%s)", rom_abs,
437 load_status.message())});
439 hash_metadata_ready =
false;
440 }
else if (expected.size() == 8) {
447 {
"rom_hash_check",
"fail",
448 "Standalone expected_hash must be an 8-character CRC32 or "
449 "40-character SHA1 hexadecimal digest"});
451 hash_metadata_ready =
false;
454 if (hash_metadata_ready) {
455 if (actual.empty()) {
457 {
"rom_hash_check",
"fail",
458 absl::StrFormat(
"Cannot read ROM for hashing: %s", rom_abs)});
460 }
else if (NormalizeHash(actual) == expected) {
462 {
"rom_hash_check",
"pass",
463 absl::StrFormat(
"%s match: %s", algorithm, actual)});
466 {
"rom_hash_check",
"fail",
467 absl::StrFormat(
"%s mismatch: expected=%s actual=%s", algorithm,
478 bool overall_ok = !any_fail;
479 int pass_count = 0, warn_count = 0, fail_count = 0;
480 for (
const auto& chk : checks) {
481 if (chk.status ==
"pass")
483 else if (chk.status ==
"warn")
489 formatter.
AddField(
"ok", overall_ok);
491 overall_ok ? std::string(
"pass") : std::string(
"fail"));
492 formatter.
AddField(
"project_path", resolved_path);
493 formatter.
AddField(
"is_bundle", is_bundle);
494 formatter.
AddField(
"pass_count", pass_count);
495 formatter.
AddField(
"warn_count", warn_count);
496 formatter.
AddField(
"fail_count", fail_count);
499 for (
const auto& chk : checks) {
501 formatter.
AddField(
"name", chk.name);
502 formatter.
AddField(
"status", chk.status);
503 formatter.
AddField(
"detail", chk.detail);
511 if (
auto rp = parser.
GetString(
"report"); rp.has_value() && !rp->empty()) {
513 nlohmann::json report;
514 report[
"ok"] = overall_ok;
515 report[
"status"] = overall_ok ?
"pass" :
"fail";
516 report[
"project_path"] = resolved_path;
517 report[
"is_bundle"] = is_bundle;
518 report[
"pass_count"] = pass_count;
519 report[
"warn_count"] = warn_count;
520 report[
"fail_count"] = fail_count;
521 nlohmann::json checks_json = nlohmann::json::array();
522 for (
const auto& chk : checks) {
523 checks_json.push_back(
524 {{
"name", chk.name}, {
"status", chk.status}, {
"detail", chk.detail}});
526 report[
"checks"] = std::move(checks_json);
528 std::ofstream report_file(
529 *rp, std::ios::out | std::ios::binary | std::ios::trunc);
530 if (!report_file.is_open()) {
531 return absl::PermissionDeniedError(absl::StrFormat(
532 "project-bundle-verify: cannot open report file: %s", *rp));
534 report_file << report.dump(2) <<
"\n";
535 if (!report_file.good()) {
536 return absl::InternalError(absl::StrFormat(
537 "project-bundle-verify: failed writing report: %s", *rp));
542 return absl::FailedPreconditionError(absl::StrFormat(
543 "project-bundle-verify: %d check(s) failed", fail_count));
545 return absl::OkStatus();
bool equals(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, BinaryPredicate p)