use clap::{Arg, Command};
use color_eyre::Result;
use std::cmp::Ordering;
use crate::models::{PackageFormat};
use crate::busybox::rpm::{pkgline_to_package_and_installed_info, select_installed_packages_by_predicate};
use crate::version_compare::compare_versions;
#[derive(Debug, Clone)]
pub struct DpkgOptions {
pub configure: bool,
#[allow(dead_code)]
pub configure_all: bool,
pub status: bool,
pub list: bool,
pub listfiles: bool,
pub search: bool,
pub print_arch: bool,
pub print_foreign_arch: bool,
pub compare_versions: bool,
pub compare_args: Vec<String>,
pub patterns: Vec<String>,
}
pub fn parse_options(matches: &clap::ArgMatches) -> Result<DpkgOptions> {
let configure = matches.get_flag("configure");
let configure_all = matches.get_flag("configure-all");
let status = matches.get_flag("status");
let list = matches.get_flag("list");
let listfiles = matches.get_flag("listfiles");
let search = matches.get_flag("search");
let print_arch = matches.get_flag("print-architecture");
let print_foreign_arch = matches.get_flag("print-foreign-architectures");
let compare_versions = matches.get_flag("compare-versions");
let trailing: Vec<String> = matches
.get_many::<String>("trailing")
.map(|vals| vals.cloned().collect())
.unwrap_or_default();
let (compare_args, patterns) = if compare_versions && trailing.len() >= 3 {
(trailing[..3].to_vec(), trailing[3..].to_vec())
} else {
(Vec::new(), trailing)
};
Ok(DpkgOptions {
configure,
configure_all,
status,
list,
listfiles,
search,
print_arch,
print_foreign_arch,
compare_versions,
compare_args,
patterns,
})
}
pub fn command() -> Command {
Command::new("dpkg")
.about("Debian package manager compatibility shim (epkg)")
.arg(
Arg::new("configure")
.long("configure")
.action(clap::ArgAction::SetTrue)
.help("No-op in epkg: configuration is handled by epkg itself"),
)
.arg(
Arg::new("configure-all")
.short('a')
.action(clap::ArgAction::SetTrue)
.requires("configure")
.help("Configure all unpacked but unconfigured packages (no-op in epkg)"),
)
.arg(
Arg::new("status")
.short('s')
.action(clap::ArgAction::SetTrue)
.help("Report status of specified package(s)"),
)
.arg(
Arg::new("list")
.short('l')
.action(clap::ArgAction::SetTrue)
.help("List packages matching pattern"),
)
.arg(
Arg::new("listfiles")
.short('L')
.action(clap::ArgAction::SetTrue)
.help("List files installed by package(s)"),
)
.arg(
Arg::new("search")
.short('S')
.action(clap::ArgAction::SetTrue)
.help("Search for which package owns a file"),
)
.arg(
Arg::new("print-architecture")
.long("print-architecture")
.action(clap::ArgAction::SetTrue)
.help("Print primary architecture"),
)
.arg(
Arg::new("print-foreign-architectures")
.long("print-foreign-architectures")
.action(clap::ArgAction::SetTrue)
.help("Print foreign architectures (none in epkg by default)"),
)
.arg(
Arg::new("compare-versions")
.long("compare-versions")
.action(clap::ArgAction::SetTrue)
.help("Compare version numbers (Debian semantics)"),
)
.arg(
Arg::new("trailing")
.value_name("ARGS")
.index(1)
.num_args(0..)
.help("For --compare-versions: VER1 OP VER2; for -s/-l/-L/-S: packages or patterns"),
)
}
fn run_print_arch() {
let cfg = crate::models::config();
println!("{}", cfg.common.arch);
}
fn find_installed_by_name(pkgname: &str) -> Option<(crate::models::Package, crate::models::InstalledPackageInfo)> {
let pkglines = select_installed_packages_by_predicate(
|package, _installed_info| package.pkgname == pkgname,
).ok()?;
let first = pkglines.first()?;
let (pkg, info_arc) = pkgline_to_package_and_installed_info(first)?;
Some((pkg, (*info_arc).clone()))
}
fn print_status_line(pkgname: &str) -> i32 {
if let Some((package, _info)) = find_installed_by_name(pkgname) {
println!("Package: {}", package.pkgname);
println!("Status: install ok installed");
if let Some(section) = package.section {
println!("Section: {}", section);
}
if let Some(priority) = package.priority {
println!("Priority: {}", priority);
}
if package.installed_size != 0 {
println!("Installed-Size: {}", package.installed_size);
}
println!("Architecture: {}", package.arch);
println!("Version: {}", package.version);
println!("Description: {}", package.summary);
if let Some(desc) = package.description {
for line in desc.lines() {
println!(" {}", line);
}
}
0
} else {
eprintln!("dpkg-query: package '{}' is not installed", pkgname);
1
}
}
fn run_status(patterns: &[String]) -> i32 {
if patterns.is_empty() {
eprintln!("dpkg: error: -s needs at least one package name");
return 2;
}
let mut exit_code = 0;
for pkg in patterns {
let code = print_status_line(pkg);
if code != 0 {
exit_code = code;
}
}
exit_code
}
fn run_dpkg_query(args: Vec<String>) -> i32 {
let cmd = crate::busybox::dpkg_query::command();
let matches = match cmd.try_get_matches_from(args.clone()) {
Ok(m) => m,
Err(e) => {
crate::utils::handle_clap_error_with_cmdline(e, args.join(" "));
}
};
let opts = match crate::busybox::dpkg_query::parse_options(&matches) {
Ok(o) => o,
Err(e) => {
eprintln!("dpkg-query: {}", e);
return 2;
}
};
if let Err(e) = crate::busybox::dpkg_query::run(opts) {
eprintln!("dpkg-query: {}", e);
return 2;
}
0
}
fn run_list(patterns: &[String]) -> i32 {
let mut args = vec!["dpkg-query".to_string(), "-l".to_string()];
args.extend(patterns.iter().cloned());
run_dpkg_query(args)
}
fn run_listfiles(patterns: &[String]) -> i32 {
if patterns.is_empty() {
eprintln!("dpkg: error: -L needs at least one package name");
return 2;
}
let mut args = vec!["dpkg-query".to_string(), "-L".to_string()];
args.extend(patterns.iter().cloned());
run_dpkg_query(args)
}
fn run_search(patterns: &[String]) -> i32 {
if patterns.is_empty() {
eprintln!("dpkg: error: -S needs at least one pattern");
return 2;
}
let mut args = vec!["dpkg-query".to_string(), "-S".to_string()];
args.push(patterns[0].clone());
run_dpkg_query(args)
}
fn eval_compare_op(ord: Ordering, op: &str) -> Option<bool> {
match op {
"lt" => Some(ord == Ordering::Less),
"le" => Some(ord != Ordering::Greater),
"gt" => Some(ord == Ordering::Greater),
"ge" => Some(ord != Ordering::Less),
"eq" => Some(ord == Ordering::Equal),
"ne" => Some(ord != Ordering::Equal),
_ => None,
}
}
fn eval_compare_op_nl(ver1: &str, ver2: &str, op: &str, ord: Ordering) -> Option<bool> {
let v1_empty = ver1.is_empty();
let v2_empty = ver2.is_empty();
match op {
"lt-nl" => {
if v1_empty && !v2_empty {
Some(true)
} else if v1_empty && v2_empty {
Some(false)
} else if !v1_empty && v2_empty {
Some(false)
} else {
Some(ord == Ordering::Less)
}
}
"le-nl" => {
if v1_empty && !v2_empty {
Some(true)
} else if v1_empty && v2_empty {
Some(true)
} else if !v1_empty && v2_empty {
Some(false)
} else {
Some(ord != Ordering::Greater)
}
}
_ => None,
}
}
fn run_compare_versions(args: &[String]) -> i32 {
if args.len() != 3 {
eprintln!("dpkg: error: --compare-versions needs exactly three arguments");
return 2;
}
let ver1 = &args[0];
let op = &args[1];
let ver2 = &args[2];
let cmp = match compare_versions(ver1, ver2, PackageFormat::Deb) {
Some(o) => o,
None => {
eprintln!("dpkg: error: failed to parse versions '{}' and '{}'", ver1, ver2);
return 2;
}
};
let result = if op.ends_with("-nl") {
eval_compare_op_nl(ver1, ver2, op, cmp)
} else {
eval_compare_op(cmp, op)
};
match result {
Some(true) => 0,
Some(false) => 1,
None => {
eprintln!("dpkg: error: unsupported comparison operator '{}'", op);
2
}
}
}
pub fn run(options: DpkgOptions) -> Result<()> {
if options.compare_versions {
let code = run_compare_versions(&options.compare_args);
std::process::exit(code);
}
if options.print_arch {
run_print_arch();
return Ok(());
}
if options.print_foreign_arch {
return Ok(());
}
if options.configure {
return Ok(());
}
if options.status {
let code = run_status(&options.patterns);
std::process::exit(code);
}
if options.list {
let code = run_list(&options.patterns);
std::process::exit(code);
}
if options.listfiles {
let code = run_listfiles(&options.patterns);
std::process::exit(code);
}
if options.search {
let code = run_search(&options.patterns);
std::process::exit(code);
}
eprintln!("dpkg: error: need an action option");
eprintln!("Try 'dpkg --help' for more information.");
std::process::exit(2);
}