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(), [](const auto& a, const auto& b) {
224 if (a.at("type") != b.at("type")) {
225 return a.at("type") == "directory";
226 }
227 return a.at("name") < b.at("name");
228 });
229
230 // Add entries to formatter
231 formatter.BeginArray("entries");
232 for (const auto& entry : entries) {
233 formatter.BeginObject();
234 for (const auto& [key, value] : entry) {
235 formatter.AddField(key, value);
236 }
237 formatter.EndObject();
238 }
239 formatter.EndArray();
240
241 formatter.AddField("total_entries", std::to_string(entries.size()));
242 formatter.EndObject();
243
244 return absl::OkStatus();
245}
246
247// ============================================================================
248// FileSystemReadTool Implementation
249// ============================================================================
250
252 const resources::ArgumentParser& parser) {
253 return parser.RequireArgs({"path"});
254}
255
257 Rom* rom, const resources::ArgumentParser& parser,
258 resources::OutputFormatter& formatter) {
259
260 auto path_str = parser.GetString("path").value();
261 int max_lines = parser.GetInt("lines").value_or(-1);
262 int offset = parser.GetInt("offset").value_or(0);
263
264 // Validate and normalize the path
265 auto path_result = ValidatePath(path_str);
266 if (!path_result.ok()) {
267 return path_result.status();
268 }
269 fs::path file_path = *path_result;
270
271 // Check if the file exists and is a regular file
272 std::error_code ec;
273 if (!fs::exists(file_path, ec)) {
274 return absl::NotFoundError(
275 absl::StrCat("File not found: ", file_path.string()));
276 }
277
278 if (!fs::is_regular_file(file_path, ec)) {
279 return absl::InvalidArgumentError(
280 absl::StrCat("Path is not a file: ", file_path.string()));
281 }
282
283 // Check if it's a text file
284 if (!IsTextFile(file_path)) {
285 return absl::InvalidArgumentError(
286 absl::StrCat("File appears to be binary: ", file_path.string(),
287 ". Only text files can be read."));
288 }
289
290 // Read the file
291 std::ifstream file(file_path, std::ios::in);
292 if (!file) {
293 return absl::InternalError(
294 absl::StrCat("Failed to open file: ", file_path.string()));
295 }
296
297 formatter.BeginObject("File Contents");
298 formatter.AddField("path", file_path.string());
299 formatter.AddField("size", FormatFileSize(fs::file_size(file_path)));
300
301 std::vector<std::string> lines;
302 std::string line;
303 int line_num = 0;
304
305 // Skip to offset
306 while (line_num < offset && std::getline(file, line)) {
307 line_num++;
308 }
309
310 // Read lines
311 while (std::getline(file, line)) {
312 if (max_lines > 0 && lines.size() >= static_cast<size_t>(max_lines)) {
313 break;
314 }
315 lines.push_back(line);
316 }
317
318 formatter.AddField("lines_read", std::to_string(lines.size()));
319 formatter.AddField("starting_line", std::to_string(offset + 1));
320
321 // Add content
322 if (parser.GetString("format").value_or("text") == "json") {
323 formatter.BeginArray("content");
324 for (const auto& content_line : lines) {
325 formatter.AddArrayItem(content_line);
326 }
327 formatter.EndArray();
328 } else {
329 std::stringstream content;
330 for (size_t i = 0; i < lines.size(); ++i) {
331 content << lines[i];
332 if (i < lines.size() - 1) {
333 content << "\n";
334 }
335 }
336 formatter.AddField("content", content.str());
337 }
338
339 formatter.EndObject();
340
341 return absl::OkStatus();
342}
343
344bool FileSystemReadTool::IsTextFile(const fs::path& path) const {
345 // Check file extension first
346 std::string ext = path.extension().string();
347 std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
348
349 // Common text file extensions
350 std::set<std::string> text_extensions = {
351 ".txt", ".md", ".cc", ".cpp", ".c",
352 ".h", ".hpp", ".py", ".js", ".ts",
353 ".json", ".xml", ".yaml", ".yml", ".toml",
354 ".ini", ".cfg", ".conf", ".sh", ".bash",
355 ".zsh", ".fish", ".cmake", ".mk", ".makefile",
356 ".html", ".css", ".scss", ".sass", ".less",
357 ".jsx", ".tsx", ".rs", ".go", ".java",
358 ".kt", ".swift", ".rb", ".pl", ".php",
359 ".lua", ".vim", ".el", ".lisp", ".clj",
360 ".hs", ".ml", ".fs", ".asm", ".s",
361 ".S", ".proto", ".thrift", ".graphql", ".sql",
362 ".gitignore", ".dockerignore", ".editorconfig", ".eslintrc"};
363
364 if (text_extensions.count(ext) > 0) {
365 return true;
366 }
367
368 // For unknown extensions, check the first few bytes
369 std::ifstream file(path, std::ios::binary);
370 if (!file) {
371 return false;
372 }
373
374 // Read first 512 bytes to check for binary content
375 char buffer[512];
376 file.read(buffer, sizeof(buffer));
377 std::streamsize bytes_read = file.gcount();
378
379 // Check for null bytes (common in binary files)
380 for (std::streamsize i = 0; i < bytes_read; ++i) {
381 if (buffer[i] == '\0') {
382 return false; // Binary file
383 }
384 // Also check for other non-printable characters
385 // (excluding common whitespace)
386 if (!std::isprint(buffer[i]) && buffer[i] != '\n' && 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", info_path.filename().string().starts_with(".")
525 ? "true"
526 : "false");
527
528 formatter.EndObject();
529
530 return absl::OkStatus();
531}
532
534 const fs::path& path) const {
535 std::error_code ec;
536 auto perms = fs::status(path, ec).permissions();
537
538 if (ec) {
539 return "unknown";
540 }
541
542 std::string result;
543
544 // Owner permissions
545 result += (perms & fs::perms::owner_read) != fs::perms::none ? 'r' : '-';
546 result += (perms & fs::perms::owner_write) != fs::perms::none ? 'w' : '-';
547 result += (perms & fs::perms::owner_exec) != fs::perms::none ? 'x' : '-';
548
549 // Group permissions
550 result += (perms & fs::perms::group_read) != fs::perms::none ? 'r' : '-';
551 result += (perms & fs::perms::group_write) != fs::perms::none ? 'w' : '-';
552 result += (perms & fs::perms::group_exec) != fs::perms::none ? 'x' : '-';
553
554 // Others permissions
555 result += (perms & fs::perms::others_read) != fs::perms::none ? 'r' : '-';
556 result += (perms & fs::perms::others_write) != fs::perms::none ? 'w' : '-';
557 result += (perms & fs::perms::others_exec) != fs::perms::none ? 'x' : '-';
558
559 return result;
560}
561
562} // namespace tools
563} // namespace agent
564} // namespace cli
565} // 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.