use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::collections::HashMap;
use tar::Archive;
use log;
use lazy_static::lazy_static;
use color_eyre::Result;
use color_eyre::eyre::{self, WrapErr};
use zstd::stream::read::Decoder as ZstdDecoder;
use crate::utils;
use crate::lfs;
use crate::tar_extract::{create_package_dirs, ExtractConfig, extract_archive_with_policy};
pub struct PkgInfoField {
#[allow(dead_code)]
pub name: &'static str,
#[allow(dead_code)]
pub description: &'static str,
pub repeatable: bool,
}
lazy_static! {
pub static ref PACKAGE_KEY_MAPPING: HashMap<&'static str, &'static str> = {
let mut m = std::collections::HashMap::new();
m.insert("pkgname", "pkgname");
m.insert("pkgver", "version");
m.insert("pkgdesc", "summary");
m.insert("url", "homepage");
m.insert("builddate", "buildTime");
m.insert("packager", "maintainer");
m.insert("size", "installedSize");
m.insert("arch", "arch");
m.insert("license", "license");
m.insert("group", "group");
m.insert("depend", "requires");
m.insert("optdepend", "suggests");
m.insert("conflict", "conflicts");
m.insert("provides", "provides");
m.insert("backup", "backup");
m.insert("replaces", "replaces");
m.insert("makedepend", "buildRequires");
m.insert("checkdepend", "checkRequires");
m
};
pub static ref PKGINFO_FIELDS: HashMap<&'static str, PkgInfoField> = {
let mut m = HashMap::new();
m.insert("pkgname", PkgInfoField {
name: "pkgname",
description: "package name",
repeatable: false,
});
m.insert("pkgver", PkgInfoField {
name: "pkgver",
description: "package version",
repeatable: false,
});
m.insert("pkgdesc", PkgInfoField {
name: "pkgdesc",
description: "package description",
repeatable: false,
});
m.insert("url", PkgInfoField {
name: "url",
description: "upstream URL",
repeatable: false,
});
m.insert("builddate", PkgInfoField {
name: "builddate",
description: "build date",
repeatable: false,
});
m.insert("packager", PkgInfoField {
name: "packager",
description: "packager",
repeatable: false,
});
m.insert("size", PkgInfoField {
name: "size",
description: "package size",
repeatable: false,
});
m.insert("arch", PkgInfoField {
name: "arch",
description: "architecture",
repeatable: false,
});
m.insert("license", PkgInfoField {
name: "license",
description: "license",
repeatable: true,
});
m.insert("depend", PkgInfoField {
name: "depend",
description: "dependency",
repeatable: true,
});
m.insert("optdepend", PkgInfoField {
name: "optdepend",
description: "optional dependency",
repeatable: true,
});
m.insert("conflict", PkgInfoField {
name: "conflict",
description: "conflict",
repeatable: true,
});
m.insert("provides", PkgInfoField {
name: "provides",
description: "provided package",
repeatable: true,
});
m.insert("replaces", PkgInfoField {
name: "replaces",
description: "replaced package",
repeatable: true,
});
m.insert("backup", PkgInfoField {
name: "backup",
description: "backup file",
repeatable: true,
});
m.insert("group", PkgInfoField {
name: "group",
description: "package group",
repeatable: true,
});
m.insert("makedepend", PkgInfoField {
name: "makedepend",
description: "make dependency",
repeatable: true,
});
m.insert("checkdepend", PkgInfoField {
name: "checkdepend",
description: "check dependency",
repeatable: true,
});
m
};
pub static ref SCRIPT_MAPPING: HashMap<&'static str, &'static str> = {
let mut m = HashMap::new();
m.insert("pre_install", "pre_install.sh");
m.insert("post_install", "post_install.sh");
m.insert("pre_upgrade", "pre_upgrade.sh");
m.insert("post_upgrade", "post_upgrade.sh");
m.insert("pre_remove", "pre_remove.sh");
m.insert("post_remove", "post_remove.sh");
m
};
}
pub fn unpack_package<P: AsRef<Path>>(pkg_file: P, store_tmp_dir: P, pkgkey: Option<&str>) -> Result<()> {
let pkg_file = pkg_file.as_ref();
let store_tmp_dir = store_tmp_dir.as_ref();
create_package_dirs(store_tmp_dir, "arch")?;
log::debug!("Unpacking Arch Linux package: {}", pkg_file.display());
if !pkg_file.to_string_lossy().ends_with(".pkg.tar.zst") {
return Err(eyre::eyre!("Unsupported Arch Linux package format: {}, only .pkg.tar.zst is supported", pkg_file.display()));
}
let file = fs::File::open(pkg_file)
.wrap_err_with(|| format!("Failed to open package file: {}", pkg_file.display()))?;
log::debug!("Using zstd decompression");
let decoder = ZstdDecoder::new(file)
.wrap_err("Failed to create zstd decoder")?;
let archive = Archive::new(decoder);
extract_package_contents(archive, store_tmp_dir)
.wrap_err("Failed to extract package contents")?;
log::debug!("Creating filelist.txt");
crate::store::create_filelist_txt(store_tmp_dir)
.wrap_err_with(|| format!("Failed to create filelist.txt for {}", store_tmp_dir.display()))?;
let install_path = crate::dirs::path_join(store_tmp_dir, &["info", "arch", ".INSTALL"]);
if lfs::exists_on_host(&install_path) {
log::debug!("Processing install script");
let install_content = fs::read(&install_path)
.wrap_err_with(|| format!("Failed to read .INSTALL file: {}", install_path.display()))?;
extract_install_scriptlets(&install_content, store_tmp_dir)
.wrap_err("Failed to extract install scriptlets")?;
}
log::debug!("Creating package.txt");
create_package_txt(store_tmp_dir, pkgkey)
.wrap_err("Failed to create package.txt")?;
log::debug!("Arch Linux package unpacking completed successfully");
Ok(())
}
fn arch_path_policy(path: &Path, _is_hard_link: bool, store_tmp_dir: &Path) -> Option<PathBuf> {
let path_str = path.to_string_lossy();
if path_str.starts_with(".") {
Some(crate::dirs::path_join(store_tmp_dir, &["info", "arch"]).join(path))
} else {
Some(store_tmp_dir.join("fs").join(path))
}
}
fn extract_package_contents<R: Read>(
archive: Archive<R>,
store_tmp_dir: &Path,
) -> Result<()> {
let config = ExtractConfig::new(store_tmp_dir)
.handle_hard_links(true);
let policy: crate::tar_extract::PathPolicy = Box::new(arch_path_policy);
let mut archive = archive;
let entries = extract_archive_with_policy(&mut archive, &config, policy)?;
let pkginfo_path = crate::dirs::path_join(store_tmp_dir, &["info", "arch", ".PKGINFO"]);
if !lfs::exists_on_host(&pkginfo_path) {
return Err(eyre::eyre!("No .PKGINFO file found in package"));
}
log::debug!("Successfully unpacked Arch Linux package with {} tar entries", entries);
Ok(())
}
fn extract_install_scriptlets(install_content: &[u8], store_tmp_dir: &Path) -> Result<()> {
log::debug!("Extracting install scriptlets");
let scriptlet_names: Vec<&str> = SCRIPT_MAPPING.keys().copied().collect();
for &scriptlet_name in scriptlet_names.iter() {
let function_pattern = format!("{scriptlet_name}() {{");
let install_content_str = String::from_utf8_lossy(install_content);
if install_content_str.contains(&function_pattern) {
if let Some(standard_name) = SCRIPT_MAPPING.get(scriptlet_name) {
let wrapper_content = format!(
"#!/bin/sh
# Wrapper script for {scriptlet_name} function
THIS_SCRIPT_DIR=$(dirname \"$0\")
source \"$THIS_SCRIPT_DIR/../arch/.INSTALL\"
{scriptlet_name} \"$@\"
"
);
let script_path =
crate::dirs::path_join(store_tmp_dir, &["info", "install"]).join(standard_name);
fs::write(&script_path, wrapper_content)
.wrap_err_with(|| format!("Failed to write scriptlet wrapper to {}", script_path.display()))?;
utils::set_executable_permissions(&script_path, 0o755)?;
log::debug!("Created scriptlet wrapper: {}", standard_name);
}
}
}
Ok(())
}
fn create_package_txt(store_tmp_dir: &Path, pkgkey: Option<&str>) -> Result<()> {
log::debug!("Creating package.txt from .PKGINFO");
let pkginfo_path = crate::dirs::path_join(store_tmp_dir, &["info", "arch", ".PKGINFO"]);
let pkginfo_content = fs::read_to_string(&pkginfo_path)
.wrap_err_with(|| format!("Failed to read .PKGINFO file: {}", pkginfo_path.display()))?;
let mut raw_fields: HashMap<String, Vec<String>> = HashMap::new();
for line in pkginfo_content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some(pos) = line.find(" = ") {
let key = line[..pos].trim();
let value = line[pos + 3..].trim();
if let Some(field_info) = PKGINFO_FIELDS.get(key) {
if field_info.repeatable {
raw_fields.entry(key.to_string()).or_insert_with(Vec::new).push(value.to_string());
} else {
raw_fields.insert(key.to_string(), vec![value.to_string()]);
}
} else {
raw_fields.insert(key.to_string(), vec![value.to_string()]);
}
}
}
let mut package_fields: std::collections::HashMap<String, String> = std::collections::HashMap::new();
for (original_field, values) in raw_fields {
let mapped_field = PACKAGE_KEY_MAPPING
.get(original_field.as_str())
.unwrap_or(&original_field.as_str())
.to_string();
let value = values.join(", ");
package_fields.insert(mapped_field, value);
}
package_fields.insert("format".to_string(), "pacman".to_string());
crate::store::save_package_txt(package_fields, store_tmp_dir, pkgkey)?;
log::debug!("Successfully created package.txt");
Ok(())
}