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 return path_str.find(root_str) == 0;
108}
109
110std::string FileSystemToolBase::FormatFileSize(uintmax_t size_bytes) const {
111 const char* units[] = {"B", "KB", "MB", "GB", "TB"};
112 int unit_index = 0;
113 double size = static_cast<double>(size_bytes);
114
115 while (size >= 1024.0 && unit_index < 4) {
116 size /= 1024.0;
117 unit_index++;
118 }
119
120 if (unit_index == 0) {
121 return absl::StrFormat("%d %s", static_cast<int>(size), units[unit_index]);
122 } else {
123 return absl::StrFormat("%.2f %s", size, units[unit_index]);
124 }
125}
126
128 const fs::file_time_type& time) const {
129 // Convert file_time_type to system_clock time_point
130 auto sctp = std::chrono::time_point_cast<std::chrono::system_clock::duration>(
131 time - fs::file_time_type::clock::now() +
132 std::chrono::system_clock::now());
133
134 // Convert to time_t for formatting
135 std::time_t tt = std::chrono::system_clock::to_time_t(sctp);
136
137 // Format the time
138 std::stringstream ss;
139 ss << std::put_time(std::localtime(&tt), "%Y-%m-%d %H:%M:%S");
140 return ss.str();
141}
142
143// ============================================================================
144// FileSystemListTool Implementation
145// ============================================================================
146
148 const resources::ArgumentParser& parser) {
149 return parser.RequireArgs({"path"});
150}
151
153 Rom* rom, const resources::ArgumentParser& parser,
154 resources::OutputFormatter& formatter) {
155
156 auto path_str = parser.GetString("path").value_or(".");
157 bool recursive = parser.HasFlag("recursive");
158
159 // Validate and normalize the path
160 auto path_result = ValidatePath(path_str);
161 if (!path_result.ok()) {
162 return path_result.status();
163 }
164 fs::path dir_path = *path_result;
165
166 // Check if the path exists and is a directory
167 std::error_code ec;
168 if (!fs::exists(dir_path, ec)) {
169 return absl::NotFoundError(
170 absl::StrCat("Directory not found: ", dir_path.string()));
171 }
172
173 if (!fs::is_directory(dir_path, ec)) {
174 return absl::InvalidArgumentError(
175 absl::StrCat("Path is not a directory: ", dir_path.string()));
176 }
177
178 formatter.BeginObject("Directory Listing");
179 formatter.AddField("path", dir_path.string());
180 formatter.AddField("recursive", recursive ? "true" : "false");
181
182 std::vector<std::map<std::string, std::string>> entries;
183
184 // List directory contents
185 if (recursive) {
186 for (const auto& entry : fs::recursive_directory_iterator(
187 dir_path, fs::directory_options::skip_permission_denied, ec)) {
188 if (ec) {
189 continue; // Skip inaccessible entries
190 }
191
192 std::map<std::string, std::string> file_info;
193 file_info["name"] = entry.path().filename().string();
194 file_info["path"] = fs::relative(entry.path(), dir_path).string();
195 file_info["type"] = entry.is_directory() ? "directory" : "file";
196
197 if (entry.is_regular_file()) {
198 file_info["size"] = FormatFileSize(entry.file_size());
199 }
200
201 entries.push_back(file_info);
202 }
203 } else {
204 for (const auto& entry : fs::directory_iterator(
205 dir_path, fs::directory_options::skip_permission_denied, ec)) {
206 if (ec) {
207 continue; // Skip inaccessible entries
208 }
209
210 std::map<std::string, std::string> file_info;
211 file_info["name"] = entry.path().filename().string();
212 file_info["type"] = entry.is_directory() ? "directory" : "file";
213
214 if (entry.is_regular_file()) {
215 file_info["size"] = FormatFileSize(entry.file_size());
216 }
217
218 entries.push_back(file_info);
219 }
220 }
221
222 // Sort entries: directories first, then files, alphabetically
223 std::sort(entries.begin(), entries.end(),
224 [](const auto& a, const auto& b) {
225 if (a.at("type") != b.at("type")) {
226 return a.at("type") == "directory";
227 }
228 return a.at("name") < b.at("name");
229 });
230
231 // Add entries to formatter
232 formatter.BeginArray("entries");
233 for (const auto& entry : entries) {
234 formatter.BeginObject();
235 for (const auto& [key, value] : entry) {
236 formatter.AddField(key, value);
237 }
238 formatter.EndObject();
239 }
240 formatter.EndArray();
241
242 formatter.AddField("total_entries", std::to_string(entries.size()));
243 formatter.EndObject();
244
245 return absl::OkStatus();
246}
247
248// ============================================================================
249// FileSystemReadTool Implementation
250// ============================================================================
251
253 const resources::ArgumentParser& parser) {
254 return parser.RequireArgs({"path"});
255}
256
258 Rom* rom, const resources::ArgumentParser& parser,
259 resources::OutputFormatter& formatter) {
260
261 auto path_str = parser.GetString("path").value();
262 int max_lines = parser.GetInt("lines").value_or(-1);
263 int offset = parser.GetInt("offset").value_or(0);
264
265 // Validate and normalize the path
266 auto path_result = ValidatePath(path_str);
267 if (!path_result.ok()) {
268 return path_result.status();
269 }
270 fs::path file_path = *path_result;
271
272 // Check if the file exists and is a regular file
273 std::error_code ec;
274 if (!fs::exists(file_path, ec)) {
275 return absl::NotFoundError(
276 absl::StrCat("File not found: ", file_path.string()));
277 }
278
279 if (!fs::is_regular_file(file_path, ec)) {
280 return absl::InvalidArgumentError(
281 absl::StrCat("Path is not a file: ", file_path.string()));
282 }
283
284 // Check if it's a text file
285 if (!IsTextFile(file_path)) {
286 return absl::InvalidArgumentError(
287 absl::StrCat("File appears to be binary: ", file_path.string(),
288 ". Only text files can be read."));
289 }
290
291 // Read the file
292 std::ifstream file(file_path, std::ios::in);
293 if (!file) {
294 return absl::InternalError(
295 absl::StrCat("Failed to open file: ", file_path.string()));
296 }
297
298 formatter.BeginObject("File Contents");
299 formatter.AddField("path", file_path.string());
300 formatter.AddField("size", FormatFileSize(fs::file_size(file_path)));
301
302 std::vector<std::string> lines;
303 std::string line;
304 int line_num = 0;
305
306 // Skip to offset
307 while (line_num < offset && std::getline(file, line)) {
308 line_num++;
309 }
310
311 // Read lines
312 while (std::getline(file, line)) {
313 if (max_lines > 0 && lines.size() >= static_cast<size_t>(max_lines)) {
314 break;
315 }
316 lines.push_back(line);
317 }
318
319 formatter.AddField("lines_read", std::to_string(lines.size()));
320 formatter.AddField("starting_line", std::to_string(offset + 1));
321
322 // Add content
323 if (parser.GetString("format").value_or("text") == "json") {
324 formatter.BeginArray("content");
325 for (const auto& content_line : lines) {
326 formatter.AddArrayItem(content_line);
327 }
328 formatter.EndArray();
329 } else {
330 std::stringstream content;
331 for (size_t i = 0; i < lines.size(); ++i) {
332 content << lines[i];
333 if (i < lines.size() - 1) {
334 content << "\n";
335 }
336 }
337 formatter.AddField("content", content.str());
338 }
339
340 formatter.EndObject();
341
342 return absl::OkStatus();
343}
344
345bool FileSystemReadTool::IsTextFile(const fs::path& path) const {
346 // Check file extension first
347 std::string ext = path.extension().string();
348 std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
349
350 // Common text file extensions
351 std::set<std::string> text_extensions = {
352 ".txt", ".md", ".cc", ".cpp", ".c", ".h", ".hpp", ".py", ".js", ".ts",
353 ".json", ".xml", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".conf",
354 ".sh", ".bash", ".zsh", ".fish", ".cmake", ".mk", ".makefile",
355 ".html", ".css", ".scss", ".sass", ".less", ".jsx", ".tsx",
356 ".rs", ".go", ".java", ".kt", ".swift", ".rb", ".pl", ".php",
357 ".lua", ".vim", ".el", ".lisp", ".clj", ".hs", ".ml", ".fs",
358 ".asm", ".s", ".S", ".proto", ".thrift", ".graphql", ".sql",
359 ".gitignore", ".dockerignore", ".editorconfig", ".eslintrc"
360 };
361
362 if (text_extensions.count(ext) > 0) {
363 return true;
364 }
365
366 // For unknown extensions, check the first few bytes
367 std::ifstream file(path, std::ios::binary);
368 if (!file) {
369 return false;
370 }
371
372 // Read first 512 bytes to check for binary content
373 char buffer[512];
374 file.read(buffer, sizeof(buffer));
375 std::streamsize bytes_read = file.gcount();
376
377 // Check for null bytes (common in binary files)
378 for (std::streamsize i = 0; i < bytes_read; ++i) {
379 if (buffer[i] == '\0') {
380 return false; // Binary file
381 }
382 // Also check for other non-printable characters
383 // (excluding common whitespace)
384 if (!std::isprint(buffer[i]) &&
385 buffer[i] != '\n' &&
386 buffer[i] != '\r' &&
387 buffer[i] != '\t') {
388 return false;
389 }
390 }
391
392 return true;
393}
394
395// ============================================================================
396// FileSystemExistsTool Implementation
397// ============================================================================
398
400 const resources::ArgumentParser& parser) {
401 return parser.RequireArgs({"path"});
402}
403
405 Rom* rom, const resources::ArgumentParser& parser,
406 resources::OutputFormatter& formatter) {
407
408 auto path_str = parser.GetString("path").value();
409
410 // Validate and normalize the path
411 auto path_result = ValidatePath(path_str);
412 if (!path_result.ok()) {
413 // For exists check, we want to handle permission denied specially
414 if (absl::IsPermissionDenied(path_result.status())) {
415 return path_result.status();
416 }
417 // Other errors might mean the file doesn't exist
418 formatter.BeginObject("File Exists Check");
419 formatter.AddField("path", path_str);
420 formatter.AddField("exists", "false");
421 formatter.AddField("error", std::string(path_result.status().message()));
422 formatter.EndObject();
423 return absl::OkStatus();
424 }
425
426 fs::path check_path = *path_result;
427 std::error_code ec;
428 bool exists = fs::exists(check_path, ec);
429
430 formatter.BeginObject("File Exists Check");
431 formatter.AddField("path", check_path.string());
432 formatter.AddField("exists", exists ? "true" : "false");
433
434 if (exists) {
435 if (fs::is_directory(check_path, ec)) {
436 formatter.AddField("type", "directory");
437 } else if (fs::is_regular_file(check_path, ec)) {
438 formatter.AddField("type", "file");
439 } else if (fs::is_symlink(check_path, ec)) {
440 formatter.AddField("type", "symlink");
441 } else {
442 formatter.AddField("type", "other");
443 }
444 }
445
446 formatter.EndObject();
447
448 return absl::OkStatus();
449}
450
451// ============================================================================
452// FileSystemInfoTool Implementation
453// ============================================================================
454
456 const resources::ArgumentParser& parser) {
457 return parser.RequireArgs({"path"});
458}
459
461 Rom* rom, const resources::ArgumentParser& parser,
462 resources::OutputFormatter& formatter) {
463
464 auto path_str = parser.GetString("path").value();
465
466 // Validate and normalize the path
467 auto path_result = ValidatePath(path_str);
468 if (!path_result.ok()) {
469 return path_result.status();
470 }
471 fs::path info_path = *path_result;
472
473 // Check if the path exists
474 std::error_code ec;
475 if (!fs::exists(info_path, ec)) {
476 return absl::NotFoundError(
477 absl::StrCat("Path not found: ", info_path.string()));
478 }
479
480 formatter.BeginObject("File Information");
481 formatter.AddField("path", info_path.string());
482 formatter.AddField("name", info_path.filename().string());
483 formatter.AddField("parent", info_path.parent_path().string());
484
485 // Type
486 if (fs::is_directory(info_path, ec)) {
487 formatter.AddField("type", "directory");
488
489 // Count entries in directory
490 size_t entry_count = 0;
491 for (auto& _ : fs::directory_iterator(info_path, ec)) {
492 entry_count++;
493 }
494 formatter.AddField("entries", std::to_string(entry_count));
495 } else if (fs::is_regular_file(info_path, ec)) {
496 formatter.AddField("type", "file");
497 formatter.AddField("extension", info_path.extension().string());
498
499 // File size
500 auto size = fs::file_size(info_path, ec);
501 formatter.AddField("size_bytes", std::to_string(size));
502 formatter.AddField("size", FormatFileSize(size));
503 } else if (fs::is_symlink(info_path, ec)) {
504 formatter.AddField("type", "symlink");
505 auto target = fs::read_symlink(info_path, ec);
506 if (!ec) {
507 formatter.AddField("target", target.string());
508 }
509 } else {
510 formatter.AddField("type", "other");
511 }
512
513 // Timestamps
514 auto last_write = fs::last_write_time(info_path, ec);
515 if (!ec) {
516 formatter.AddField("modified", FormatTimestamp(last_write));
517 }
518
519 // Permissions
520 formatter.AddField("permissions", GetPermissionString(info_path));
521
522 // Additional info
523 formatter.AddField("absolute_path", fs::absolute(info_path).string());
524 formatter.AddField("is_hidden",
525 info_path.filename().string().starts_with(".") ? "true" : "false");
526
527 formatter.EndObject();
528
529 return absl::OkStatus();
530}
531
533 const fs::path& path) const {
534 std::error_code ec;
535 auto perms = fs::status(path, ec).permissions();
536
537 if (ec) {
538 return "unknown";
539 }
540
541 std::string result;
542
543 // Owner permissions
544 result += (perms & fs::perms::owner_read) != fs::perms::none ? 'r' : '-';
545 result += (perms & fs::perms::owner_write) != fs::perms::none ? 'w' : '-';
546 result += (perms & fs::perms::owner_exec) != fs::perms::none ? 'x' : '-';
547
548 // Group permissions
549 result += (perms & fs::perms::group_read) != fs::perms::none ? 'r' : '-';
550 result += (perms & fs::perms::group_write) != fs::perms::none ? 'w' : '-';
551 result += (perms & fs::perms::group_exec) != fs::perms::none ? 'x' : '-';
552
553 // Others permissions
554 result += (perms & fs::perms::others_read) != fs::perms::none ? 'r' : '-';
555 result += (perms & fs::perms::others_write) != fs::perms::none ? 'w' : '-';
556 result += (perms & fs::perms::others_exec) != fs::perms::none ? 'x' : '-';
557
558 return result;
559}
560
561} // namespace tools
562} // namespace agent
563} // namespace cli
564} // 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:24
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.