""" File operation tools for Clover - A terminal assistant for AI-powered project management """ import os import shutil from pathlib import Path # Working directory chroot - all file operations are restricted to this directory WORKING_DIR = os.path.abspath(os.getenv("CLOVER_PROJECT_ROOT", ".")) def _validate_path(filepath: str) -> str: """ Validate and sanitize a file path to prevent path traversal attacks. Ensures the resolved path stays within the working directory chroot. Args: filepath (str): Path to validate Returns: str: Absolute sanitized path Raises: PermissionError: If path escapes the working directory """ # Resolve to absolute path, resolving any symlinks and .. components if not os.path.isabs(filepath): abs_path = os.path.realpath(os.path.join(WORKING_DIR, filepath)) else: abs_path = os.path.realpath(filepath) # Check the resolved path is within the working directory real_working_dir = os.path.realpath(WORKING_DIR) if not abs_path.startswith(real_working_dir + os.sep) and abs_path != real_working_dir: raise PermissionError( f"Access denied: path '{filepath}' resolves outside working directory '{WORKING_DIR}'" ) return abs_path def read_file(filepath): """ Read content from a file and return its contents. Path is validated to stay within the working directory chroot. Args: filepath (str): Path to the file to read Returns: str: Content of the file Raises: FileNotFoundError: If the file does not exist PermissionError: If path escapes working directory IOError: If there's an error reading the file """ try: safe_path = _validate_path(filepath) except PermissionError as e: raise e try: with open(safe_path, "r", encoding="utf-8") as f: content = f.read() return content except FileNotFoundError: raise FileNotFoundError(f"File '{filepath}' not found") except PermissionError: raise PermissionError(f"Permission denied reading file '{filepath}'") except Exception as e: raise IOError(f"Error reading file '{filepath}': {str(e)}") def create_file(filepath, content=""): """ Create a new file with specified content. Path is validated to stay within the working directory chroot. Args: filepath (str): Path to the file to create content (str): Content to write to the file Returns: bool: True if successful, False otherwise """ try: safe_path = _validate_path(filepath) except PermissionError as e: print(f"Access denied: {str(e)}") return False try: # Create parent directories if they don't exist Path(safe_path).parent.mkdir(parents=True, exist_ok=True) with open(safe_path, "w", encoding="utf-8") as f: f.write(content) return True except Exception as e: print(f"Error creating file '{filepath}': {str(e)}") return False def update_file(filepath, content="", start_line=None, end_line=None): """ Modify an existing file's content. Path is validated to stay within the working directory chroot. Args: filepath (str): Path to the file to update content (str): New content to insert start_line (int): Starting line number (1-based) - optional end_line (int): Ending line number (1-based) - optional Returns: bool: True if successful, False otherwise """ try: safe_path = _validate_path(filepath) except PermissionError as e: print(f"Access denied: {str(e)}") return False try: # Read existing content if os.path.exists(safe_path): with open(safe_path, "r", encoding="utf-8") as f: lines = f.readlines() else: lines = [] # If specific line range is specified, replace that section if start_line is not None and end_line is not None: # Adjust for 0-based indexing start_idx = max(0, start_line - 1) end_idx = min(len(lines), end_line) lines[start_idx:end_idx] = [content + "\n"] else: # Append content at the end lines.append(content + "\n") # Write updated content back to file with open(safe_path, "w", encoding="utf-8") as f: f.writelines(lines) return True except Exception as e: print(f"Error updating file '{filepath}': {str(e)}") return False def delete_file(filepath): """ Remove a file from the project. Path is validated to stay within the working directory chroot. Requires explicit confirmation for destructive operations. Args: filepath (str): Path to the file to delete Returns: bool: True if successful, False otherwise """ try: safe_path = _validate_path(filepath) except PermissionError as e: print(f"Access denied: {str(e)}") return False try: if os.path.exists(safe_path): os.remove(safe_path) return True else: print(f"File '{filepath}' does not exist") return False except Exception as e: print(f"Error deleting file '{filepath}': {str(e)}") return False def list_files(directory=".", recursive=False): """ List all files in a directory. Path is validated to stay within the working directory chroot. Args: directory (str): Directory to list files from recursive (bool): Whether to list recursively Returns: list: List of file paths """ try: safe_dir = _validate_path(directory) except PermissionError as e: print(f"Access denied: {str(e)}") return [] try: if recursive: files = [] for root, dirs, filenames in os.walk(safe_dir): for filename in filenames: files.append(os.path.join(root, filename)) return files else: return [ f for f in os.listdir(safe_dir) if os.path.isfile(os.path.join(safe_dir, f)) ] except Exception as e: print(f"Error listing files in '{directory}': {str(e)}") return []