use super::backup::get_backup_files;
use super::lists::{get_installed_file_list, get_remote_file_list};
use crate::state::modal::{FileChange, FileChangeType, PackageFileInfo};
use crate::state::types::Source;
use std::collections::{HashMap, HashSet};
use std::process::Command;
#[must_use]
pub fn batch_get_remote_file_lists(packages: &[(&str, &Source)]) -> HashMap<String, Vec<String>> {
const BATCH_SIZE: usize = 50;
let mut result_map = HashMap::new();
let mut repo_groups: HashMap<String, Vec<&str>> = HashMap::new();
for (name, source) in packages {
if let Source::Official { repo, .. } = source {
let repo_key = if repo.is_empty() {
String::new()
} else {
repo.clone()
};
repo_groups.entry(repo_key).or_default().push(name);
}
}
for (repo, names) in repo_groups {
for chunk in names.chunks(BATCH_SIZE) {
let specs: Vec<String> = chunk
.iter()
.map(|name| {
if repo.is_empty() {
(*name).to_string()
} else {
format!("{repo}/{name}")
}
})
.collect();
let mut args = vec!["-Fl"];
args.extend(specs.iter().map(String::as_str));
match Command::new("pacman")
.args(&args)
.env("LC_ALL", "C")
.env("LANG", "C")
.output()
{
Ok(output) if output.status.success() => {
let text = String::from_utf8_lossy(&output.stdout);
let mut pkg_files: HashMap<String, Vec<String>> = HashMap::new();
for line in text.lines() {
if let Some((pkg, path)) = line.split_once(' ') {
let pkg_name = if let Some((_, name)) = pkg.split_once('/') {
name
} else {
pkg
};
pkg_files
.entry(pkg_name.to_string())
.or_default()
.push(path.to_string());
}
}
result_map.extend(pkg_files);
}
_ => {
break;
}
}
}
}
result_map
}
pub fn resolve_package_files(
name: &str,
source: &Source,
action: crate::state::modal::PreflightAction,
) -> Result<PackageFileInfo, String> {
match action {
crate::state::modal::PreflightAction::Install => resolve_install_files(name, source),
crate::state::modal::PreflightAction::Remove => resolve_remove_files(name),
crate::state::modal::PreflightAction::Downgrade => resolve_downgrade_files(name, source),
}
}
pub fn resolve_install_files(name: &str, source: &Source) -> Result<PackageFileInfo, String> {
let remote_files = get_remote_file_list(name, source)?;
resolve_install_files_with_remote_list(name, source, remote_files)
}
pub fn resolve_install_files_with_remote_list(
name: &str,
source: &Source,
remote_files: Vec<String>,
) -> Result<PackageFileInfo, String> {
let installed_files = get_installed_file_list(name).unwrap_or_default();
let installed_set: HashSet<&str> = installed_files.iter().map(String::as_str).collect();
let mut file_changes = Vec::new();
let mut new_count = 0;
let mut changed_count = 0;
let mut config_count = 0;
let mut pacnew_candidates = 0;
let backup_files = get_backup_files(name, source).unwrap_or_default();
let backup_set: HashSet<&str> = backup_files.iter().map(String::as_str).collect();
for path in remote_files {
let is_config = path.starts_with("/etc/");
let is_dir = path.ends_with('/');
if is_dir {
continue;
}
let change_type = if installed_set.contains(path.as_str()) {
changed_count += 1;
FileChangeType::Changed
} else {
new_count += 1;
FileChangeType::New
};
if is_config {
config_count += 1;
}
let predicted_pacnew = backup_set.contains(path.as_str())
&& installed_set.contains(path.as_str())
&& is_config;
if predicted_pacnew {
pacnew_candidates += 1;
}
file_changes.push(FileChange {
path,
change_type,
package: name.to_string(),
is_config,
predicted_pacnew,
predicted_pacsave: false, });
}
file_changes.sort_by(|a, b| a.path.cmp(&b.path));
Ok(PackageFileInfo {
name: name.to_string(),
files: file_changes,
total_count: new_count + changed_count,
new_count,
changed_count,
removed_count: 0,
config_count,
pacnew_candidates,
pacsave_candidates: 0,
})
}
pub fn resolve_remove_files(name: &str) -> Result<PackageFileInfo, String> {
let installed_files = get_installed_file_list(name)?;
let mut file_changes = Vec::new();
let mut config_count = 0;
let mut pacsave_candidates = 0;
let backup_files = get_backup_files(
name,
&Source::Official {
repo: String::new(),
arch: String::new(),
},
)
.unwrap_or_default();
let backup_set: HashSet<&str> = backup_files.iter().map(String::as_str).collect();
for path in installed_files {
let is_config = path.starts_with("/etc/");
let is_dir = path.ends_with('/');
if is_dir {
continue;
}
if is_config {
config_count += 1;
}
let predicted_pacsave = backup_set.contains(path.as_str()) && is_config;
if predicted_pacsave {
pacsave_candidates += 1;
}
file_changes.push(FileChange {
path,
change_type: FileChangeType::Removed,
package: name.to_string(),
is_config,
predicted_pacnew: false,
predicted_pacsave,
});
}
file_changes.sort_by(|a, b| a.path.cmp(&b.path));
let removed_count = file_changes.len();
Ok(PackageFileInfo {
name: name.to_string(),
files: file_changes,
total_count: removed_count,
new_count: 0,
changed_count: 0,
removed_count,
config_count,
pacnew_candidates: 0,
pacsave_candidates,
})
}
pub fn resolve_downgrade_files(name: &str, source: &Source) -> Result<PackageFileInfo, String> {
let remote_files = get_remote_file_list(name, source)?;
let installed_files = get_installed_file_list(name)?;
let normalize_path = |p: &str| p.trim_end_matches('/').to_string();
let installed_set: HashSet<String> =
installed_files.iter().map(|p| normalize_path(p)).collect();
let remote_set: HashSet<String> = remote_files.iter().map(|p| normalize_path(p)).collect();
let backup_files = get_backup_files(name, source).unwrap_or_default();
let backup_set: HashSet<String> = backup_files.iter().map(|p| normalize_path(p)).collect();
let mut file_changes = Vec::new();
let mut changed_count = 0;
let mut new_count = 0;
let mut config_count = 0;
let mut pacnew_candidates = 0;
for path in installed_files {
let normalized_path = normalize_path(&path);
let is_config = path.starts_with("/etc/");
let is_dir = path.ends_with('/');
if is_dir {
continue;
}
if is_config {
config_count += 1;
}
if remote_set.contains(&normalized_path) {
changed_count += 1;
let predicted_pacnew = backup_set.contains(&normalized_path) && is_config;
if predicted_pacnew {
pacnew_candidates += 1;
}
file_changes.push(FileChange {
path,
change_type: FileChangeType::Changed,
package: name.to_string(),
is_config,
predicted_pacnew,
predicted_pacsave: false,
});
}
else {
file_changes.push(FileChange {
path,
change_type: FileChangeType::Removed,
package: name.to_string(),
is_config,
predicted_pacnew: false,
predicted_pacsave: backup_set.contains(&normalized_path) && is_config,
});
}
}
for path in remote_files {
let normalized_path = normalize_path(&path);
let is_config = path.starts_with("/etc/");
let is_dir = path.ends_with('/');
if is_dir {
continue;
}
if !installed_set.contains(&normalized_path) {
new_count += 1;
file_changes.push(FileChange {
path,
change_type: FileChangeType::New,
package: name.to_string(),
is_config,
predicted_pacnew: false,
predicted_pacsave: false,
});
}
}
file_changes.sort_by(|a, b| a.path.cmp(&b.path));
let removed_count = file_changes
.iter()
.filter(|f| matches!(f.change_type, FileChangeType::Removed))
.count();
Ok(PackageFileInfo {
name: name.to_string(),
files: file_changes,
total_count: changed_count + new_count + removed_count,
new_count,
changed_count,
removed_count,
config_count,
pacnew_candidates,
pacsave_candidates: 0,
})
}