use std::fs;
use std::io::{self, Read};
use std::path::Path;
use std::collections::HashMap;
use tar::Archive;
use log;
use flate2::read::GzDecoder;
use liblzma::read::XzDecoder;
use color_eyre::Result;
use color_eyre::eyre::{self, WrapErr};
use crate::deb_repo::PACKAGE_KEY_MAPPING;
use crate::lfs;
use crate::tar_extract::{create_package_dirs, unpack_tar_archive};
pub fn unpack_package<P: AsRef<Path>>(deb_file: P, store_tmp_dir: P, pkgkey: Option<&str>) -> Result<()> {
let deb_file = deb_file.as_ref();
let store_tmp_dir = store_tmp_dir.as_ref();
create_package_dirs(store_tmp_dir, "deb")?;
extract_ar_archive(deb_file, store_tmp_dir)?;
crate::store::create_filelist_txt(store_tmp_dir)?;
create_scriptlets(store_tmp_dir)?;
let (interest_triggers, activate_triggers) = crate::deb_triggers::read_package_triggers(store_tmp_dir)?;
crate::deb_triggers::write_deb_trigger_hooks(&interest_triggers, &activate_triggers, store_tmp_dir)?;
create_package_txt(deb_file, store_tmp_dir, pkgkey)?;
Ok(())
}
fn extract_ar_archive<P: AsRef<Path>>(deb_file: P, store_tmp_dir: P) -> Result<()> {
let deb_file = deb_file.as_ref();
let store_tmp_dir = store_tmp_dir.as_ref();
let file = fs::File::open(deb_file)
.wrap_err_with(|| format!("Failed to open deb file: {}", deb_file.display()))?;
let mut archive = ar::Archive::new(file);
let mut data_tar_path = None;
let mut control_tar_path = None;
while let Some(entry_result) = archive.next_entry() {
let mut entry = entry_result
.wrap_err("Failed to read AR archive entry")?;
let header = entry.header().clone();
let identifier = std::str::from_utf8(header.identifier())
.wrap_err("Invalid UTF-8 in AR entry identifier")?;
match identifier {
"data.tar.gz" | "data.tar.xz" | "data.tar.zst" | "data.tar" => {
let temp_path = store_tmp_dir.join(identifier);
let mut temp_file = lfs::file_create(&temp_path)?;
io::copy(&mut entry, &mut temp_file)?;
data_tar_path = Some(temp_path);
}
"control.tar.gz" | "control.tar.xz" | "control.tar.zst" | "control.tar" => {
let temp_path = store_tmp_dir.join(identifier);
let mut temp_file = lfs::file_create(&temp_path)?;
io::copy(&mut entry, &mut temp_file)?;
control_tar_path = Some(temp_path);
}
_ => {
continue;
}
}
}
if let Some(data_tar) = data_tar_path {
extract_tar(&data_tar, &store_tmp_dir.join("fs"))?;
lfs::remove_file(&data_tar)?;
} else {
return Err(eyre::eyre!("No data.tar found in deb archive"));
}
if let Some(control_tar) = control_tar_path {
extract_tar(&control_tar, &crate::dirs::path_join(store_tmp_dir, &["info", "deb"]))?;
lfs::remove_file(&control_tar)?;
} else {
return Err(eyre::eyre!("No control.tar found in deb archive"));
}
Ok(())
}
fn extract_tar<P: AsRef<Path>>(tar_path: P, target_dir: P) -> Result<()> {
let tar_path = tar_path.as_ref();
let target_dir = target_dir.as_ref();
lfs::create_dir_all(target_dir)?;
let file = fs::File::open(tar_path)?;
let filename = tar_path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("");
let reader: Box<dyn Read> = if filename.ends_with(".gz") {
Box::new(GzDecoder::new(file))
} else if filename.ends_with(".xz") {
Box::new(XzDecoder::new(file))
} else if filename.ends_with(".zst") {
Box::new(zstd::stream::Decoder::new(file)?)
} else {
Box::new(file)
};
let mut archive = Archive::new(reader);
unpack_tar_archive(&mut archive, target_dir, None)
.wrap_err_with(|| format!("Failed to extract tar archive: {}", tar_path.display()))?;
Ok(())
}
fn create_scriptlets<P: AsRef<Path>>(store_tmp_dir: P) -> Result<()> {
let store_tmp_dir = store_tmp_dir.as_ref();
let deb_dir = crate::dirs::path_join(store_tmp_dir, &["info", "deb"]);
let install_dir = crate::dirs::path_join(store_tmp_dir, &["info", "install"]);
let scriptlet_mapping: HashMap<&str, &str> = [
("preinst", "pre_install.sh"),
("postinst", "post_install.sh"),
("prerm", "pre_uninstall.sh"),
("postrm", "post_uninstall.sh"),
].into_iter().collect();
crate::utils::copy_scriptlets_by_mapping(&scriptlet_mapping, &deb_dir, &install_dir, true)?;
Ok(())
}
fn create_package_txt<P: AsRef<Path>>(deb_file: P, store_tmp_dir: P, pkgkey: Option<&str>) -> Result<()> {
let deb_file = deb_file.as_ref();
let store_tmp_dir = store_tmp_dir.as_ref();
let control_path = crate::dirs::path_join(store_tmp_dir, &["info", "deb", "control"]);
if !lfs::exists_on_host(&control_path) {
return Err(eyre::eyre!("Control file not found: {}", control_path.display()));
}
let control_content = fs::read_to_string(&control_path)?;
let mut raw_fields: Vec<(String, String)> = Vec::new();
let mut current_field = None;
let mut current_value = String::new();
for line in control_content.lines() {
if line.is_empty() {
continue;
}
if line.starts_with(' ') || line.starts_with('\t') {
if !current_value.is_empty() {
current_value.push('\n');
}
current_value.push_str(line.trim());
} else if let Some((key, value)) = line.split_once(": ") {
if let Some(field_name) = current_field.take() {
raw_fields.push((field_name, current_value.clone()));
}
current_field = Some(key.to_string());
current_value = value.to_string();
}
}
if let Some(field_name) = current_field {
raw_fields.push((field_name, current_value));
}
let mut package_fields: HashMap<String, String> = HashMap::new();
for (original_field, value) in raw_fields {
if original_field == "Description" {
let lines: Vec<&str> = value.lines().collect();
if !lines.is_empty() {
package_fields.insert("summary".to_string(), lines[0].to_string());
if lines.len() > 1 {
let description_lines = &lines[1..];
let description_content = description_lines.join("\n");
let indented_description = description_content.replace("\n", "\n ");
package_fields.insert("description".to_string(), indented_description);
}
}
} else if let Some(mapped_field) = PACKAGE_KEY_MAPPING.get(original_field.as_str()) {
let mut current_value = value;
if *mapped_field == "installedSize" {
current_value.push_str("000");
}
package_fields.insert(mapped_field.to_string(), current_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(deb_file)
.wrap_err_with(|| format!("Failed to calculate SHA256 hash for deb file: {}", deb_file.display()))?;
package_fields.insert("sha256".to_string(), sha256);
package_fields.insert("format".to_string(), "deb".to_string());
crate::store::save_package_txt(package_fields, store_tmp_dir, pkgkey)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_description_field_splitting() {
let temp_dir = TempDir::new().unwrap();
let store_tmp_dir = temp_dir.path();
let deb_dir = crate::dirs::path_join(store_tmp_dir, &["info", "deb"]);
lfs::create_dir_all(&deb_dir).unwrap();
let control_content = r#"Package: base-passwd
Version: 3.6.3
Priority: required
Section: admin
Maintainer: Colin Watson <cjwatson@debian.org>
Description: Debian base system master password and group files
These are the canonical master copies of the user database files
(/etc/passwd and /etc/group), containing the Debian-allocated user and
group IDs. The update-passwd tool is provided to keep the system databases
synchronized with these master files.
Architecture: all
"#;
let control_path = deb_dir.join("control");
lfs::write(&control_path, control_content).unwrap();
let mock_deb_file = store_tmp_dir.join("mock.deb");
lfs::write(&mock_deb_file, b"mock deb file content").unwrap();
let store_tmp_dir_buf = store_tmp_dir.to_path_buf();
create_package_txt(&mock_deb_file, &store_tmp_dir_buf, None).unwrap();
let package_txt_path = crate::dirs::path_join(store_tmp_dir, &["info", "package.txt"]);
assert!(package_txt_path.exists());
let package_txt_content = fs::read_to_string(&package_txt_path).unwrap();
println!("Generated package.txt content:\n{}", package_txt_content);
assert!(package_txt_content.contains("summary: Debian base system master password and group files"));
assert!(package_txt_content.contains("description: These are the canonical master copies of the user database files"));
assert!(package_txt_content.contains(" (/etc/passwd and /etc/group), containing the Debian-allocated user and"));
assert!(package_txt_content.contains(" group IDs. The update-passwd tool is provided to keep the system databases"));
assert!(package_txt_content.contains(" synchronized with these master files."));
}
}