diff options
Diffstat (limited to 'src/pkgtoolkit/archive.rs')
| -rw-r--r-- | src/pkgtoolkit/archive.rs | 377 |
1 files changed, 377 insertions, 0 deletions
diff --git a/src/pkgtoolkit/archive.rs b/src/pkgtoolkit/archive.rs new file mode 100644 index 0000000..9d9a7d1 --- /dev/null +++ b/src/pkgtoolkit/archive.rs @@ -0,0 +1,377 @@ +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<bool, std::io::Error>; + fn loadmeta( + minimal_package_meta: &mut Package, + ) -> Result<(Install, Option<Setts>, Option<Build>), std::io::Error>; + fn validate_custom_script(script: &str) -> Result<(), std::io::Error>; + fn validate_path(path: &str, base_dir: &Path) -> Result<std::path::PathBuf, std::io::Error>; +} + +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<Setts>, Option<Build>) + /// + /// 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<Setts>, Option<Build>), 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<Setts> = None; + let mut build_meta: Option<Build> = 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 предупреждения, если они пустые или отсутствуют. + /// Ожидаем структуру: <cachedir>/<packagename>/{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<bool, std::io::Error> { + 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<Vec<std::path::PathBuf>, 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", + ) + })?; + + // Проверим, что структура именно <cachedir>/<packagename>/INSTALL + if pkg_dir.parent() != Some(cache_root) { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Invalid meta-files layout: expected <cachedir>/<packagename>/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<Install, toml::de::Error> = 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<std::path::PathBuf, std::io::Error> { + 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) + } +} |
