use crate::backend::PkgBackend;
use crate::cli::CacheCmd;
use crate::engine::{Ctx, ExitStatus};
use crate::error::{PkgError, Result};
use crate::i18n::Language;
use crate::model::*;
use crate::{backend, output, path_util};
pub(super) fn lang() -> Language {
Language::current()
}
pub(super) fn format_time_ago(secs: u64) -> String {
let l = lang();
if secs < 60 {
match l {
Language::Zh => format!("{}秒", secs),
Language::En => format!("{} second{}", secs, if secs == 1 { "" } else { "s" }),
}
} else if secs < 3600 {
let m = secs / 60;
let s = secs % 60;
match l {
Language::Zh => format!("{}分{}秒", m, s),
Language::En => format!(
"{} minute{} {} second{}",
m,
if m == 1 { "" } else { "s" },
s,
if s == 1 { "" } else { "s" }
),
}
} else if secs < 86400 {
let h = secs / 3600;
let m = (secs % 3600) / 60;
match l {
Language::Zh => format!("{}小时{}分", h, m),
Language::En => format!(
"{} hour{} {} minute{}",
h,
if h == 1 { "" } else { "s" },
m,
if m == 1 { "" } else { "s" }
),
}
} else {
let d = secs / 86400;
let h = (secs % 86400) / 3600;
match l {
Language::Zh => format!("{}天{}小时", d, h),
Language::En => format!(
"{} day{} {} hour{}",
d,
if d == 1 { "" } else { "s" },
h,
if h == 1 { "" } else { "s" }
),
}
}
}
fn format_full_timestamp(ts: i64) -> String {
if ts == 0 {
return String::new();
}
let l = lang();
let (year, month, day, h, m, s, weekday_idx) = local_time_parts(ts);
match l {
Language::Zh => {
let wd = ["日", "一", "二", "三", "四", "五", "六"];
format!(
"{}年{:02}月{:02}日 星期{} {:02}:{:02}:{:02}",
year, month, day, wd[weekday_idx], h, m, s
)
}
Language::En => {
let wd = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
let mon = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
];
format!(
"{} {} {} {:02}:{:02}:{:02} {}",
wd[weekday_idx],
mon[(month - 1) as usize],
day,
h,
m,
s,
year
)
}
}
}
fn local_time_parts(ts: i64) -> (i64, u32, u32, u32, u32, u32, usize) {
use std::os::raw::c_int;
#[repr(C)]
struct Tm {
tm_sec: c_int,
tm_min: c_int,
tm_hour: c_int,
tm_mday: c_int,
tm_mon: c_int, tm_year: c_int, tm_wday: c_int, tm_yday: c_int,
tm_isdst: c_int,
tm_gmtoff: i64,
tm_zone: *const u8,
}
extern "C" {
fn localtime_r(timep: *const i64, result: *mut Tm) -> *mut Tm;
}
let tm = unsafe {
let t: i64 = ts;
let mut tm: Tm = std::mem::zeroed();
if localtime_r(&t as *const i64 as *const _, &mut tm as *mut Tm).is_null() {
return (1970, 1, 1, 0, 0, 0, 4);
}
tm
};
(
tm.tm_year as i64 + 1900,
(tm.tm_mon + 1) as u32,
tm.tm_mday as u32,
tm.tm_hour as u32,
tm.tm_min as u32,
tm.tm_sec as u32,
tm.tm_wday.max(0) as usize % 7,
)
}
pub(super) fn print_metadata_check() {
let l = lang();
let mtime = get_repo_metadata_mtime();
if mtime == 0 {
return;
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let ago = format_time_ago(now.saturating_sub(mtime));
let ts = format_full_timestamp(mtime as i64);
eprintln!("{}", l.metadata_check(&ago, &ts));
}
fn get_repo_metadata_mtime() -> u64 {
use std::path::Path;
let own = crate::cache::cache_dir_mtime();
if own > 0 {
return own;
}
let candidates: &[&str] = &["/var/cache/dnf", "/var/cache/yum"];
for c in candidates {
let p = shellexpand(c);
if Path::new(&p).exists() {
if let Ok(m) = std::fs::metadata(&p) {
if let Ok(t) = m.modified() {
let s = t
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
if s > 0 {
return s;
}
}
}
}
}
if let Ok(entries) = std::fs::read_dir("/var/lib/apt/lists") {
let mut max_mtime: u64 = 0;
for entry in entries.flatten() {
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str.ends_with("_Packages") || name_str.ends_with("_Contents-") {
if let Ok(m) = entry.metadata() {
if let Ok(t) = m.modified() {
let s = t
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
if s > max_mtime {
max_mtime = s;
}
}
}
}
}
if max_mtime > 0 {
return max_mtime;
}
}
let stamp = Path::new("/var/lib/apt/periodic/update-success-stamp");
if let Ok(m) = std::fs::metadata(stamp) {
if let Ok(t) = m.modified() {
return t
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
}
}
0
}
fn shellexpand(path: &str) -> String {
if let Some(rest) = path.strip_prefix("~/") {
if let Some(home) = dirs::home_dir() {
return home.join(rest).to_string_lossy().to_string();
}
}
path.to_string()
}
pub(super) fn show_banner(shown: &mut bool) {
if !*shown {
print_metadata_check();
*shown = true;
}
}
pub(super) fn install_cmd(backend: &dyn PkgBackend, name: &str) -> String {
match backend.system_type() {
PackageSystem::Rpm => format!("dnf install {}", name),
PackageSystem::Deb => format!("apt-get install {}", name),
}
}
pub(super) fn word_boundary_contains(hay_lower: &str, needle_lower: &str) -> bool {
if needle_lower.is_empty() {
return false;
}
let mut start = 0;
while let Some(pos) = hay_lower[start..].find(needle_lower) {
let abs = start + pos;
let end = abs + needle_lower.len();
let before_ok = hay_lower[..abs]
.chars()
.next_back()
.is_none_or(|c| !c.is_ascii_alphanumeric());
let after_ok = hay_lower[end..]
.chars()
.next()
.is_none_or(|c| !c.is_ascii_alphanumeric());
if before_ok && after_ok {
return true;
}
start = end;
}
false
}
pub(super) fn supplement_source_pkg(pkg: &mut PkgMetadata) {
if pkg.source_pkg.is_none() {
pkg.source_pkg = Some(pkg.name.clone());
}
}
pub(super) fn is_path_query(pattern: &str) -> bool {
pattern.starts_with('/') || path_util::contains_glob(pattern)
}
pub(super) fn keyword_search(
backend: &dyn PkgBackend,
pattern: &str,
use_regex: bool,
source: PackageSource,
cfg: &CacheConfig,
) -> Result<Vec<SearchResult>> {
let packages = backend.search_packages(pattern, source, cfg)?;
let pl = pattern.to_lowercase();
Ok(packages
.iter()
.filter_map(|p| {
let matched = if use_regex {
regex::Regex::new(pattern)
.ok()
.map(|re| re.is_match(&p.name) || re.is_match(&p.summary))
.unwrap_or(false)
} else {
p.name.to_lowercase().contains(&pl) || p.summary.to_lowercase().contains(&pl)
};
if matched {
Some(SearchResult {
pkg_name: p.name.clone(),
matched_text: p.summary.clone(),
match_type: "keyword".to_string(),
source: match source {
PackageSource::Installed => "installed",
PackageSource::Repo => "repo",
}
.to_string(),
})
} else {
None
}
})
.collect())
}
pub(super) fn handle_file_query(
ctx: &mut Ctx,
pattern: &str,
source: PackageSource,
show_all: bool,
) -> Result<ExitStatus> {
let l = lang();
let Ctx {
backend, cfg, fmt, ..
} = ctx;
if path_util::contains_glob(pattern) {
let regex_pattern = path_util::glob_to_regex(pattern);
let local_results = backend.search_by_pattern(®ex_pattern, true, source, cfg)?;
if local_results.is_empty() {
eprintln!("{}", l.no_files_matched());
return Ok(ExitStatus::NotFound);
}
let mut grouped: std::collections::BTreeMap<String, Vec<String>> =
std::collections::BTreeMap::new();
for r in &local_results {
grouped
.entry(r.pkg_name.clone())
.or_default()
.push(r.matched_text.clone());
}
output::print_file_owners_grouped(&grouped, *fmt)?;
return Ok(ExitStatus::Hit);
}
let candidates = path_util::normalize_path(pattern);
let resolved_path = &candidates[0];
if source == PackageSource::Installed && !std::path::Path::new(resolved_path).exists() {
eprintln!("{}", l.path_not_found(pattern));
return Ok(ExitStatus::NotFound);
}
let metadata = std::fs::metadata(resolved_path);
let is_dir = metadata.as_ref().map(|m| m.is_dir()).unwrap_or(false);
let mut owners = Vec::new();
for candidate in &candidates {
owners = backend.find_file_owner(candidate, source, cfg)?;
if !owners.is_empty() {
break;
}
}
if owners.is_empty() {
eprintln!("{}", l.no_owner());
return Ok(ExitStatus::NotFound);
}
let source_str = match source {
PackageSource::Installed => "installed",
PackageSource::Repo => "repo",
};
let total = owners.len();
let default_limit = crate::backend::common::DEFAULT_PAGE_LIMIT;
let truncated = !show_all && total > default_limit;
let shown: Vec<String> = if truncated {
owners.into_iter().take(default_limit).collect()
} else {
owners
};
let items: Vec<(String, String)> = shown
.into_iter()
.map(|n| (n, source_str.to_string()))
.collect();
if is_dir && items.len() > 1 {
output::print_directory_owners(&items, resolved_path, total, *fmt)?;
} else {
output::print_file_owners(&items, resolved_path, *fmt)?;
}
if truncated {
eprintln!("{}", l.limit_note(default_limit));
}
Ok(ExitStatus::Hit)
}
pub(super) fn handle_cache(ctx: &mut Ctx, cmd: &CacheCmd) -> Result<ExitStatus> {
let Ctx {
backend, cfg, fmt, ..
} = ctx;
match cmd {
CacheCmd::Status => {
let items = crate::cache::cache_status();
let total: u64 = items.iter().map(|(_, s)| s).sum();
let dir = dirs::cache_dir().map(|d| d.join("pkq")).unwrap_or_default();
output::print_cache_status(&dir.to_string_lossy(), &items, total, *fmt)?;
Ok(ExitStatus::Hit)
}
CacheCmd::Update => {
if cfg.offline_mode {
eprintln!("{}", lang().metadata_refresh_offline());
return Ok(ExitStatus::NotFound);
}
let start = std::time::Instant::now();
match backend.refresh_metadata(cfg) {
Ok(report) => {
let secs = start.elapsed().as_secs_f64();
println!("{}", lang().metadata_refreshed(report.package_count, secs));
if report.stats.all_online() {
Ok(ExitStatus::Hit)
} else {
let s = report.stats;
eprintln!("{}", lang().metadata_refresh_partial(s.fallback, s.failed));
Ok(ExitStatus::NotFound)
}
}
Err(_) => {
eprintln!("{}", lang().metadata_refresh_failed());
Ok(ExitStatus::NotFound)
}
}
}
CacheCmd::Clean { target, yes } => {
if !matches!(target.as_str(), "all" | "index" | "repos" | "contents") {
return Err(PkgError::InvalidArgument(format!(
"未知清理目标: {}(可选 all|index|repos|contents)",
target
)));
}
let target_size = crate::cache::cache_target_size(target);
if !*yes {
let l = lang();
eprintln!("{}", l.cache_clean_confirm(target, target_size));
return Ok(ExitStatus::NotFound);
}
let freed = crate::cache::cache_clean(target)?;
let l = lang();
println!("{}", l.cache_cleaned(freed));
Ok(ExitStatus::Hit)
}
}
}
pub(super) fn detect_backend() -> Result<Box<dyn PkgBackend>> {
let system = detect_system();
tracing::debug!("detected package system: {:?}", system);
match system {
Some(PackageSystem::Deb) => Ok(Box::new(backend::deb::DebBackend::new())),
Some(PackageSystem::Rpm) => Ok(Box::new(backend::rpm::RpmBackend::new())),
None => Err(PkgError::InvalidArgument(
"不支持的系统:既未找到 RPM 也未找到 DEB".into(),
)),
}
}
pub(super) fn detect_system() -> Option<PackageSystem> {
if let Some(sys) = system_from_os_release() {
return Some(sys);
}
let rpm_dbs = [
"/var/lib/rpm/rpmdb.sqlite",
"/var/lib/rpm/Packages.db",
"/var/lib/rpm/Packages",
];
if rpm_dbs.iter().any(|p| std::path::Path::new(p).exists()) {
return Some(PackageSystem::Rpm);
}
if std::path::Path::new("/var/lib/dpkg/status").exists() {
return Some(PackageSystem::Deb);
}
None
}
fn system_from_os_release() -> Option<PackageSystem> {
let content = std::fs::read_to_string("/etc/os-release").ok()?;
let mut id = String::new();
let mut id_like = String::new();
for line in content.lines() {
if let Some(v) = line.strip_prefix("ID=") {
id = v.trim_matches('"').to_lowercase();
} else if let Some(v) = line.strip_prefix("ID_LIKE=") {
id_like = v.trim_matches('"').to_lowercase();
}
}
system_from_id_tokens(&id, &id_like)
}
pub(super) fn system_from_id_tokens(id: &str, id_like: &str) -> Option<PackageSystem> {
let tokens: Vec<&str> = id_like
.split_whitespace()
.chain(std::iter::once(id))
.collect();
const DEB_FAMILY: &[&str] = &["debian", "ubuntu", "deepin"];
const RPM_FAMILY: &[&str] = &[
"fedora",
"rhel",
"centos",
"suse",
"opensuse",
"anolis",
"opencloudos",
"euleros",
"openeuler",
"fedora-like",
];
if tokens.iter().any(|t| DEB_FAMILY.contains(t)) {
return Some(PackageSystem::Deb);
}
if tokens.iter().any(|t| RPM_FAMILY.contains(t)) {
return Some(PackageSystem::Rpm);
}
None
}
const COMMON_DIRS: &[&str] = &[
"/",
"/.",
"/..",
"/usr",
"/usr/bin",
"/usr/sbin",
"/usr/lib",
"/usr/lib32",
"/usr/lib64",
"/usr/libx32",
"/usr/share",
"/usr/share/doc",
"/usr/share/man",
"/usr/share/locale",
"/usr/share/lintian",
"/usr/share/bug",
"/etc",
"/etc/default",
"/etc/init.d",
"/bin",
"/sbin",
"/lib",
"/lib32",
"/lib64",
"/libx32",
"/var",
"/var/lib",
"/var/cache",
"/opt",
];
pub(super) fn is_common_dir(path: &str) -> bool {
let p = path.trim_end_matches('/');
p.is_empty() || COMMON_DIRS.contains(&p) || COMMON_DIRS.contains(&path)
}
pub(super) fn filter_common_dirs(files: &[String]) -> Vec<String> {
files
.iter()
.filter(|f| !is_common_dir(f))
.cloned()
.collect()
}