/* * file-utils.vala * * File system operations for valaq. * This class provides utility functions for file operations. */ /** * Utility class for file system operations. * * Provides comprehensive file system functionality including VAPI file discovery, * path resolution, validation, and security checks. This class handles * the complex task of locating VAPI files across multiple standard directories * and provides safe file access with proper validation. */ public class FileUtils : Object { /** * Array of VAPI search paths in order of preference. * * These paths are searched sequentially when resolving VAPI file names. * The order is important for precedence - earlier paths have priority. */ private string[] vapi_search_paths; /** * Creates a new FileUtils instance. * * Initializes the VAPI search paths with standard system directories and any additional fallback paths that might exist on the system. */ public FileUtils () { // Initialize VAPI search paths initialize_vapi_search_paths (); } /** * Initializes the VAPI search paths in order of preference. */ private void initialize_vapi_search_paths () { vapi_search_paths = new string[0]; // Primary search path vapi_search_paths += "/usr/share/vala-0.56/vapi"; // Secondary search path vapi_search_paths += "/usr/share/vala/vapi"; // Additional fallback paths (for backward compatibility) string[] fallback_paths = { "/usr/share/vala-0.54/vapi", "/usr/share/vala-0.52/vapi", "/usr/local/share/vala/vapi" }; foreach (string path in fallback_paths) { if (file_exists (path)) { vapi_search_paths += path; } } } /** * Gets the VAPI search paths. * * @return Array of VAPI search paths in order of preference */ public string[] get_vapi_search_paths () { return vapi_search_paths; } /** * Checks if a file exists. * * @param path Path to the file * @return True if file exists, false otherwise */ public bool file_exists (string path) { return File.new_for_path (path).query_exists (); } /** * Checks if a path is a VAPI file. * * Determines if the given path points to a VAPI file by checking * the file extension. This is used for filtering and validation. * * @param path Path to check * @return True if path is a VAPI file, false otherwise */ public bool is_vapi_file (string path) { return path.has_suffix (".vapi"); } /** * Finds VAPI files in a directory. * * Scans the specified directory for files with .vapi extension. * Returns full paths to all found VAPI files. Handles directory * access errors gracefully and continues operation. * * @param directory Directory path to search for VAPI files * @return Array of VAPI file paths found in directory */ public Gee.ArrayList find_vapi_files (string directory) { var vapi_files = new Gee.ArrayList (); try { var dir = File.new_for_path (directory); if (!dir.query_exists ()) { stderr.printf ("Warning: VAPI directory does not exist: %s\n", directory); return vapi_files; } var enumerator = dir.enumerate_children ( FileAttribute.STANDARD_NAME + "," + FileAttribute.STANDARD_TYPE, FileQueryInfoFlags.NONE ); FileInfo file_info; while ((file_info = enumerator.next_file ()) != null) { string filename = file_info.get_name (); if (is_vapi_file (filename)) { vapi_files.add (Path.build_filename (directory, filename)); } } } catch (Error e) { stderr.printf ("Error scanning VAPI directory: %s\n", e.message); } return vapi_files; } /** * Finds VAPI files across all search paths. * * Scans all configured VAPI directories and returns a consolidated * list of available VAPI files. Removes duplicates based on basename * to avoid showing the same file from multiple directories. * * @return Array of VAPI file paths with duplicates removed */ public Gee.ArrayList find_all_vapi_files () { var all_vapi_files = new Gee.ArrayList (); var seen_files = new Gee.HashSet (); foreach (string search_path in vapi_search_paths) { var vapi_files = find_vapi_files (search_path); foreach (string file_path in vapi_files) { var file = File.new_for_path (file_path); string basename = file.get_basename (); // Avoid duplicates by checking basename if (!seen_files.contains (basename)) { seen_files.add (basename); all_vapi_files.add (file_path); } } } return all_vapi_files; } /** * Resolves a VAPI file name across all search paths. * Supports both basenames (e.g., "zlib.vapi") and names without extension (e.g., "zlib"). * The first found file is used, with primary search path taking precedence. * * @param vapi_name Name of the VAPI file (with or without .vapi extension) * @return Full path to the VAPI file, or null if not found */ public string? resolve_vapi_file (string vapi_name) { // Validate input if (vapi_name == "" || vapi_name.strip () == "") { return null; } // Check if this is a full path (contains path separators) if (vapi_name.contains ("/") || vapi_name.contains ("\\")) { // For full paths, we don't resolve through search paths // Just check if the file exists as-is return file_exists (vapi_name) ? vapi_name : null; } // This is a basename - ensure it has .vapi extension for searching string filename = vapi_name; if (!filename.has_suffix (".vapi")) { filename += ".vapi"; } // First, check the current working directory string current_dir = Environment.get_current_dir (); string current_dir_path = Path.build_filename (current_dir, filename); if (file_exists (current_dir_path)) { return current_dir_path; } // Then search in all VAPI paths in order of preference foreach (string search_path in vapi_search_paths) { string full_path = Path.build_filename (search_path, filename); if (file_exists (full_path)) { return full_path; } } return null; } /** * Gets information about which search path contains a VAPI file. * * @param vapi_name Name of the VAPI file (with or without .vapi extension) * @return Array containing [search_path, full_path], or null if not found */ public string[]? find_vapi_file_location (string vapi_name) { // Ensure the name has .vapi extension string filename = vapi_name; if (!filename.has_suffix (".vapi")) { filename += ".vapi"; } // Search in all paths in order of preference foreach (string search_path in vapi_search_paths) { string full_path = Path.build_filename (search_path, filename); if (file_exists (full_path)) { return { search_path, full_path }; } } return null; } /** * Gets the default VAPI directory. * * @return Path to the default VAPI directory (first in search paths) */ public string get_default_vapi_directory () { // Return the first search path that exists foreach (string path in vapi_search_paths) { if (file_exists (path)) { return path; } } // Default fallback to the primary search path return vapi_search_paths.length > 0 ? vapi_search_paths[0] : "/usr/share/vala/vapi"; } /** * Validates a file path for security and accessibility. * * Performs security checks to prevent directory traversal attacks * and ensures the path is within allowed directories. This is * crucial for preventing unauthorized file access. * * Security measures: * - Prevents "../" directory traversal * - Restricts access to VAPI directories and current working directory * - Allows absolute paths to existing VAPI files * * @param path Path to validate for security and accessibility * @return True if path is safe and allowed, false otherwise */ public bool validate_path (string path) { // Check for directory traversal attempts if (path.contains ("../") || path.contains ("..\\")) { return false; } // Convert to absolute path and check if it's within allowed directories var file = File.new_for_path (path); string absolute_path; try { absolute_path = file.get_path (); } catch (Error e) { return false; } // Allow paths in any VAPI search directory or current working directory string[] allowed_prefixes = {}; // Add all VAPI search paths foreach (string vapi_path in vapi_search_paths) { allowed_prefixes += vapi_path; } // Add current working directory allowed_prefixes += Environment.get_current_dir (); foreach (string prefix in allowed_prefixes) { if (absolute_path.has_prefix (prefix)) { return true; } } // Also allow absolute paths to VAPI files directly if (absolute_path.has_suffix (".vapi") && file_exists (absolute_path)) { return true; } return false; } }