use crate::rpm_repo::PACKAGE_KEY_MAPPING;
#[cfg(all(target_os = "linux", debug_assertions))]
use crate::rpm_verify;
use crate::rpm_triggers::{extract_rpm_triggers, extract_install_prefixes};
#[cfg(unix)]
use crate::utils;
use color_eyre::eyre::WrapErr;
use color_eyre::Result;
use rpm::{DependencyFlags, FileMode, IndexTag, Package};
use std::collections::HashMap;
use crate::lfs;
use crate::tar_extract::create_package_dirs;
use std::path::Path;
pub fn unpack_package<P: AsRef<Path>>(rpm_file: P, store_tmp_dir: P, pkgkey: Option<&str>) -> Result<()> {
let rpm_file = rpm_file.as_ref();
let store_tmp_dir = store_tmp_dir.as_ref();
create_package_dirs(store_tmp_dir, "rpm")?;
let package = Package::open(rpm_file)
.wrap_err_with(|| format!("Failed to open RPM file: {}", rpm_file.display()))?;
let target_fs_dir = store_tmp_dir.join("fs");
extract_rpm_files(&package, &target_fs_dir)?;
#[cfg(all(target_os = "linux", debug_assertions))]
{
if let Err(e) = rpm_verify::verify_rpm_extraction(rpm_file, &target_fs_dir) {
log::warn!("RPM extraction verification check failed for {}: {}. Continuing with epkg's version.", rpm_file.display(), e);
}
}
crate::store::create_filelist_txt(store_tmp_dir)?;
create_scriptlets(&package, store_tmp_dir)?;
extract_rpm_triggers(&package, store_tmp_dir)?;
extract_install_prefixes(&package, store_tmp_dir)?;
create_package_txt(&package, rpm_file, store_tmp_dir, pkgkey)?;
Ok(())
}
fn extract_rpm_files<P: AsRef<Path>>(package: &Package, target_dir: P) -> Result<()> {
let target_dir = target_dir.as_ref();
match package.metadata.get_file_entries() {
Ok(file_entries) if file_entries.is_empty() => {
log::debug!("RPM package contains no files, skipping extraction");
return Ok(());
}
Ok(_) => {
for file_result in package.files()
.wrap_err_with(|| "Failed to get file iterator from RPM package")? {
let file = file_result
.wrap_err_with(|| "Failed to read file from RPM package")?;
if file.metadata.flags.contains(rpm::FileFlags::GHOST) {
log::debug!("Skipping ghost file: {}", file.metadata.path.display());
continue;
}
let path_in_pkg = file.metadata.path.to_string_lossy();
let rel = std::path::Path::new(path_in_pkg.trim_start_matches('/'));
let file_path = target_dir.join(crate::lfs::sanitize_path_for_windows(rel));
if let Some(parent) = file_path.parent() {
lfs::create_dir_all_with_case_sensitivity(parent)?;
}
match file.metadata.mode {
FileMode::Regular { permissions } => {
lfs::write(&file_path, &file.content)?;
#[cfg(unix)]
{
let mode = permissions | 0o600;
utils::set_permissions_from_mode(&file_path, mode.into())
.wrap_err_with(|| format!("Failed to set permissions for file at {}", file_path.display()))?;
}
#[cfg(not(unix))]
let _ = permissions;
}
FileMode::Dir { permissions } => {
lfs::create_dir_all_with_case_sensitivity(&file_path)?;
#[cfg(unix)]
{
let mode = permissions | 0o700;
utils::set_permissions_from_mode(&file_path, mode.into())
.wrap_err_with(|| format!("Failed to set permissions for directory at {}", file_path.display()))?;
}
#[cfg(not(unix))]
let _ = permissions;
}
FileMode::SymbolicLink { permissions: _ } => {
if !file.metadata.linkto.is_empty() {
#[cfg(unix)]
{
let link_target = lfs::sanitize_path_for_windows(Path::new(&file.metadata.linkto));
if let Err(e) = lfs::symlink_for_virtiofs(&link_target, &file_path) {
log::warn!("Failed to create symlink {:?} -> {:?}: {}", file_path, link_target, e);
}
}
}
}
FileMode::Invalid { raw_mode: _, reason } => {
log::warn!("Invalid file mode for {:?}: {}", file_path, reason);
}
_ => {
log::warn!("Unsupported file mode for {:?}", file_path);
}
}
}
}
Err(_) => {
log::debug!("Failed to get file entries, assuming empty package");
}
}
Ok(())
}
pub fn create_scriptlets<P: AsRef<Path>>(package: &Package, store_tmp_dir: P) -> Result<()> {
let store_tmp_dir = store_tmp_dir.as_ref();
let install_dir = crate::dirs::path_join(store_tmp_dir, &["info", "install"]);
let scriptlet_mapping: HashMap<&str, &str> = [
("prein", "pre_install.sh"),
("postin", "post_install.sh"),
("preun", "pre_uninstall.sh"),
("postun", "post_uninstall.sh"),
("pretrans", "pre_trans.sh"),
("posttrans", "post_trans.sh"),
("preuntrans", "pre_untrans.sh"),
("postuntrans", "post_untrans.sh"),
].into_iter().collect();
let metadata = &package.metadata;
for (rpm_script, common_script) in &scriptlet_mapping {
if let Some((script_content, file_extension)) = get_scriptlet_with_extension(metadata, rpm_script) {
let script_name = if file_extension != "sh" {
format!("{}.{}", common_script.trim_end_matches(".sh"), file_extension)
} else {
common_script.to_string()
};
let target_path = install_dir.join(&script_name);
crate::utils::write_scriptlet_content(&target_path, script_content.as_bytes())?;
}
}
Ok(())
}
fn get_scriptlet_with_extension(metadata: &rpm::PackageMetadata, scriptlet_name: &str) -> Option<(String, String)> {
let scriptlet = match scriptlet_name {
"prein" => metadata.get_pre_install_script().ok(),
"postin" => metadata.get_post_install_script().ok(),
"preun" => metadata.get_pre_uninstall_script().ok(),
"postun" => metadata.get_post_uninstall_script().ok(),
"pretrans" => metadata.get_pre_trans_script().ok(),
"posttrans" => metadata.get_post_trans_script().ok(),
"preuntrans" => get_scriptlet_from_header(metadata, "preuntrans"),
"postuntrans" => get_scriptlet_from_header(metadata, "postuntrans"),
_ => None,
}?;
let script_content = scriptlet.script.clone();
let (file_extension, modified_content) = determine_script_extension(&scriptlet, &script_content);
Some((modified_content, file_extension))
}
pub fn get_scriptlet_from_header(metadata: &rpm::PackageMetadata, scriptlet_name: &str) -> Option<rpm::Scriptlet> {
let script_tag = match scriptlet_name {
"preuntrans" => IndexTag::RPMTAG_PREUNTRANS,
"postuntrans" => IndexTag::RPMTAG_POSTUNTRANS,
"triggerprein" => IndexTag::RPMTAG_TRIGGERPREIN,
"triggerin" => IndexTag::RPMTAG_TRIGGERIN,
"triggerun" => IndexTag::RPMTAG_TRIGGERUN,
"triggerpostun" => IndexTag::RPMTAG_TRIGGERPOSTUN,
"filetriggerin" => IndexTag::RPMTAG_FILETRIGGERIN,
"filetriggerun" => IndexTag::RPMTAG_FILETRIGGERUN,
"filetriggerpostun" => IndexTag::RPMTAG_FILETRIGGERPOSTUN,
"transfiletriggerin" => IndexTag::RPMTAG_TRANSFILETRIGGERIN,
"transfiletriggerun" => IndexTag::RPMTAG_TRANSFILETRIGGERUN,
"transfiletriggerpostun" => IndexTag::RPMTAG_TRANSFILETRIGGERPOSTUN,
_ => return None,
};
if !metadata.header.entry_is_present(script_tag) {
return None;
}
let script = metadata.header.get_entry_data_as_string(script_tag).ok()?.to_string();
let program = None;
Some(rpm::Scriptlet {
script,
program,
flags: Some(rpm::ScriptletFlags::empty()),
})
}
* Determines file extension and adds shebang for RPM scriptlets.
*
* Step 1: Process scriptlet.program (if present and non-empty)
* - Deduplicate consecutive identical elements in program array
* - Determine extension via interpreter_to_extension()
* - Add shebang if content lacks one and interpreter is either:
* * A path starting with '/' (use full program vector)
* * Maps to a known extension (use "/usr/bin/env" with stripped interpreter name)
* - Lua interpreter mapped to "rpmlua" for RPM compatibility
* - Examples:
* program = ["<lua>"] -> ext = "lua", adds "#!/usr/bin/env -S rpmlua\n"
* program = ["/bin/sh", "-c"] -> ext = "sh", adds "#!/bin/sh -c\n"
*
* Step 2: Handle empty content with no extension (one-liner utility programs)
* - When script_content is empty and no extension determined, create a .sh wrapper
* - Format: "#!/bin/sh\n{program.join(' ')}\n"
* - Example: program = ["/sbin/ldconfig", "libs"], content = "" ->
* content = "#!/bin/sh\n/sbin/ldconfig libs\n", ext = "sh"
*
* Step 3: Default fallback
* - If no extension determined yet, default to "sh"
*/
pub fn determine_script_extension(scriptlet: &rpm::Scriptlet, script_content: &str) -> (String, String) {
let mut extension = String::new();
let mut content = script_content.to_string();
if let Some(ref program) = scriptlet.program {
if program.is_empty() {
} else {
let mut program_dedup = Vec::new();
for item in program {
let item_str = item.as_str();
if program_dedup.last() != Some(&item_str) {
program_dedup.push(item_str);
}
}
let interpreter = &program_dedup[0];
extension = interpreter_to_extension(interpreter);
if !content.trim_start().starts_with("#!") {
if (interpreter.starts_with("/") || !extension.is_empty()) && extension != "lua" {
let shebang = if interpreter.starts_with("/") {
format!("#!{}\n", program_dedup.join(" "))
} else {
let interpreter_clean = interpreter.trim_matches(|c| c == '<' || c == '>');
let interpreter_name = if interpreter_clean == "lua" { "rpmlua" } else { interpreter_clean };
let mut parts = vec!["/usr/bin/env", "-S", interpreter_name];
parts.extend_from_slice(&program_dedup[1..]);
format!("#!{}\n", parts.join(" "))
};
content = format!("{}{}", shebang, content);
}
}
if content.trim().is_empty() && extension.is_empty() {
content = format!("#!/bin/sh\n{}\n", program_dedup.join(" "));
extension = "sh".to_string();
}
}
}
if extension.is_empty() {
extension = "sh".to_string();
}
(extension, content)
}
fn interpreter_to_extension(interpreter: &str) -> String {
let interpreter_name = std::path::Path::new(interpreter)
.file_name()
.and_then(|name| name.to_str())
.unwrap_or(interpreter);
match interpreter_name {
name if name.contains("lua") => "lua".to_string(),
name if name.contains("python") => "py".to_string(),
name if name.contains("perl") => "pl".to_string(),
name if name.contains("node") => "js".to_string(),
name if name.contains("ruby") => "rb".to_string(),
"tcl" | "tclsh" => "tcl".to_string(),
"awk" | "gawk" | "mawk" => "awk".to_string(),
"bash" | "sh" | "dash" | "zsh" | "fish" => "sh".to_string(),
_ => {
log::debug!("Unknown interpreter '{}'", interpreter_name);
"".to_string()
}
}
}
pub(crate) fn dependency_flags_to_operator(flags: DependencyFlags) -> Option<&'static str> {
if flags.contains(DependencyFlags::LE) {
Some("<=")
} else if flags.contains(DependencyFlags::GE) {
Some(">=")
} else if flags.contains(DependencyFlags::LESS) {
Some("<")
} else if flags.contains(DependencyFlags::GREATER) {
Some(">")
} else if flags.contains(DependencyFlags::EQUAL) {
Some("=")
} else {
None
}
}
fn format_rpm_dependency(dep: &rpm::Dependency) -> String {
let name = &dep.name;
let version = &dep.version;
let flags = dep.flags;
if version.is_empty() {
return name.to_string();
}
let op = dependency_flags_to_operator(flags).unwrap_or("=");
format!("{} {} {}", name, op, version)
}
fn format_rpm_dependencies(deps: &[rpm::Dependency]) -> String {
deps.iter()
.map(format_rpm_dependency)
.collect::<Vec<String>>()
.join(", ")
}
pub fn create_package_txt<P: AsRef<Path>>(package: &Package, rpm_file: P, store_tmp_dir: P, pkgkey: Option<&str>) -> Result<()> {
let store_tmp_dir = store_tmp_dir.as_ref();
let metadata = &package.metadata;
let mut raw_fields: Vec<(String, String)> = Vec::new();
raw_fields.push(("name".to_string(), metadata.get_name().unwrap_or("unknown").to_string()));
let version = metadata.get_version().unwrap_or("unknown");
let release = metadata.get_release().unwrap_or("unknown");
let epoch = metadata.get_epoch().unwrap_or(0);
let version_str = if epoch == 0 {
format!("{}-{}", version, release)
} else {
format!("{}:{}-{}", epoch, version, release)
};
raw_fields.push(("version".to_string(), version_str));
raw_fields.push(("arch".to_string(), metadata.get_arch().unwrap_or("unknown").to_string()));
if let Ok(summary) = metadata.get_summary() {
raw_fields.push(("summary".to_string(), summary.to_string()));
}
if let Ok(description) = metadata.get_description() {
raw_fields.push(("description".to_string(), description.to_string().replace("\n", "\n ")));
}
if let Ok(url) = metadata.get_url() {
raw_fields.push(("url".to_string(), url.to_string()));
}
if let Ok(license) = metadata.get_license() {
raw_fields.push(("license".to_string(), license.to_string()));
}
if let Ok(vendor) = metadata.get_vendor() {
raw_fields.push(("vendor".to_string(), vendor.to_string()));
}
if let Ok(group) = metadata.get_group() {
if group != "Unspecified" {
raw_fields.push(("group".to_string(), group.to_string()));
}
}
if let Ok(buildhost) = metadata.get_build_host() {
raw_fields.push(("buildhost".to_string(), buildhost.to_string()));
}
if let Ok(source_rpm) = metadata.get_source_rpm() {
raw_fields.push(("sourcerpm".to_string(), source_rpm.to_string()));
}
if let Ok(packager) = metadata.get_packager() {
raw_fields.push(("packager".to_string(), packager.to_string()));
}
if let Ok(installed_size) = metadata.get_installed_size() {
raw_fields.push(("installed-size".to_string(), installed_size.to_string()));
}
if let Ok(build_time) = metadata.get_build_time() {
raw_fields.push(("time".to_string(), build_time.to_string()));
}
macro_rules! add_dep_field {
($metadata:expr, $method:ident, $field:expr) => {
if let Ok(deps) = $metadata.$method() {
let formatted = format_rpm_dependencies(&deps);
if !formatted.is_empty() {
raw_fields.push(($field.to_string(), formatted));
}
}
};
}
add_dep_field!(metadata, get_provides, "provides");
add_dep_field!(metadata, get_requires, "requires");
add_dep_field!(metadata, get_conflicts, "conflicts");
add_dep_field!(metadata, get_obsoletes, "obsoletes");
add_dep_field!(metadata, get_enhances, "enhances");
add_dep_field!(metadata, get_recommends, "recommends");
add_dep_field!(metadata, get_suggests, "suggests");
add_dep_field!(metadata, get_supplements, "supplements");
if let Ok(file_entries) = metadata.get_file_entries() {
let files: Vec<String> = file_entries.iter()
.map(|file| file.path.to_string_lossy().to_string())
.collect();
if !files.is_empty() {
raw_fields.push(("files".to_string(), files.join(", ")));
}
}
let mut package_fields: HashMap<String, String> = HashMap::new();
for (original_field, value) in raw_fields {
if let Some(mapped_field) = PACKAGE_KEY_MAPPING.get(original_field.as_str()) {
package_fields.insert(mapped_field.to_string(), value);
} else {
log::warn!("Field name '{}' not found in predefined mapping list", original_field);
package_fields.insert(original_field, value);
}
}
let sha256 = crate::store::calculate_file_sha256(rpm_file.as_ref())
.wrap_err_with(|| format!("Failed to calculate SHA256 hash for rpm file: {}", rpm_file.as_ref().display()))?;
package_fields.insert("sha256".to_string(), sha256);
package_fields.insert("format".to_string(), "rpm".to_string());
crate::store::save_package_txt(package_fields, store_tmp_dir, pkgkey)?;
Ok(())
}