""" ZIP utilities for handling Ada project ZIP files and creating Python project ZIP files. """ import os import tempfile import zipfile import shutil from pathlib import Path from typing import Tuple, Optional from loguru import logger class ZipHandler: """Handles ZIP file operations for Ada to Python conversion.""" def __init__(self): self.temp_base_dir = Path(tempfile.gettempdir()) / "ada-conversion" # Clean up any existing sessions from previous runs self._cleanup_previous_sessions() self.temp_base_dir.mkdir(exist_ok=True) logger.debug("ZIP handler initialized", temp_dir=str(self.temp_base_dir)) def _cleanup_previous_sessions(self) -> None: """Clean up any existing session directories from previous runs.""" if self.temp_base_dir.exists(): logger.debug("Cleaning up previous sessions", temp_base=str(self.temp_base_dir)) shutil.rmtree(self.temp_base_dir) def is_zip_file(self, file_path: str) -> bool: """ Check if the given file path is a ZIP file. Args: file_path: Path to the file to check Returns: True if the file is a ZIP file, False otherwise """ try: path = Path(file_path) if not path.exists() or not path.is_file(): return False # Check file extension if path.suffix.lower() != '.zip': return False # Verify it's actually a ZIP file by trying to open it with zipfile.ZipFile(path, 'r') as zip_file: # Try to read the file list zip_file.namelist() return True except (zipfile.BadZipFile, Exception): return False def extract_ada_project(self, zip_path: str, session_id: Optional[str] = None) -> Tuple[str, str]: """ Extract Ada project ZIP file to temporary directory. Args: zip_path: Path to the ZIP file containing Ada project session_id: Optional session ID for unique directory naming Returns: Tuple of (extracted_project_path, temp_session_dir) Raises: FileNotFoundError: If ZIP file doesn't exist zipfile.BadZipFile: If file is not a valid ZIP ValueError: If no Ada files found in ZIP """ zip_path = Path(zip_path) if not zip_path.exists(): raise FileNotFoundError(f"ZIP file does not exist: {zip_path}") if not self.is_zip_file(str(zip_path)): raise zipfile.BadZipFile(f"Not a valid ZIP file: {zip_path}") # Create unique session directory if session_id is None: session_id = f"session_{os.getpid()}_{zip_path.stem}" session_dir = self.temp_base_dir / session_id session_dir.mkdir(parents=True, exist_ok=True) logger.info("Extracting Ada project ZIP", zip_file=str(zip_path), extract_to=str(session_dir)) # Extract ZIP file with zipfile.ZipFile(zip_path, 'r') as zip_file: zip_file.extractall(session_dir) # Find the Ada project directory within the extracted content ada_project_path = self._find_ada_project_root(session_dir) if ada_project_path is None: raise ValueError(f"No Ada files found in ZIP: {zip_path}") logger.success("Ada project extracted successfully", ada_project=str(ada_project_path), ada_files_found=len(list(ada_project_path.rglob("*.ad[s|b]"))) + len(list(ada_project_path.rglob("*.ada"))), session_dir=str(session_dir)) return str(ada_project_path), str(session_dir) def _find_ada_project_root(self, extracted_dir: Path) -> Optional[Path]: """ Find the root directory containing Ada files within extracted content. Args: extracted_dir: Directory where ZIP was extracted Returns: Path to Ada project root, or None if no Ada files found """ ada_extensions = {'.ads', '.adb', '.ada'} # Check if extracted_dir itself contains Ada files ada_files = [] for ext in ada_extensions: ada_files.extend(list(extracted_dir.rglob(f"*{ext}"))) if not ada_files: return None # Find the common parent directory of all Ada files if len(ada_files) == 1: # If only one file, its parent is the project root return ada_files[0].parent # Find common parent of all Ada files common_parent = Path(ada_files[0]).parent for ada_file in ada_files[1:]: # Find common path between current common_parent and this file's parent try: # Get relative path from common_parent to this file's parent ada_file.parent.relative_to(common_parent) except ValueError: # Files are in different branches, need to go up while True: try: ada_file.parent.relative_to(common_parent) break except ValueError: common_parent = common_parent.parent if common_parent == extracted_dir.parent: # Gone too far up common_parent = extracted_dir break return common_parent def create_python_project_zip(self, python_project_path: str, output_zip_path: Optional[str] = None) -> str: """ Create a ZIP file from the generated Python project. Args: python_project_path: Path to the Python project directory output_zip_path: Optional output ZIP path (defaults to project_name.zip) Returns: Path to the created ZIP file Raises: FileNotFoundError: If Python project directory doesn't exist """ project_path = Path(python_project_path) if not project_path.exists(): raise FileNotFoundError(f"Python project directory does not exist: {python_project_path}") if not project_path.is_dir(): raise ValueError(f"Path is not a directory: {python_project_path}") # Determine output ZIP path if output_zip_path is None: output_zip_path = str(project_path.parent / f"{project_path.name}.zip") output_zip_path = Path(output_zip_path) logger.info("Creating Python project ZIP", python_project=str(project_path), output_zip=str(output_zip_path)) # Create ZIP file with zipfile.ZipFile(output_zip_path, 'w', zipfile.ZIP_DEFLATED) as zip_file: for file_path in project_path.rglob('*'): if file_path.is_file(): # Get relative path within the project relative_path = file_path.relative_to(project_path) zip_file.write(file_path, relative_path) logger.success("Python project ZIP created", zip_file=str(output_zip_path), zip_size=f"{output_zip_path.stat().st_size / (1024*1024):.1f} MB") return str(output_zip_path)