yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
filesystem_tool.cc
Go to the documentation of this file.
2
3#include <chrono>
4#include <fstream>
5#include <iomanip>
6#include <sstream>
7
8#include "absl/strings/str_cat.h"
9#include "absl/strings/str_format.h"
10#include "absl/strings/str_split.h"
11
12namespace yaze {
13namespace cli {
14namespace agent {
15namespace tools {
16
17namespace fs = std::filesystem;
18
19// ============================================================================
20// FileSystemToolBase Implementation
21// ============================================================================
22
23absl::StatusOr<fs::path> FileSystemToolBase::ValidatePath(
24 const std::string& path_str) const {
25 if (path_str.empty()) {
26 return absl::InvalidArgumentError("Path cannot be empty");
27 }
28
29 // Check for path traversal attempts
30 if (path_str.find("..") != std::string::npos) {
31 return absl::InvalidArgumentError(
32 "Path traversal (..) is not allowed for security reasons");
33 }
34
35 fs::path path;
36 std::error_code ec;
37
38 // Convert to absolute path
39 if (fs::path(path_str).is_relative()) {
40 path = fs::absolute(GetProjectRoot() / path_str, ec);
41 } else {
42 path = fs::absolute(path_str, ec);
43 }
44
45 if (ec) {
46 return absl::InvalidArgumentError(
47 absl::StrCat("Failed to resolve path: ", ec.message()));
48 }
49
50 // Normalize the path (resolve symlinks, remove redundant separators)
51 path = fs::canonical(path, ec);
52 if (ec && ec != std::errc::no_such_file_or_directory) {
53 // Allow non-existent files for exists checks
54 path = fs::weakly_canonical(path, ec);
55 if (ec) {
56 return absl::InvalidArgumentError(
57 absl::StrCat("Failed to normalize path: ", ec.message()));
58 }
59 }
60
61 // Verify the path is within the project directory
62 if (!IsPathInProject(path)) {
63 return absl::PermissionDeniedError(
64 absl::StrCat("Access denied: Path '", path.string(),
65 "' is outside the project directory"));
66 }
67
68 return path;
69}
70
72 // Look for common project markers to find the root
73 fs::path current = fs::current_path();
74 fs::path root = current;
75
76 // Walk up the directory tree looking for project markers
77 while (!root.empty() && root != root.root_path()) {
78 // Check for yaze-specific markers
79 if (fs::exists(root / "CMakeLists.txt") &&
80 fs::exists(root / "src" / "yaze.cc")) {
81 return root;
82 }
83 // Also check for .git directory as a fallback
84 if (fs::exists(root / ".git")) {
85 // Verify this is the yaze project
86 if (fs::exists(root / "src" / "cli") &&
87 fs::exists(root / "src" / "app")) {
88 return root;
89 }
90 }
91 root = root.parent_path();
92 }
93
94 // Default to current directory if project root not found
95 return current;
96}
97
98bool FileSystemToolBase::IsPathInProject(const fs::path& path) const {
99 fs::path project_root = GetProjectRoot();
100 fs::path normalized_path = fs::weakly_canonical(path);
101 fs::path normalized_root = fs::canonical(project_root);
102
103 // Check if path starts with project root
104 auto path_str = normalized_path.string();
105 auto root_str = normalized_root.string();
106
107#ifdef __EMSCRIPTEN__
108 // Allow paths that are exactly "/.yaze" or start with "/.yaze/"
109 // This prevents paths like "/.yazeevil" from bypassing the guard
110 if (path_str == "/.yaze" || path_str.rfind("/.yaze/", 0) == 0) {
111 return true;
112 }
113#endif
114
115 return path_str.find(root_str) == 0;
116}
117
118std::string FileSystemToolBase::FormatFileSize(uintmax_t size_bytes) const {
119 const char* units[] = {"B", "KB", "MB", "GB", "TB"};
120 int unit_index = 0;
121 double size = static_cast<double>(size_bytes);
122
123 while (size >= 1024.0 && unit_index < 4) {
124 size /= 1024.0;
125 unit_index++;
126 }
127
128 if (unit_index == 0) {
129 return absl::StrFormat("%d %s", static_cast<int>(size), units[unit_index]);
130 } else {
131 return absl::StrFormat("%.2f %s", size, units[unit_index]);
132 }
133}
134
136 const fs::file_time_type& time) const {
137 // Convert file_time_type to system_clock time_point
138 auto sctp = std::chrono::time_point_cast<std::chrono::system_clock::duration>(
139 time - fs::file_time_type::clock::now() +
140 std::chrono::system_clock::now());
141
142 // Convert to time_t for formatting
143 std::time_t tt = std::chrono::system_clock::to_time_t(sctp);
144
145 // Format the time
146 std::stringstream ss;
147 ss << std::put_time(std::localtime(&tt), "%Y-%m-%d %H:%M:%S");
148 return ss.str();
149}
150
151// ============================================================================
152// FileSystemListTool Implementation
153// ============================================================================
154
156 const resources::ArgumentParser& parser) {
157 return parser.RequireArgs({"path"});
158}
159
161 Rom* rom, const resources::ArgumentParser& parser,
162 resources::OutputFormatter& formatter) {
163
164 auto path_str = parser.GetString("path").value_or(".");
165 bool recursive = parser.HasFlag("recursive");
166
167 // Validate and normalize the path
168 auto path_result = ValidatePath(path_str);
169 if (!path_result.ok()) {
170 return path_result.status();
171 }
172 fs::path dir_path = *path_result;
173
174 // Check if the path exists and is a directory
175 std::error_code ec;
176 if (!fs::exists(dir_path, ec)) {
177 return absl::NotFoundError(
178 absl::StrCat("Directory not found: ", dir_path.string()));
179 }
180
181 if (!fs::is_directory(dir_path, ec)) {
182 return absl::InvalidArgumentError(
183 absl::StrCat("Path is not a directory: ", dir_path.string()));
184 }
185
186 formatter.BeginObject("Directory Listing");
187 formatter.AddField("path", dir_path.string());
188 formatter.AddField("recursive", recursive ? "true" : "false");
189
190 std::vector<std::map<std::string, std::string>> entries;
191
192 // List directory contents
193 if (recursive) {
194 for (const auto& entry : fs::recursive_directory_iterator(
195 dir_path, fs::directory_options::skip_permission_denied, ec)) {
196 if (ec) {
197 continue; // Skip inaccessible entries
198 }
199
200 std::map<std::string, std::string> file_info;
201 file_info["name"] = entry.path().filename().string();
202 file_info["path"] = fs::relative(entry.path(), dir_path).string();
203 file_info["type"] = entry.is_directory() ? "directory" : "file";
204
205 if (entry.is_regular_file()) {
206 file_info["size"] = FormatFileSize(entry.file_size());
207 }
208
209 entries.push_back(file_info);
210 }
211 } else {
212 for (const auto& entry : fs::directory_iterator(
213 dir_path, fs::directory_options::skip_permission_denied, ec)) {
214 if (ec) {
215 continue; // Skip inaccessible entries
216 }
217
218 std::map<std::string, std::string> file_info;
219 file_info["name"] = entry.path().filename().string();
220 file_info["type"] = entry.is_directory() ? "directory" : "file";
221
222 if (entry.is_regular_file()) {
223 file_info["size"] = FormatFileSize(entry.file_size());
224 }
225
226 entries.push_back(file_info);
227 }
228 }
229
230 // Sort entries: directories first, then files, alphabetically
231 std::sort(entries.begin(), entries.end(), [](const auto& a, const auto& b) {
232 if (a.at("type") != b.at("type")) {
233 return a.at("type") == "directory";
234 }
235 return a.at("name") < b.at("name");
236 });
237
238 // Add entries to formatter
239 formatter.BeginArray("entries");
240 for (const auto& entry : entries) {
241 formatter.BeginObject();
242 for (const auto& [key, value] : entry) {
243 formatter.AddField(key, value);
244 }
245 formatter.EndObject();
246 }
247 formatter.EndArray();
248
249 formatter.AddField("total_entries", std::to_string(entries.size()));
250 formatter.EndObject();
251
252 return absl::OkStatus();
253}
254
255// ============================================================================
256// FileSystemReadTool Implementation
257// ============================================================================
258
260 const resources::ArgumentParser& parser) {
261 return parser.RequireArgs({"path"});
262}
263
265 Rom* rom, const resources::ArgumentParser& parser,
266 resources::OutputFormatter& formatter) {
267
268 auto path_str = parser.GetString("path").value();
269 int max_lines = parser.GetInt("lines").value_or(-1);
270 int offset = parser.GetInt("offset").value_or(0);
271
272 // Validate and normalize the path
273 auto path_result = ValidatePath(path_str);
274 if (!path_result.ok()) {
275 return path_result.status();
276 }
277 fs::path file_path = *path_result;
278
279 // Check if the file exists and is a regular file
280 std::error_code ec;
281 if (!fs::exists(file_path, ec)) {
282 return absl::NotFoundError(
283 absl::StrCat("File not found: ", file_path.string()));
284 }
285
286 if (!fs::is_regular_file(file_path, ec)) {
287 return absl::InvalidArgumentError(
288 absl::StrCat("Path is not a file: ", file_path.string()));
289 }
290
291 // Check if it's a text file
292 if (!IsTextFile(file_path)) {
293 return absl::InvalidArgumentError(
294 absl::StrCat("File appears to be binary: ", file_path.string(),
295 ". Only text files can be read."));
296 }
297
298 // Read the file
299 std::ifstream file(file_path, std::ios::in);
300 if (!file) {
301 return absl::InternalError(
302 absl::StrCat("Failed to open file: ", file_path.string()));
303 }
304
305 formatter.BeginObject("File Contents");
306 formatter.AddField("path", file_path.string());
307 formatter.AddField("size", FormatFileSize(fs::file_size(file_path)));
308
309 std::vector<std::string> lines;
310 std::string line;
311 int line_num = 0;
312
313 // Skip to offset
314 while (line_num < offset && std::getline(file, line)) {
315 line_num++;
316 }
317
318 // Read lines
319 while (std::getline(file, line)) {
320 if (max_lines > 0 && lines.size() >= static_cast<size_t>(max_lines)) {
321 break;
322 }
323 lines.push_back(line);
324 }
325
326 formatter.AddField("lines_read", std::to_string(lines.size()));
327 formatter.AddField("starting_line", std::to_string(offset + 1));
328
329 // Add content
330 if (parser.GetString("format").value_or("text") == "json") {
331 formatter.BeginArray("content");
332 for (const auto& content_line : lines) {
333 formatter.AddArrayItem(content_line);
334 }
335 formatter.EndArray();
336 } else {
337 std::stringstream content;
338 for (size_t i = 0; i < lines.size(); ++i) {
339 content << lines[i];
340 if (i < lines.size() - 1) {
341 content << "\n";
342 }
343 }
344 formatter.AddField("content", content.str());
345 }
346
347 formatter.EndObject();
348
349 return absl::OkStatus();
350}
351
352bool FileSystemReadTool::IsTextFile(const fs::path& path) const {
353 // Check file extension first
354 std::string ext = path.extension().string();
355 std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
356
357 // Common text file extensions
358 std::set<std::string> text_extensions = {
359 ".txt", ".md", ".cc", ".cpp", ".c",
360 ".h", ".hpp", ".py", ".js", ".ts",
361 ".json", ".xml", ".yaml", ".yml", ".toml",
362 ".ini", ".cfg", ".conf", ".sh", ".bash",
363 ".zsh", ".fish", ".cmake", ".mk", ".makefile",
364 ".html", ".css", ".scss", ".sass", ".less",
365 ".jsx", ".tsx", ".rs", ".go", ".java",
366 ".kt", ".swift", ".rb", ".pl", ".php",
367 ".lua", ".vim", ".el", ".lisp", ".clj",
368 ".hs", ".ml", ".fs", ".asm", ".s",
369 ".S", ".proto", ".thrift", ".graphql", ".sql",
370 ".gitignore", ".dockerignore", ".editorconfig", ".eslintrc"};
371
372 if (text_extensions.count(ext) > 0) {
373 return true;
374 }
375
376 // For unknown extensions, check the first few bytes
377 std::ifstream file(path, std::ios::binary);
378 if (!file) {
379 return false;
380 }
381
382 // Read first 512 bytes to check for binary content
383 char buffer[512];
384 file.read(buffer, sizeof(buffer));
385 std::streamsize bytes_read = file.gcount();
386
387 // Check for null bytes (common in binary files)
388 for (std::streamsize i = 0; i < bytes_read; ++i) {
389 if (buffer[i] == '\0') {
390 return false; // Binary file
391 }
392 // Also check for other non-printable characters
393 // (excluding common whitespace)
394 if (!std::isprint(buffer[i]) && buffer[i] != '\n' && buffer[i] != '\r' &&
395 buffer[i] != '\t') {
396 return false;
397 }
398 }
399
400 return true;
401}
402
403// ============================================================================
404// FileSystemExistsTool Implementation
405// ============================================================================
406
408 const resources::ArgumentParser& parser) {
409 return parser.RequireArgs({"path"});
410}
411
413 Rom* rom, const resources::ArgumentParser& parser,
414 resources::OutputFormatter& formatter) {
415
416 auto path_str = parser.GetString("path").value();
417
418 // Validate and normalize the path
419 auto path_result = ValidatePath(path_str);
420 if (!path_result.ok()) {
421 // For exists check, we want to handle permission denied specially
422 if (absl::IsPermissionDenied(path_result.status())) {
423 return path_result.status();
424 }
425 // Other errors might mean the file doesn't exist
426 formatter.BeginObject("File Exists Check");
427 formatter.AddField("path", path_str);
428 formatter.AddField("exists", "false");
429 formatter.AddField("error", std::string(path_result.status().message()));
430 formatter.EndObject();
431 return absl::OkStatus();
432 }
433
434 fs::path check_path = *path_result;
435 std::error_code ec;
436 bool exists = fs::exists(check_path, ec);
437
438 formatter.BeginObject("File Exists Check");
439 formatter.AddField("path", check_path.string());
440 formatter.AddField("exists", exists ? "true" : "false");
441
442 if (exists) {
443 if (fs::is_directory(check_path, ec)) {
444 formatter.AddField("type", "directory");
445 } else if (fs::is_regular_file(check_path, ec)) {
446 formatter.AddField("type", "file");
447 } else if (fs::is_symlink(check_path, ec)) {
448 formatter.AddField("type", "symlink");
449 } else {
450 formatter.AddField("type", "other");
451 }
452 }
453
454 formatter.EndObject();
455
456 return absl::OkStatus();
457}
458
459// ============================================================================
460// FileSystemInfoTool Implementation
461// ============================================================================
462
464 const resources::ArgumentParser& parser) {
465 return parser.RequireArgs({"path"});
466}
467
469 Rom* rom, const resources::ArgumentParser& parser,
470 resources::OutputFormatter& formatter) {
471
472 auto path_str = parser.GetString("path").value();
473
474 // Validate and normalize the path
475 auto path_result = ValidatePath(path_str);
476 if (!path_result.ok()) {
477 return path_result.status();
478 }
479 fs::path info_path = *path_result;
480
481 // Check if the path exists
482 std::error_code ec;
483 if (!fs::exists(info_path, ec)) {
484 return absl::NotFoundError(
485 absl::StrCat("Path not found: ", info_path.string()));
486 }
487
488 formatter.BeginObject("File Information");
489 formatter.AddField("path", info_path.string());
490 formatter.AddField("name", info_path.filename().string());
491 formatter.AddField("parent", info_path.parent_path().string());
492
493 // Type
494 if (fs::is_directory(info_path, ec)) {
495 formatter.AddField("type", "directory");
496
497 // Count entries in directory
498 size_t entry_count = 0;
499 for (auto& _ : fs::directory_iterator(info_path, ec)) {
500 entry_count++;
501 }
502 formatter.AddField("entries", std::to_string(entry_count));
503 } else if (fs::is_regular_file(info_path, ec)) {
504 formatter.AddField("type", "file");
505 formatter.AddField("extension", info_path.extension().string());
506
507 // File size
508 auto size = fs::file_size(info_path, ec);
509 formatter.AddField("size_bytes", std::to_string(size));
510 formatter.AddField("size", FormatFileSize(size));
511 } else if (fs::is_symlink(info_path, ec)) {
512 formatter.AddField("type", "symlink");
513 auto target = fs::read_symlink(info_path, ec);
514 if (!ec) {
515 formatter.AddField("target", target.string());
516 }
517 } else {
518 formatter.AddField("type", "other");
519 }
520
521 // Timestamps
522 auto last_write = fs::last_write_time(info_path, ec);
523 if (!ec) {
524 formatter.AddField("modified", FormatTimestamp(last_write));
525 }
526
527 // Permissions
528 formatter.AddField("permissions", GetPermissionString(info_path));
529
530 // Additional info
531 formatter.AddField("absolute_path", fs::absolute(info_path).string());
532 formatter.AddField("is_hidden", info_path.filename().string().starts_with(".")
533 ? "true"
534 : "false");
535
536 formatter.EndObject();
537
538 return absl::OkStatus();
539}
540
542 const fs::path& path) const {
543 std::error_code ec;
544 auto perms = fs::status(path, ec).permissions();
545
546 if (ec) {
547 return "unknown";
548 }
549
550 std::string result;
551
552 // Owner permissions
553 result += (perms & fs::perms::owner_read) != fs::perms::none ? 'r' : '-';
554 result += (perms & fs::perms::owner_write) != fs::perms::none ? 'w' : '-';
555 result += (perms & fs::perms::owner_exec) != fs::perms::none ? 'x' : '-';
556
557 // Group permissions
558 result += (perms & fs::perms::group_read) != fs::perms::none ? 'r' : '-';
559 result += (perms & fs::perms::group_write) != fs::perms::none ? 'w' : '-';
560 result += (perms & fs::perms::group_exec) != fs::perms::none ? 'x' : '-';
561
562 // Others permissions
563 result += (perms & fs::perms::others_read) != fs::perms::none ? 'r' : '-';
564 result += (perms & fs::perms::others_write) != fs::perms::none ? 'w' : '-';
565 result += (perms & fs::perms::others_exec) != fs::perms::none ? 'x' : '-';
566
567 return result;
568}
569
570} // namespace tools
571} // namespace agent
572} // namespace cli
573} // 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
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
std::string GetPermissionString(const std::filesystem::path &path) const
Get permission string (Unix-style: rwxrwxrwx)
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
bool IsTextFile(const std::filesystem::path &path) const
Check if a file is likely text (not binary)
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
std::string FormatTimestamp(const std::filesystem::file_time_type &time) const
Format timestamp for readable output.
std::filesystem::path GetProjectRoot() const
Get the project root directory.
std::string FormatFileSize(uintmax_t size_bytes) const
Format file size for human-readable output.
absl::StatusOr< std::filesystem::path > ValidatePath(const std::string &path_str) const
Validate and normalize a path for safe access.
bool IsPathInProject(const std::filesystem::path &path) const
Check if a path is within the project directory.
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.
absl::Status RequireArgs(const std::vector< std::string > &required) const
Validate that required arguments are present.
absl::StatusOr< int > GetInt(const std::string &name) const
Parse an integer argument (supports hex with 0x prefix)
Utility for consistent output formatting across commands.
void BeginArray(const std::string &key)
Begin an array.
void AddArrayItem(const std::string &item)
Add an item to current 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.