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)?; // Extract to temporary directory first let temp_dir = Path::new(cache_dir).join("temp_extract"); if temp_dir.exists() { fs::remove_dir_all(&temp_dir)?; } fs::create_dir_all(&temp_dir)?; let file = File::open(path_to_archive)?; let gz = GzDecoder::new(file); let mut archive = Archive::new(gz); // Unpack into temporary directory match archive.unpack(&temp_dir) { Ok(()) => Ok(()), Err(e) if e.kind() == io::ErrorKind::AlreadyExists => Ok(()), Err(e) => Err(e), }?; // Read package name from INSTALL file to determine final directory let install_path = temp_dir.join("INSTALL"); if !install_path.exists() { // Try to find INSTALL in subdirectories for entry in fs::read_dir(&temp_dir)? { let entry = entry?; let path = entry.path(); if path.is_dir() { let install_in_subdir = path.join("INSTALL"); if install_in_subdir.exists() { // Move contents to temp_dir for item in fs::read_dir(&path)? { let item = item?; let item_path = item.path(); let dest_path = temp_dir.join(item_path.file_name().unwrap()); fs::rename(&item_path, &dest_path)?; } fs::remove_dir(&path)?; break; } } } } let mut install_path = None; let mut pkg_dir_name = None; let root_install = temp_dir.join("INSTALL"); if root_install.exists() { install_path = Some(root_install); pkg_dir_name = Some("package".to_string()); } else { for entry in fs::read_dir(&temp_dir)? { let entry = entry?; let path = entry.path(); if path.is_dir() { let install_in_subdir = path.join("INSTALL"); if install_in_subdir.exists() { install_path = Some(install_in_subdir); pkg_dir_name = Some(path.file_name().unwrap().to_string_lossy().to_string()); break; } } } } let install_path = install_path.ok_or_else(|| { io::Error::new(io::ErrorKind::NotFound, "INSTALL file not found in archive") })?; 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 final_dir_name = format!( "{}-{}", install_meta.package.name, install_meta.package.version ); let final_dir = Path::new(cache_dir).join(&final_dir_name); if final_dir.exists() { fs::remove_dir_all(&final_dir)?; } fs::create_dir_all(&final_dir)?; if pkg_dir_name.clone().unwrap() == "package" { for entry in fs::read_dir(&temp_dir)? { let entry = entry?; let src_path = entry.path(); let dest_path = final_dir.join(src_path.file_name().unwrap()); fs::rename(&src_path, &dest_path)?; } fs::remove_dir(&temp_dir)?; } else { let pkg_subdir = temp_dir.join(pkg_dir_name.clone().unwrap()); fs::rename(&pkg_subdir, &final_dir)?; fs::remove_dir(&temp_dir)?; } Ok(()) } /// 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 Package, ) -> 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" license = "BSD-2-Clause" url = "/repo/my-package.mesk" git_repo = "https://github.com/example/my-package.git" [install] path = "/usr/bin/my-package" dependencies = ["package", "i2pd", "llvm-19-devel", "etc..."] # Leave it empty if there are no dependencies 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 """ If there is a custom_script field, mesk will not automatically install your package and other fields in [install] will not be required. */ 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 pkg_dir_name = format!( "{}-{}", minimal_package_meta.name, minimal_package_meta.version ); let install_path = Path::new(cache_dir).join(format!("{}/INSTALL", pkg_dir_name)); let setts_path = Path::new(cache_dir).join(format!("{}/SETTS", pkg_dir_name)); let build_path = Path::new(cache_dir).join(format!("{}/BUILD", pkg_dir_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); 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", ) })?; 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) } }