use std::collections::HashSet;
use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
use tar::Archive;
use color_eyre::Result;
use crate::lfs;
use crate::utils;
#[cfg(windows)]
fn rel_fs_path_under_fs_root(fs_root: &Path, full: &Path) -> Option<String> {
let rel = full.strip_prefix(fs_root).ok()?;
Some(rel.to_string_lossy().replace('\\', "/"))
}
/// Deferred Windows symlinks with correct file vs directory type, optional env_root (e.g. unpack_tar_archive),
/// and optional global path lookup from [`crate::models::PACKAGE_CACHE::installed_path_lookup_for_unpack`].
#[cfg(windows)]
fn finalize_windows_tar_symlinks(
fs_root: &Path,
created_dirs: &HashSet<PathBuf>,
symlinks: Vec<(PathBuf, PathBuf)>,
env_root_override: Option<&Path>,
) -> Result<()> {
for (link_path, target_path) in symlinks {
let target_full = if target_path.is_relative() {
link_path
.parent()
.map(|p| p.join(&target_path))
.unwrap_or_else(|| link_path.clone())
} else {
fs_root.join(&target_path)
};
let target_full_normalized = lfs::normalize_path_components(&target_full);
let mut is_dir = created_dirs.contains(&target_full_normalized);
if !is_dir {
if let Ok(guard) = crate::models::PACKAGE_CACHE.installed_path_lookup_for_unpack.read() {
if let Some(ref lookup) = *guard {
if let Some(rel) = rel_fs_path_under_fs_root(fs_root, &target_full_normalized) {
if crate::risks::installed_path_is_directory_in_map(lookup, &rel) {
is_dir = true;
}
}
}
}
}
if !is_dir {
// Only check default env root if env_root_override is provided
// to avoid calling env_config() during early initialization (e.g., self install)
let env = if let Some(env) = env_root_override {
Some(env)
} else {
None
};
if let Some(env) = env {
let env_target = if target_path.is_absolute() {
let relative = target_path.strip_prefix("/").unwrap_or(&target_path);
env.join(relative)
} else if let Some(fs_pos) = link_path.to_string_lossy().find("/fs/") {
let after_fs = &link_path.to_string_lossy()[fs_pos + 4..];
if let Some(parent) = Path::new(after_fs).parent() {
env.join(parent).join(&target_path)
} else {
env.join(&target_path)
}
} else {
env.join(&target_path)
};
let decoded_target = lfs::decode_path_from_windows(&env_target);
is_dir = env_target.is_dir() || decoded_target.is_dir();
if is_dir {
log::debug!(
"Symlink target {} found as directory in env_root: {}",
target_path.display(),
env_target.display()
);
}
}
}
log::debug!(
"Creating symlink: {} -> {}, is_dir={}",
link_path.display(),
target_path.display(),
is_dir
);
if is_dir {
lfs::symlink_dir_for_virtiofs(&target_path, &link_path)?;
} else {
lfs::symlink_file_for_virtiofs(&target_path, &link_path)?;
}
}
Ok(())
}
/// Configuration for tar extraction
#[derive(Debug, Clone)]
pub struct ExtractConfig {
/// The base directory where files will be extracted
pub target_dir: PathBuf,
/// Number of leading path components to strip
pub strip_components: usize,
/// Whether to collect hard links for deferred creation
pub handle_hard_links: bool,
/// Directory for metadata files starting with "." (e.g., ".PKGINFO")
pub meta_dir: Option<PathBuf>,
}
impl ExtractConfig {
/// Create a new extract configuration with the target directory
pub fn new<P: AsRef<Path>>(target_dir: P) -> Self {
Self {
target_dir: target_dir.as_ref().to_path_buf(),
strip_components: 0,
handle_hard_links: true,
meta_dir: None,
}
}
/// Set the number of components to strip from paths
#[allow(dead_code)]
pub fn strip_components(mut self, count: usize) -> Self {
self.strip_components = count;
self
}
/// Set whether to handle hard links
pub fn handle_hard_links(mut self, handle: bool) -> Self {
self.handle_hard_links = handle;
self
}
/// Set the metadata directory for dot files
pub fn meta_dir<P: AsRef<Path>>(mut self, dir: P) -> Self {
self.meta_dir = Some(dir.as_ref().to_path_buf());
self
}
}
/// Path classification policy function type
///
/// This function is called for each tar entry to determine:
/// - Where the entry should be extracted (target path)
/// - Whether the entry should be skipped (return None)
///
/// # Arguments
///
/// * `path` - The original path from the tar entry
/// * `is_hard_link` - Whether this entry is a hard link
/// * `store_tmp_dir` - The base store directory
///
/// # Returns
///
/// * `Some(PathBuf)` - The target path where the entry should be extracted
/// * `None` - Skip this entry (don't extract)
pub type PathPolicy =
Box<dyn Fn(&Path, bool, &Path) -> Option<PathBuf>>;
pub fn extract_archive_with_policy<R: Read>(
archive: &mut Archive<R>,
config: &ExtractConfig,
path_policy: PathPolicy,
) -> Result<usize> {
let mut entries_processed = 0;
let mut hard_links: Vec<(PathBuf, PathBuf)> = Vec::new();
let mut created_dirs: HashSet<PathBuf> = HashSet::new();
#[cfg(windows)]
let mut symlinks: Vec<(PathBuf, PathBuf)> = Vec::new();
lfs::create_dir_all_with_case_sensitivity(&config.target_dir)?;
created_dirs.insert(config.target_dir.clone());
for entry_result in archive.entries()? {
let mut entry = entry_result?;
let path = entry.path()?.to_path_buf();
entries_processed += 1;
log::trace!(
"Processing tar entry #{}: {}",
entries_processed,
path.display()
);
let header = entry.header();
let is_hard_link = matches!(header.entry_type(), tar::EntryType::Link);
let is_symlink = matches!(header.entry_type(), tar::EntryType::Symlink);
let mode = header.mode().unwrap_or(0o644);
let is_dir = matches!(header.entry_type(), tar::EntryType::Directory);
let target_path = match (path_policy)(&path, is_hard_link, &config.target_dir) {
Some(tp) => lfs::sanitize_path_for_windows(&tp),
None => continue,
};
if config.handle_hard_links && is_hard_link {
if let Ok(Some(link_path)) = entry.link_name() {
let source_path = match (path_policy)(&link_path, false, &config.target_dir) {
Some(sp) => lfs::sanitize_path_for_windows(&sp),
None => continue,
};
log::trace!(
"Queued hard link: {} -> {}",
target_path.display(),
source_path.display()
);
hard_links.push((source_path, target_path));
continue;
}
}
#[cfg(windows)]
if is_symlink {
if let Ok(Some(link_path)) = entry.link_name() {
let link_target = lfs::sanitize_path_for_windows(&link_path);
symlinks.push((target_path, link_target));
continue;
}
}
if let Some(parent) = target_path.parent() {
if !created_dirs.contains(parent) {
lfs::create_dir_all(parent)?;
created_dirs.insert(parent.to_path_buf());
}
}
#[cfg(not(windows))]
if is_symlink {
}
entry.unpack(&target_path)?;
if !is_symlink {
utils::fixup_file_permissions_with_mode(&target_path, mode, is_dir);
}
if is_dir {
created_dirs.insert(target_path.clone());
}
}
#[cfg(windows)]
finalize_windows_tar_symlinks(&config.target_dir, &created_dirs, symlinks, None)?;
create_hard_links(&hard_links)?;
Ok(entries_processed)
}
pub fn extract_archive<R: Read>(
archive: &mut Archive<R>,
config: &ExtractConfig,
) -> Result<usize> {
let mut entries_processed = 0;
let mut hard_links: Vec<(PathBuf, PathBuf)> = Vec::new();
let mut created_dirs: HashSet<PathBuf> = HashSet::new();
#[cfg(windows)]
let mut symlinks: Vec<(PathBuf, PathBuf)> = Vec::new();
lfs::create_dir_all_with_case_sensitivity(&config.target_dir)?;
created_dirs.insert(config.target_dir.clone());
for entry_result in archive.entries()? {
let mut entry = entry_result?;
let path = entry.path()?.to_path_buf();
entries_processed += 1;
log::trace!("Processing tar entry #{}: {}", entries_processed, path.display());
let target_path = calculate_target_path(&path, config)?;
let header = entry.header();
let is_hard_link = matches!(header.entry_type(), tar::EntryType::Link);
let is_symlink = matches!(header.entry_type(), tar::EntryType::Symlink);
let mode = header.mode().unwrap_or(0o644);
let is_dir = matches!(header.entry_type(), tar::EntryType::Directory);
if config.handle_hard_links {
if is_hard_link {
if let Ok(Some(link_path)) = entry.link_name() {
let source_path = calculate_target_path(&link_path, config)?;
log::trace!(
"Queued hard link: {} -> {}",
target_path.display(),
source_path.display()
);
hard_links.push((source_path, target_path));
continue;
}
}
}
#[cfg(windows)]
if is_symlink {
if let Ok(Some(link_path)) = entry.link_name() {
let link_target = lfs::sanitize_path_for_windows(&link_path);
symlinks.push((target_path, link_target));
continue;
}
}
if let Some(parent) = target_path.parent() {
if !created_dirs.contains(parent) {
lfs::create_dir_all(parent)?;
created_dirs.insert(parent.to_path_buf());
}
}
entry.unpack(&target_path)?;
if !is_symlink {
utils::fixup_file_permissions_with_mode(&target_path, mode, is_dir);
}
if is_dir {
created_dirs.insert(target_path.clone());
}
}
#[cfg(windows)]
finalize_windows_tar_symlinks(&config.target_dir, &created_dirs, symlinks, None)?;
create_hard_links(&hard_links)?;
Ok(entries_processed)
}
fn calculate_target_path(path: &Path, config: &ExtractConfig) -> Result<PathBuf> {
let stripped_path = strip_path_components(path, config.strip_components);
let result = if let Some(ref meta_dir) = config.meta_dir {
if let Some(file_name) = stripped_path.file_name() {
let name_str = file_name.to_string_lossy();
if name_str.starts_with('.') {
return Ok(meta_dir.join(stripped_path));
}
}
if stripped_path.starts_with("info") {
let relative = stripped_path.strip_prefix("info").unwrap_or(&stripped_path);
return Ok(meta_dir.join(relative));
}
config.target_dir.join(&stripped_path)
} else {
config.target_dir.join(&stripped_path)
};
Ok(lfs::sanitize_path_for_windows(&result))
}
fn strip_path_components(path: &Path, components_to_strip: usize) -> PathBuf {
let components: Vec<_> = path.components().collect();
if components.len() <= components_to_strip {
return PathBuf::from(".");
}
components
.into_iter()
.skip(components_to_strip)
.collect()
}
pub fn create_package_dirs<P: AsRef<Path>>(
store_tmp_dir: P,
format: &str,
) -> Result<()> {
let store_tmp_dir = store_tmp_dir.as_ref();
lfs::create_dir_all(store_tmp_dir.join("fs"))?;
lfs::create_dir_all(crate::dirs::path_join(store_tmp_dir, &["info", format]))?;
lfs::create_dir_all(crate::dirs::path_join(store_tmp_dir, &["info", "install"]))?;
Ok(())
}
pub fn unpack_tar_archive<R: Read>(
archive: &mut Archive<R>,
dest: &Path,
env_root: Option<&Path>,
) -> Result<()> {
#[cfg(windows)]
{
lfs::create_dir_all(dest)?;
let mut hard_links: Vec<(PathBuf, PathBuf)> = Vec::new();
let mut symlinks: Vec<(PathBuf, PathBuf)> = Vec::new();
let mut directories: std::collections::HashSet<PathBuf> = std::collections::HashSet::new();
for entry_result in archive.entries()? {
let mut entry = entry_result?;
let entry_path = entry.path()?.to_path_buf();
let sanitized_path = lfs::sanitize_path_for_windows(&entry_path);
if entry_path != sanitized_path {
log::debug!(
"Sanitized tar entry path: '{}' -> '{}'",
entry_path.display(),
sanitized_path.display()
);
}
let dest_path = dest.join(&sanitized_path);
let header = entry.header();
match header.entry_type() {
tar::EntryType::Directory => {
directories.insert(dest_path.clone());
if let Some(parent) = dest_path.parent() {
lfs::create_dir_all(parent)?;
}
entry.unpack(&dest_path)?;
}
tar::EntryType::Link => {
if let Ok(Some(link_path)) = entry.link_name() {
let source_path = dest.join(lfs::sanitize_path_for_windows(&link_path));
hard_links.push((source_path, dest_path));
}
}
tar::EntryType::Symlink => {
if let Ok(Some(link_path)) = entry.link_name() {
let target_path = lfs::sanitize_path_for_windows(&link_path);
symlinks.push((dest_path, target_path));
}
}
_ => {
if let Some(parent) = dest_path.parent() {
lfs::create_dir_all(parent)?;
}
entry.unpack(&dest_path)?;
}
}
}
directories.insert(dest.to_path_buf());
finalize_windows_tar_symlinks(dest, &directories, symlinks, env_root)?;
create_hard_links(&hard_links)?;
}
#[cfg(not(windows))]
{
let _ = env_root;
archive.unpack(dest)?;
}
Ok(())
}
pub fn create_hard_links(links: &[(PathBuf, PathBuf)]) -> Result<()> {
for (source_path, target_path) in links {
if let Some(parent) = target_path.parent() {
if let Err(e) = lfs::create_dir_all(parent) {
log::warn!("Failed to create directory {} for hard link: {}", parent.display(), e);
continue;
}
}
if lfs::exists_on_host(source_path) {
if lfs::exists_on_host(target_path) {
if let Err(e) = lfs::remove_file(target_path) {
log::warn!("Failed to remove existing file {}: {}", target_path.display(), e);
}
}
if let Err(e) = fs::hard_link(source_path, target_path) {
log::warn!("Failed to create hard link from {} to {}: {}",
source_path.display(), target_path.display(), e);
} else {
log::trace!("Created hard link: {} -> {}", target_path.display(), source_path.display());
}
} else {
log::warn!("Cannot create hard link {}: source file {} does not exist",
target_path.display(), source_path.display());
}
}
Ok(())
}