92 auto project_path = parser.
GetString(
"project");
93 if (!project_path.has_value() || project_path->empty()) {
94 return absl::InvalidArgumentError(
95 "project-bundle-verify: --project is required");
100 rp.has_value() && !rp->empty()) {
101 const fs::path rp_path(*rp);
103 const bool existed_before = fs::exists(rp_path, ec);
104 std::ofstream probe(*rp, std::ios::out | std::ios::binary | std::ios::app);
105 if (!probe.is_open()) {
106 return absl::PermissionDeniedError(absl::StrFormat(
107 "project-bundle-verify: cannot open report file for writing: %s",
111 if (!existed_before) {
112 fs::remove(rp_path, ec);
115 return absl::OkStatus();
121 const std::string project_path = *parser.
GetString(
"project");
122 std::vector<CheckResult> checks;
123 bool any_fail =
false;
130 bool exists = fs::exists(project_path, ec);
133 {
"path_exists",
"fail",
134 absl::StrFormat(
"Path does not exist: %s", project_path)});
137 checks.push_back({
"path_exists",
"pass", project_path});
144 std::string resolved_path = project_path;
145 bool is_bundle =
false;
147 fs::path fsp(project_path);
148 std::string ext = fsp.extension().string();
151 std::string bundle_root =
153 if (!bundle_root.empty()) {
154 resolved_path = bundle_root;
156 }
else if (ext ==
".yazeproj") {
158 resolved_path = project_path;
160 }
else if (ext !=
".yaze" && ext !=
".zsproj") {
162 {
"format_recognized",
"fail",
163 absl::StrFormat(
"Unrecognized project format: %s", ext)});
169 {
"format_recognized",
"pass",
170 is_bundle ?
"yazeproj bundle" : absl::StrFormat(
"file (%s)", ext)});
177 if (is_bundle && !any_fail) {
178 fs::path bundle_dir(resolved_path);
179 fs::path project_yaze = bundle_dir /
"project.yaze";
181 if (fs::exists(project_yaze, ec) && !ec) {
182 checks.push_back({
"bundle_project_yaze",
"pass",
183 "project.yaze found in bundle root"});
186 {
"bundle_project_yaze",
"warn",
187 "project.yaze missing — will be auto-created on open"});
195 bool parse_ok =
false;
197 auto status = proj.
Open(resolved_path);
201 {
"project_parses",
"pass",
202 absl::StrFormat(
"name=%s", proj.
name)});
205 {
"project_parses",
"fail",
206 absl::StrFormat(
"Parse failed: %s",
207 std::string(status.message()))});
216 if (HasAbsolutePaths(proj)) {
217 auto abs_paths = ListAbsolutePaths(proj);
218 std::string detail =
"Absolute paths reduce portability: ";
219 for (
size_t i = 0; i < abs_paths.size(); ++i) {
220 if (i > 0) detail +=
"; ";
221 detail += abs_paths[i];
223 checks.push_back({
"path_portability",
"warn", detail});
226 {
"path_portability",
"pass",
"All paths are relative"});
236 if (fs::exists(rom_abs, ec) && !ec) {
237 auto fsize = fs::file_size(rom_abs, ec);
239 checks.push_back({
"rom_accessible",
"warn",
240 absl::StrFormat(
"ROM exists but size unreadable: %s",
244 {
"rom_accessible",
"pass",
245 absl::StrFormat(
"%s (%zu bytes)", rom_abs, fsize)});
249 {
"rom_accessible",
"fail",
250 absl::StrFormat(
"ROM not found: %s (resolved: %s)",
254 }
else if (parse_ok) {
255 checks.push_back({
"rom_accessible",
"warn",
"No ROM path in project"});
261 if (parser.
HasFlag(
"check-rom-hash") && parse_ok) {
263 checks.push_back({
"rom_hash_check",
"warn",
264 "Hash check only supported for .yazeproj bundles"});
266 fs::path manifest_path = fs::path(resolved_path) /
"manifest.json";
268 if (!fs::exists(manifest_path, ec) || ec) {
269 checks.push_back({
"rom_hash_check",
"warn",
270 "No manifest.json in bundle (hash unavailable)"});
272 std::ifstream mf(manifest_path);
273 auto manifest = nlohmann::json::parse(mf,
nullptr,
false);
274 if (manifest.is_discarded()) {
275 checks.push_back({
"rom_hash_check",
"fail",
276 "manifest.json parse failed"});
279 std::string raw_expected =
280 manifest.value(
"rom_sha1", std::string{});
281 if (raw_expected.empty()) {
282 checks.push_back({
"rom_hash_check",
"warn",
283 "No rom_sha1 field in manifest.json"});
285 std::string expected_sha1 = NormalizeHash(raw_expected);
288 if (actual_sha1.empty()) {
290 {
"rom_hash_check",
"fail",
291 absl::StrFormat(
"Cannot read ROM for hashing: %s",
294 }
else if (NormalizeHash(actual_sha1) == expected_sha1) {
296 {
"rom_hash_check",
"pass",
297 absl::StrFormat(
"SHA1 match: %s", actual_sha1)});
300 {
"rom_hash_check",
"fail",
301 absl::StrFormat(
"SHA1 mismatch: expected=%s actual=%s",
302 expected_sha1, actual_sha1)});
314 bool overall_ok = !any_fail;
315 int pass_count = 0, warn_count = 0, fail_count = 0;
316 for (
const auto& chk : checks) {
317 if (chk.status ==
"pass") ++pass_count;
318 else if (chk.status ==
"warn") ++warn_count;
322 formatter.
AddField(
"ok", overall_ok);
324 overall_ok ? std::string(
"pass") : std::string(
"fail"));
325 formatter.
AddField(
"project_path", resolved_path);
326 formatter.
AddField(
"is_bundle", is_bundle);
327 formatter.
AddField(
"pass_count", pass_count);
328 formatter.
AddField(
"warn_count", warn_count);
329 formatter.
AddField(
"fail_count", fail_count);
332 for (
const auto& chk : checks) {
334 formatter.
AddField(
"name", chk.name);
335 formatter.
AddField(
"status", chk.status);
336 formatter.
AddField(
"detail", chk.detail);
344 if (
auto rp = parser.
GetString(
"report");
345 rp.has_value() && !rp->empty()) {
347 nlohmann::json report;
348 report[
"ok"] = overall_ok;
349 report[
"status"] = overall_ok ?
"pass" :
"fail";
350 report[
"project_path"] = resolved_path;
351 report[
"is_bundle"] = is_bundle;
352 report[
"pass_count"] = pass_count;
353 report[
"warn_count"] = warn_count;
354 report[
"fail_count"] = fail_count;
355 nlohmann::json checks_json = nlohmann::json::array();
356 for (
const auto& chk : checks) {
357 checks_json.push_back({{
"name", chk.name},
358 {
"status", chk.status},
359 {
"detail", chk.detail}});
361 report[
"checks"] = std::move(checks_json);
363 std::ofstream report_file(
364 *rp, std::ios::out | std::ios::binary | std::ios::trunc);
365 if (!report_file.is_open()) {
366 return absl::PermissionDeniedError(absl::StrFormat(
367 "project-bundle-verify: cannot open report file: %s", *rp));
369 report_file << report.dump(2) <<
"\n";
370 if (!report_file.good()) {
371 return absl::InternalError(absl::StrFormat(
372 "project-bundle-verify: failed writing report: %s", *rp));
377 return absl::FailedPreconditionError(absl::StrFormat(
378 "project-bundle-verify: %d check(s) failed", fail_count));
380 return absl::OkStatus();