use crate::cfg::config::Config; use std::{ fs::{self, File, create_dir_all}, io, os::unix::fs::PermissionsExt, path::Path, }; use flate2::read::GzDecoder; use tar::Archive; use toml; use super::types::{Build, Install, Package, Setts}; pub trait ArchiveOperations { fn extract_archive(path_to_archive: &str) -> Result<(), std::io::Error>; fn check(path_to_archive: String) -> Result; fn loadmeta( minimal_package_meta: &mut Package, ) -> Result<(Install, Option, Option), std::io::Error>; fn validate_custom_script(script: &str) -> Result<(), std::io::Error>; fn validate_path(path: &str, base_dir: &Path) -> Result; } impl ArchiveOperations for Package { /// Extracts a .tar.gz archive to the cache directory specified in Config. /// /// This function handles opening the archive file, decompressing it with GzDecoder, /// and unpacking the contents into the configured cache directory. /// /// # Arguments /// * `path_to_archive` - A string representing the path to the .tar.gz file. /// /// # Returns /// * `Ok(())` if the archive is successfully unpacked. /// * `Err(std::io::Error)` if there's an issue opening, reading, or unpacking the archive. fn extract_archive(path_to_archive: &str) -> Result<(), std::io::Error> { let config = Config::parse().map_err(|e| std::io::Error::other(e.to_string()))?; let cache_dir = &config.paths.cache_dir; create_dir_all(cache_dir)?; for meta_name in ["INSTALL", "SETTS", "BUILD"] { let meta_path = Path::new(cache_dir).join(meta_name); if meta_path.exists() { fs::remove_file(&meta_path)?; } } let file = File::open(path_to_archive)?; let gz = GzDecoder::new(file); let mut archive = Archive::new(gz); // Unpack directly into the cache directory. Игнорируем AlreadyExists, чтобы не мешать валидации. match archive.unpack(cache_dir) { Ok(()) => Ok(()), Err(e) if e.kind() == io::ErrorKind::AlreadyExists => Ok(()), Err(e) => Err(e), } } /// Load meta information from the .mesk archive. /// /// This function parses the meta information from the .mesk archive, /// which includes the package name, version, architecture, description, /// installation path, user, group, mode, and custom installation script /// and deserializing this information. Returns (Install, Option, Option) /// /// The function expects the 'INSTALL', 'SETTS', and 'BUILD' files to be present /// in the `config.paths.cache_dir`. It specifically requires the 'INSTALL' file. /// /// # Errors /// /// Returns an error if the `cache_dir` cannot be created, if the required 'INSTALL' file /// is not found, or if the 'INSTALL' file is empty. #[allow(clippy::type_complexity)] fn loadmeta( minimal_package_meta: &mut Self, ) -> Result<(Install, Option, Option), std::io::Error> { // Changed return type for more flexibility /* Example INSTALL format: [package] name = "my-package" version = "1.0.0" arch = "X86_64" descr = "Just example INSTALL script" [install] path = "/usr/bin/my-package" user = "root" group = "root" mode = "755" # Also [install] can be # path = "/usr/bin/my-package" # user = "root" # group = "root" # mode = "755" # custom_script = "./install.sh" OR # custom_script = """ # echo "Installing my-package" # sudo apt-get install my-package # """ */ let config = Config::parse().map_err(|e| std::io::Error::other(e.to_string()))?; fs::create_dir_all(&config.paths.cache_dir)?; let cache_dir = &config.paths.cache_dir; let install_path = Path::new(cache_dir).join(format!("{}/INSTALL", minimal_package_meta.name)); let setts_path = Path::new(cache_dir).join(format!("{}/SETTS", minimal_package_meta.name)); let build_path = Path::new(cache_dir).join(format!("{}/BUILD", minimal_package_meta.name)); if !install_path.exists() { return Err(io::Error::new( io::ErrorKind::NotFound, "File INSTALL not found in cache directory", )); } let install_content = fs::read_to_string(&install_path)?; let install_meta: Install = toml::from_str(&install_content) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; let mut setts_meta: Option = None; let mut build_meta: Option = None; if setts_path.exists() { let setts_content = fs::read_to_string(&setts_path)?; setts_meta = Some( toml::from_str(&setts_content) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?, ); } if build_path.exists() { let build_content = fs::read_to_string(&build_path)?; build_meta = Some( toml::from_str(&build_content) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?, ); } if let Some(ref _script) = install_meta.install.custom_script { log::warn!( "Custom script found for package: {}", install_meta.package.name ); } else { log::info!( "No custom script for package: {}", install_meta.package.name ); } Ok((install_meta, setts_meta, build_meta)) } /// Checks if the archive contains INSTALL, SETTS and BUILD files. /// /// Checks if INSTALL file exists and is not empty. If it does not exist or is empty, returns an error. /// Checks SETTS and BUILD (if present) and logs предупреждения, если они пустые или отсутствуют. /// Ожидаем структуру: //{INSTALL, SETTS, BUILD, ...}. /// /// # Errors /// * Returns an error if INSTALL file does not exist or is empty. /// * Returns an error if INSTALL file is empty. fn check(path_to_archive: String) -> Result { Self::extract_archive(&path_to_archive)?; let config = Config::parse().map_err(|e| std::io::Error::other(e.to_string()))?; let cache_root = Path::new(&config.paths.cache_dir); // Рекурсивно находим все файлы INSTALL под cache_root fn find_install(root: &Path) -> Result, std::io::Error> { let mut installs = Vec::new(); for entry in fs::read_dir(root)? { let entry = entry?; let path = entry.path(); if path.is_dir() { installs.extend(find_install(&path)?); } else if path.file_name().and_then(|n| n.to_str()) == Some("INSTALL") { installs.push(path); } } Ok(installs) } let installs = find_install(cache_root)?; if installs.is_empty() { return Err(std::io::Error::new( std::io::ErrorKind::NotFound, "INSTALL file not found in archive", )); } if installs.len() > 1 { return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, "Multiple INSTALL files found in archive; expected exactly one", )); } let install_path = &installs[0]; let pkg_dir = install_path.parent().ok_or_else(|| { std::io::Error::new( std::io::ErrorKind::InvalidData, "INSTALL has no parent directory", ) })?; // Проверим, что структура именно //INSTALL if pkg_dir.parent() != Some(cache_root) { return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, "Invalid meta-files layout: expected //INSTALL", )); } let setts_path = pkg_dir.join("SETTS"); let build_path = pkg_dir.join("BUILD"); let install_content = std::fs::read_to_string(install_path)?; if install_content.trim().is_empty() { return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, "INSTALL file is empty", )); } if !setts_path.exists() { log::warn!("SETTS file not found in archive. Make sure you dont need this."); } else { let setts_content = std::fs::read_to_string(&setts_path)?; if setts_content.trim().is_empty() { log::warn!("SETTS file is empty. Make sure you dont need this."); } } if !build_path.exists() { log::warn!("BUILD file not found in archive. Make sure you dont need this."); } else { let build_content = std::fs::read_to_string(&build_path)?; if build_content.trim().is_empty() { log::warn!("BUILD file is empty. Make sure you dont need this."); } } let content = std::fs::read_to_string(install_path).map_err(|e| { log::warn!("Failed to read file: {}", e); e })?; let install_content: Result = toml::from_str(&content); log::info!("Validating arch..."); let install_parsed = match install_content { Ok(v) => v, Err(e) => { log::error!("Arch mismatch while parsing INSTALL: {}", e); return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, format!("Arch mismatch: {}", e), )); } }; if std::env::consts::ARCH != install_parsed.package.arch.as_str() { let pkg_arch = &install_parsed.package.arch; log::error!( "Arch mismatch. Package arch: {:?}, Host arch: {}", pkg_arch, std::env::consts::ARCH ); return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, "Arch mismatch", )); } Ok(true) } /// Validates a custom script for security and basic syntax /// Now only minimal validation is performed fn validate_custom_script(script: &str) -> Result<(), std::io::Error> { // Check for dangerous commands let dangerous_patterns = [ "rm -rf /", "sudo rm", "chmod 777", "chown root", "dd if=", "mkfs", "fdisk", "format", ":(){ :|:& };:", ]; for pattern in &dangerous_patterns { if script.contains(pattern) { return Err(std::io::Error::other(format!( "Dangerous command detected in script: {}", pattern ))); } } // Check for script path existence if it's a file path if script.starts_with("./") { let script_path = Path::new(script); if !script_path.exists() { return Err(std::io::Error::other(format!( "Script file not found: {}", script ))); } // Check if it's executable let metadata = fs::metadata(script_path)?; if !metadata.permissions().mode() & 0o111 != 0 { log::warn!("Script {} is not executable", script); } } Ok(()) } /// Validates and sanitizes a file path for security fn validate_path(path: &str, base_dir: &Path) -> Result { let path = Path::new(path); // Convert to absolute path let absolute_path = if path.is_absolute() { path.to_path_buf() } else { base_dir.join(path) }; // Normalize the path let normalized = match absolute_path.canonicalize() { Ok(p) => p, Err(_) => { // If canonicalization fails, at least clean up the path let mut cleaned = std::path::PathBuf::new(); for component in absolute_path.components() { match component { std::path::Component::ParentDir => { // Don't go above base directory if cleaned.pop() { // Successfully removed parent } else { return Err(std::io::Error::other( "Path traversal attempt detected", )); } } std::path::Component::CurDir => { // Skip current directory } _ => cleaned.push(component), } } cleaned } }; // Ensure the path is within the base directory (for relative paths) if !path.is_absolute() && let Ok(base_absolute) = base_dir.canonicalize() && !normalized.starts_with(&base_absolute) { return Err(std::io::Error::other( "Path is outside of allowed directory", )); } Ok(normalized) } }