yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
command_handler.cc
Go to the documentation of this file.
2
3#include <iostream>
4#include <optional>
5#include <utility>
6
7#include "absl/flags/declare.h"
8#include "absl/flags/flag.h"
9#include "absl/strings/str_format.h"
11#include "util/macro.h"
12
13ABSL_DECLARE_FLAG(bool, sandbox);
14
15namespace yaze {
16namespace cli {
17namespace resources {
18namespace {
19
20absl::StatusOr<std::filesystem::path> CaptureRomPathIdentity(
21 const std::filesystem::path& path) {
22 std::error_code absolute_ec;
23 const std::filesystem::path absolute =
24 std::filesystem::absolute(path, absolute_ec);
25 if (absolute_ec) {
26 return absl::FailedPreconditionError(
27 absl::StrFormat("Cannot capture ROM path identity for %s: %s",
28 path.string(), absolute_ec.message()));
29 }
30
31 // Resolve symlinks before collapsing '..': link/../rom.sfc can identify a
32 // different file than the lexically normalized spelling.
33 std::error_code canonical_ec;
34 const std::filesystem::path canonical =
35 std::filesystem::weakly_canonical(absolute, canonical_ec);
36 if (canonical_ec) {
37 return absl::FailedPreconditionError(
38 absl::StrFormat("Cannot capture ROM path identity for %s: %s",
39 absolute.string(), canonical_ec.message()));
40 }
41 return canonical.lexically_normal();
42}
43
44} // namespace
45
46absl::StatusOr<std::filesystem::path> ResolveStableArtifactPath(
47 const std::filesystem::path& path) {
48 std::error_code absolute_ec;
49 const std::filesystem::path absolute =
50 std::filesystem::absolute(path, absolute_ec);
51 if (absolute_ec) {
52 return absl::InvalidArgumentError(absl::StrFormat(
53 "Cannot normalize path %s: %s", path.string(), absolute_ec.message()));
54 }
55 // Resolve every existing component once. Publication then uses this stable
56 // path instead of following a caller-supplied parent symlink a second time.
57 // Do not collapse '..' before symlink resolution changes its parent.
58 std::error_code canonical_ec;
59 const std::filesystem::path canonical =
60 std::filesystem::weakly_canonical(absolute, canonical_ec);
61 if (canonical_ec) {
62 return absl::FailedPreconditionError(
63 absl::StrFormat("Cannot safely resolve path %s: %s", absolute.string(),
64 canonical_ec.message()));
65 }
66 const std::filesystem::path resolved = canonical.lexically_normal();
67 const std::filesystem::path parent = resolved.parent_path();
68 std::error_code parent_ec;
69 const auto parent_status = std::filesystem::status(parent, parent_ec);
70 if (parent_ec || !std::filesystem::exists(parent_status) ||
71 !std::filesystem::is_directory(parent_status)) {
72 return absl::FailedPreconditionError(absl::StrFormat(
73 "Artifact parent directory must already exist: %s%s", parent.string(),
74 !parent_ec ? "" : absl::StrFormat(" (%s)", parent_ec.message())));
75 }
76 return resolved;
77}
78
79absl::StatusOr<bool> PathsAlias(const std::filesystem::path& lhs,
80 const std::filesystem::path& rhs) {
81 ASSIGN_OR_RETURN(const auto normalized_lhs, ResolveStableArtifactPath(lhs));
82 ASSIGN_OR_RETURN(const auto normalized_rhs, ResolveStableArtifactPath(rhs));
83 if (normalized_lhs == normalized_rhs) {
84 return true;
85 }
86
87 std::error_code equivalent_ec;
88 const bool equivalent = std::filesystem::equivalent(
89 normalized_lhs, normalized_rhs, equivalent_ec);
90 if (!equivalent_ec) {
91 return equivalent;
92 }
93
94 // equivalent() reports an error when either path does not exist. Lexical
95 // normalization above is sufficient in that case. If both paths do exist,
96 // fail closed rather than risk truncating a ROM we could not compare.
97 std::error_code lhs_exists_ec;
98 std::error_code rhs_exists_ec;
99 const bool lhs_exists =
100 std::filesystem::exists(normalized_lhs, lhs_exists_ec);
101 const bool rhs_exists =
102 std::filesystem::exists(normalized_rhs, rhs_exists_ec);
103 if (lhs_exists_ec || rhs_exists_ec || (lhs_exists && rhs_exists)) {
104 return absl::FailedPreconditionError(absl::StrFormat(
105 "Could not safely compare paths %s and %s: %s", normalized_lhs.string(),
106 normalized_rhs.string(), equivalent_ec.message()));
107 }
108 return false;
109}
110
112 absl::string_view option_name, const std::filesystem::path& artifact_path,
113 const CommandInvocationContext& invocation_context) {
114 if (invocation_context.active_rom_path.has_value()) {
116 const bool aliases_active_rom,
117 PathsAlias(artifact_path, *invocation_context.active_rom_path));
118 if (aliases_active_rom) {
119 return absl::InvalidArgumentError(absl::StrFormat(
120 "%s path aliases the active ROM; choose a separate artifact file: "
121 "%s",
122 option_name, artifact_path.string()));
123 }
124 }
125
126 if (invocation_context.source_rom_path.has_value()) {
128 const bool aliases_source_rom,
129 PathsAlias(artifact_path, *invocation_context.source_rom_path));
130 if (aliases_source_rom) {
131 return absl::InvalidArgumentError(absl::StrFormat(
132 "%s path aliases the %s; choose a separate artifact file: %s",
133 option_name,
134 invocation_context.sandbox_enabled ? "sandbox source ROM"
135 : "source ROM",
136 artifact_path.string()));
137 }
138 }
139
140 return absl::OkStatus();
141}
142
143absl::Status CommandHandler::Run(const std::vector<std::string>& args,
144 Rom* rom_context,
145 std::string* captured_output) {
146 // 1. Parse arguments
147 ArgumentParser parser(args);
148
149 // 2. Validate arguments
150 auto validation_status = ValidateArgs(parser);
151 if (!validation_status.ok()) {
152 std::cerr << "Error: " << validation_status.message() << "\n\n";
153 std::cerr << "Usage: " << GetUsage() << "\n";
154 return validation_status;
155 }
156
157 // 3. Get format string (output format). Some commands reuse --format for
158 // data formatting (hex/ascii/both). If so, fall back to default output.
159 std::string format_str =
160 parser.GetString("format").value_or(GetDefaultFormat());
161
162 // 4. Create output formatter
163 auto formatter_or = OutputFormatter::FromString(format_str);
164 if (!formatter_or.ok()) {
165 if (format_str == "hex" || format_str == "ascii" || format_str == "both" ||
166 format_str == "binary") {
168 } else {
169 return formatter_or.status();
170 }
171 }
172 OutputFormatter formatter = std::move(formatter_or.value());
173
174 // 5. Setup command context
176 config.external_rom_context = rom_context;
177 config.format = format_str;
178 config.verbose = parser.HasFlag("verbose");
179
180 // Check for --rom override
181 if (auto rom_path = parser.GetString("rom"); rom_path.has_value()) {
182 config.rom_path = *rom_path;
183 }
184
185 // Check for --symbols override
186 if (auto symbols_path = parser.GetString("symbols");
187 symbols_path.has_value()) {
188 config.symbols_path = *symbols_path;
189 }
190
191 // Optional project runtime context used to mirror editor feature/custom object
192 // behavior during CLI execution.
193 if (auto project_path = parser.GetString("project-context");
194 project_path.has_value()) {
195 config.project_context_path = *project_path;
196 }
197
198 // Check for --mock-rom flag
199 config.use_mock_rom = parser.HasFlag("mock-rom");
200
201 CommandContext context(config);
202
203 // 6. Get ROM (loads if needed) - only if command requires it
204 Rom* rom = nullptr;
205 std::optional<Rom> sandbox_rom;
206 bool sandbox_enabled = false;
207 CommandInvocationContext invocation_context;
208
209 // Set symbol provider regardless of ROM loading (it might load its own symbols)
211
212 if (RequiresRom()) {
213 ASSIGN_OR_RETURN(rom, context.GetRom());
214 if (!rom->filename().empty()) {
216 const auto rom_path_identity,
217 CaptureRomPathIdentity(std::filesystem::path(rom->filename())));
218 invocation_context.source_rom_path = rom_path_identity;
219 invocation_context.active_rom_path = rom_path_identity;
220 }
221 SetRomContext(rom);
223
224 if (absl::GetFlag(FLAGS_sandbox) || parser.HasFlag("sandbox")) {
225 sandbox_enabled = true;
226 auto sandbox_or =
228 if (!sandbox_or.ok()) {
229 return sandbox_or.status();
230 }
231 invocation_context.sandbox_enabled = true;
232 if (!invocation_context.source_rom_path.has_value()) {
233 ASSIGN_OR_RETURN(invocation_context.source_rom_path,
234 CaptureRomPathIdentity(
235 std::filesystem::path(sandbox_or->source_rom)));
236 }
237 ASSIGN_OR_RETURN(invocation_context.active_rom_path,
238 CaptureRomPathIdentity(sandbox_or->rom_path));
239 sandbox_rom.emplace();
240 auto load_status =
241 sandbox_rom->LoadFromFile(sandbox_or->rom_path.string());
242 if (!load_status.ok()) {
243 return load_status;
244 }
245 rom = &*sandbox_rom;
246 SetRomContext(rom);
247 }
248
249 // 7. Ensure labels are loaded if required
250 if (RequiresLabels()) {
252 }
253 }
254
255 // 8. Begin output formatting
256 formatter.BeginObject(GetOutputTitle());
257
258 // 9. Execute command business logic
259 auto execute_status =
260 ExecuteWithContext(rom, parser, formatter, invocation_context);
261 if (!execute_status.ok()) {
262 // Preserve structured output for failing commands so callers can inspect
263 // machine-readable diagnostics even when status is non-OK.
264 formatter.EndObject();
265 if (captured_output) {
266 *captured_output = formatter.GetOutput();
267 } else {
268 formatter.Print();
269 }
270 return execute_status;
271 }
272
273 if (sandbox_enabled && rom != nullptr && rom->dirty()) {
274 auto save_status = rom->SaveToFile({.save_new = false});
275 if (!save_status.ok()) {
276 return save_status;
277 }
278 }
279
280 // 10. Finalize and print output
281 formatter.EndObject();
282
283 if (captured_output) {
284 *captured_output = formatter.GetOutput();
285 } else {
286 formatter.Print();
287 }
288
289 return absl::OkStatus();
290}
291
293 Descriptor descriptor;
294 descriptor.display_name = GetName(); // Use GetName() for display.
295 descriptor.summary = "Command summary not provided.";
296 descriptor.todo_reference = "todo#unassigned";
297 return descriptor;
298}
299
300} // namespace resources
301} // namespace cli
302} // namespace yaze
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
auto filename() const
Definition rom.h:175
absl::Status SaveToFile(const SaveSettings &settings)
Definition rom.cc:416
bool dirty() const
Definition rom.h:156
absl::StatusOr< SandboxMetadata > CreateSandbox(Rom &rom, absl::string_view description)
static RomSandboxManager & Instance()
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.
Encapsulates common context for CLI command execution.
absl::StatusOr< Rom * > GetRom()
Get the ROM instance (loads if not already loaded)
absl::Status EnsureLabelsLoaded(Rom *rom)
Ensure resource labels are loaded.
project::YazeProject * GetProjectContext()
Returns loaded project context when –project-context was used.
emu::debug::SymbolProvider * GetSymbolProvider()
Get the SymbolProvider instance.
virtual bool RequiresLabels() const
Check if the command requires ROM labels.
virtual std::string GetUsage() const =0
Get the command usage string.
virtual std::string GetName() const =0
Get the command name.
virtual void SetRomContext(Rom *rom)
Set the ROM context for tools that need ROM access. Default implementation stores the ROM pointer for...
virtual void SetSymbolProvider(emu::debug::SymbolProvider *provider)
Set the SymbolProvider context.
absl::Status Run(const std::vector< std::string > &args, Rom *rom_context, std::string *captured_output=nullptr)
Execute the command.
virtual std::string GetOutputTitle() const
Get the output title for formatting.
virtual std::string GetDefaultFormat() const
Get the default output format ("json" or "text")
virtual bool RequiresRom() const
Check if the command requires a loaded ROM.
virtual void SetProjectContext(project::YazeProject *project)
Set the YazeProject context. Default implementation does nothing, override if tool needs project info...
virtual Descriptor Describe() const
Provide metadata for TUI/help summaries.
virtual absl::Status ExecuteWithContext(Rom *rom, const ArgumentParser &parser, OutputFormatter &formatter, const CommandInvocationContext &invocation_context)
Execute with immutable, invocation-scoped ROM path identity.
virtual absl::Status ValidateArgs(const ArgumentParser &parser)=0
Validate command arguments.
Utility for consistent output formatting across commands.
std::string GetOutput() const
Get the formatted output.
static absl::StatusOr< OutputFormatter > FromString(const std::string &format)
Create formatter from string ("json" or "text")
void BeginObject(const std::string &title="")
Start a JSON object or text section.
void EndObject()
End a JSON object or text section.
void Print() const
Print the formatted output to stdout.
ABSL_DECLARE_FLAG(bool, sandbox)
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
absl::StatusOr< std::filesystem::path > CaptureRomPathIdentity(const std::filesystem::path &path)
absl::StatusOr< std::filesystem::path > ResolveStableArtifactPath(const std::filesystem::path &path)
absl::Status RejectArtifactRomAliases(absl::string_view option_name, const std::filesystem::path &artifact_path, const CommandInvocationContext &invocation_context)
absl::StatusOr< bool > PathsAlias(const std::filesystem::path &lhs, const std::filesystem::path &rhs)
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
Configuration for command context.
std::optional< std::string > project_context_path
std::optional< std::string > symbols_path
std::optional< std::filesystem::path > active_rom_path
std::optional< std::filesystem::path > source_rom_path