yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
project_bundle_verify_commands.cc
Go to the documentation of this file.
2
3#include <cctype>
4#include <filesystem>
5#include <fstream>
6#include <ios>
7#include <string>
8#include <vector>
9
10#include "absl/status/status.h"
11#include "absl/strings/str_format.h"
12#include "core/project.h"
13#include "nlohmann/json.hpp"
14#include "util/rom_hash.h"
15
16namespace yaze::cli::handlers {
17
18namespace fs = std::filesystem;
19
20namespace {
21
22// Normalize a hex hash string: trim whitespace, lowercase.
23std::string NormalizeHash(const std::string& hash) {
24 std::string result;
25 result.reserve(hash.size());
26 for (char ch : hash) {
27 if (ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r') continue;
28 result += static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
29 }
30 return result;
31}
32
34 std::string name;
35 std::string status; // "pass" | "warn" | "fail"
36 std::string detail;
37};
38
40 auto is_abs = [](const std::string& p) {
41 return !p.empty() && fs::path(p).is_absolute();
42 };
43 return is_abs(proj.rom_filename) || is_abs(proj.code_folder) ||
44 is_abs(proj.assets_folder) || is_abs(proj.patches_folder) ||
45 is_abs(proj.labels_filename) || is_abs(proj.custom_objects_folder);
46}
47
48std::vector<std::string> ListAbsolutePaths(
49 const project::YazeProject& proj) {
50 std::vector<std::string> results;
51 auto check = [&](const std::string& field_name, const std::string& value) {
52 if (!value.empty() && fs::path(value).is_absolute()) {
53 results.push_back(
54 absl::StrFormat("%s = %s", field_name, value));
55 }
56 };
57 check("rom_filename", proj.rom_filename);
58 check("code_folder", proj.code_folder);
59 check("assets_folder", proj.assets_folder);
60 check("patches_folder", proj.patches_folder);
61 check("labels_filename", proj.labels_filename);
62 check("custom_objects_folder", proj.custom_objects_folder);
63 return results;
64}
65
66} // namespace
67
70 Descriptor desc;
71 desc.display_name = "Project Bundle Verify";
72 desc.summary =
73 "Verify the structural integrity and portability of a .yaze project "
74 "file or .yazeproj bundle directory. Checks path existence, config "
75 "parsing, reference sanity, and ROM accessibility.";
76 desc.todo_reference = "todo#project-bundle-infra";
77 desc.entries = {
78 {"--project",
79 "Path to .yaze project file or .yazeproj bundle directory (required)",
80 ""},
81 {"--check-rom-hash",
82 "Verify ROM SHA1 hash against manifest.json (if present in bundle)",
83 ""},
84 {"--report",
85 "Write full JSON summary to this path in addition to stdout", ""},
86 };
87 return desc;
88}
89
91 const resources::ArgumentParser& parser) {
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");
96 }
97
98 // Probe --report path writability.
99 if (auto rp = parser.GetString("report");
100 rp.has_value() && !rp->empty()) {
101 const fs::path rp_path(*rp);
102 std::error_code ec;
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",
108 *rp));
109 }
110 probe.close();
111 if (!existed_before) {
112 fs::remove(rp_path, ec);
113 }
114 }
115 return absl::OkStatus();
116}
117
119 Rom* /*rom*/, const resources::ArgumentParser& parser,
120 resources::OutputFormatter& formatter) {
121 const std::string project_path = *parser.GetString("project");
122 std::vector<CheckResult> checks;
123 bool any_fail = false;
124
125 // ------------------------------------------------------------------
126 // Check 1: Path exists
127 // ------------------------------------------------------------------
128 {
129 std::error_code ec;
130 bool exists = fs::exists(project_path, ec);
131 if (!exists || ec) {
132 checks.push_back(
133 {"path_exists", "fail",
134 absl::StrFormat("Path does not exist: %s", project_path)});
135 any_fail = true;
136 } else {
137 checks.push_back({"path_exists", "pass", project_path});
138 }
139 }
140
141 // ------------------------------------------------------------------
142 // Check 2: Recognized format
143 // ------------------------------------------------------------------
144 std::string resolved_path = project_path;
145 bool is_bundle = false;
146 {
147 fs::path fsp(project_path);
148 std::string ext = fsp.extension().string();
149
150 // Try bundle root resolution (handles paths inside bundles too)
151 std::string bundle_root =
153 if (!bundle_root.empty()) {
154 resolved_path = bundle_root;
155 is_bundle = true;
156 } else if (ext == ".yazeproj") {
157 // Accept .yazeproj by extension (directory may or may not exist yet)
158 resolved_path = project_path;
159 is_bundle = true;
160 } else if (ext != ".yaze" && ext != ".zsproj") {
161 checks.push_back(
162 {"format_recognized", "fail",
163 absl::StrFormat("Unrecognized project format: %s", ext)});
164 any_fail = true;
165 }
166
167 if (!any_fail) {
168 checks.push_back(
169 {"format_recognized", "pass",
170 is_bundle ? "yazeproj bundle" : absl::StrFormat("file (%s)", ext)});
171 }
172 }
173
174 // ------------------------------------------------------------------
175 // Check 3: Bundle structure (yazeproj only)
176 // ------------------------------------------------------------------
177 if (is_bundle && !any_fail) {
178 fs::path bundle_dir(resolved_path);
179 fs::path project_yaze = bundle_dir / "project.yaze";
180 std::error_code ec;
181 if (fs::exists(project_yaze, ec) && !ec) {
182 checks.push_back({"bundle_project_yaze", "pass",
183 "project.yaze found in bundle root"});
184 } else {
185 checks.push_back(
186 {"bundle_project_yaze", "warn",
187 "project.yaze missing — will be auto-created on open"});
188 }
189 }
190
191 // ------------------------------------------------------------------
192 // Check 4: Project parses
193 // ------------------------------------------------------------------
195 bool parse_ok = false;
196 if (!any_fail) {
197 auto status = proj.Open(resolved_path);
198 if (status.ok()) {
199 parse_ok = true;
200 checks.push_back(
201 {"project_parses", "pass",
202 absl::StrFormat("name=%s", proj.name)});
203 } else {
204 checks.push_back(
205 {"project_parses", "fail",
206 absl::StrFormat("Parse failed: %s",
207 std::string(status.message()))});
208 any_fail = true;
209 }
210 }
211
212 // ------------------------------------------------------------------
213 // Check 5: Path portability (warnings for absolute paths)
214 // ------------------------------------------------------------------
215 if (parse_ok) {
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];
222 }
223 checks.push_back({"path_portability", "warn", detail});
224 } else {
225 checks.push_back(
226 {"path_portability", "pass", "All paths are relative"});
227 }
228 }
229
230 // ------------------------------------------------------------------
231 // Check 6: ROM accessibility
232 // ------------------------------------------------------------------
233 if (parse_ok && !proj.rom_filename.empty()) {
234 std::string rom_abs = proj.GetAbsolutePath(proj.rom_filename);
235 std::error_code ec;
236 if (fs::exists(rom_abs, ec) && !ec) {
237 auto fsize = fs::file_size(rom_abs, ec);
238 if (ec) {
239 checks.push_back({"rom_accessible", "warn",
240 absl::StrFormat("ROM exists but size unreadable: %s",
241 rom_abs)});
242 } else {
243 checks.push_back(
244 {"rom_accessible", "pass",
245 absl::StrFormat("%s (%zu bytes)", rom_abs, fsize)});
246 }
247 } else {
248 checks.push_back(
249 {"rom_accessible", "fail",
250 absl::StrFormat("ROM not found: %s (resolved: %s)",
251 proj.rom_filename, rom_abs)});
252 any_fail = true;
253 }
254 } else if (parse_ok) {
255 checks.push_back({"rom_accessible", "warn", "No ROM path in project"});
256 }
257
258 // ------------------------------------------------------------------
259 // Check 7: ROM hash verification (optional, --check-rom-hash)
260 // ------------------------------------------------------------------
261 if (parser.HasFlag("check-rom-hash") && parse_ok) {
262 if (!is_bundle) {
263 checks.push_back({"rom_hash_check", "warn",
264 "Hash check only supported for .yazeproj bundles"});
265 } else {
266 fs::path manifest_path = fs::path(resolved_path) / "manifest.json";
267 std::error_code ec;
268 if (!fs::exists(manifest_path, ec) || ec) {
269 checks.push_back({"rom_hash_check", "warn",
270 "No manifest.json in bundle (hash unavailable)"});
271 } else {
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"});
277 any_fail = true;
278 } else {
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"});
284 } else {
285 std::string expected_sha1 = NormalizeHash(raw_expected);
286 std::string rom_abs = proj.GetAbsolutePath(proj.rom_filename);
287 std::string actual_sha1 = util::ComputeFileSha1Hex(rom_abs);
288 if (actual_sha1.empty()) {
289 checks.push_back(
290 {"rom_hash_check", "fail",
291 absl::StrFormat("Cannot read ROM for hashing: %s",
292 rom_abs)});
293 any_fail = true;
294 } else if (NormalizeHash(actual_sha1) == expected_sha1) {
295 checks.push_back(
296 {"rom_hash_check", "pass",
297 absl::StrFormat("SHA1 match: %s", actual_sha1)});
298 } else {
299 checks.push_back(
300 {"rom_hash_check", "fail",
301 absl::StrFormat("SHA1 mismatch: expected=%s actual=%s",
302 expected_sha1, actual_sha1)});
303 any_fail = true;
304 }
305 }
306 }
307 }
308 }
309 }
310
311 // ------------------------------------------------------------------
312 // Emit results
313 // ------------------------------------------------------------------
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;
319 else ++fail_count;
320 }
321
322 formatter.AddField("ok", overall_ok);
323 formatter.AddField("status",
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);
330
331 formatter.BeginArray("checks");
332 for (const auto& chk : checks) {
333 formatter.BeginObject("");
334 formatter.AddField("name", chk.name);
335 formatter.AddField("status", chk.status);
336 formatter.AddField("detail", chk.detail);
337 formatter.EndObject();
338 }
339 formatter.EndArray();
340
341 // ------------------------------------------------------------------
342 // Write report file
343 // ------------------------------------------------------------------
344 if (auto rp = parser.GetString("report");
345 rp.has_value() && !rp->empty()) {
346 // Build a standalone JSON report (formatter may be text mode)
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}});
360 }
361 report["checks"] = std::move(checks_json);
362
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));
368 }
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));
373 }
374 }
375
376 if (!overall_ok) {
377 return absl::FailedPreconditionError(absl::StrFormat(
378 "project-bundle-verify: %d check(s) failed", fail_count));
379 }
380 return absl::OkStatus();
381}
382
383} // namespace yaze::cli::handlers
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:28
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
Descriptor Describe() const override
Provide metadata for TUI/help summaries.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
Utility for parsing common CLI argument patterns.
std::optional< std::string > GetString(const std::string &name) const
Parse a named argument (e.g., –format=json or –format json)
bool HasFlag(const std::string &name) const
Check if a flag is present.
Utility for consistent output formatting across commands.
void BeginArray(const std::string &key)
Begin an array.
void BeginObject(const std::string &title="")
Start a JSON object or text section.
void EndObject()
End a JSON object or text section.
void AddField(const std::string &key, const std::string &value)
Add a key-value pair.
std::vector< std::string > ListAbsolutePaths(const project::YazeProject &proj)
std::string ComputeFileSha1Hex(const std::string &path)
Definition rom_hash.cc:213
Modern project structure with comprehensive settings consolidation.
Definition project.h:120
std::string custom_objects_folder
Definition project.h:140
static std::string ResolveBundleRoot(const std::string &path)
Definition project.cc:269
std::string patches_folder
Definition project.h:136
std::string assets_folder
Definition project.h:135
std::string labels_filename
Definition project.h:137
std::string GetAbsolutePath(const std::string &relative_path) const
Definition project.cc:1287
absl::Status Open(const std::string &project_path)
Definition project.cc:295