summaryrefslogtreecommitdiff
path: root/src/pkgtoolkit/archive.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/pkgtoolkit/archive.rs')
-rw-r--r--src/pkgtoolkit/archive.rs377
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)
+ }
+}