use crate::model::PkgMetadata;
pub const DEFAULT_PAGE_LIMIT: usize = 50;
pub(crate) fn lock_recover<T>(mutex: &std::sync::Mutex<T>) -> std::sync::MutexGuard<'_, T> {
mutex
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
pub const NOISE_EXTENSIONS: &[&str] = &[
".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".bmp", ".html", ".htm", ".css",
];
pub(crate) fn has_noise_extension(path_lower: &str) -> bool {
NOISE_EXTENSIONS.iter().any(|ext| path_lower.ends_with(ext))
}
pub(crate) fn is_noise_ext_bytes(path: &[u8]) -> bool {
let from = path.len().saturating_sub(6);
let tail = &path[from..];
NOISE_EXTENSIONS.iter().any(|ext| {
let ext = ext.as_bytes();
tail.len() >= ext.len() && tail[tail.len() - ext.len()..].eq_ignore_ascii_case(ext)
})
}
pub(crate) fn path_segment_match(path: &str, pattern_lower: &str) -> bool {
if pattern_lower.starts_with('/') {
return path.trim_end_matches('/') == pattern_lower.trim_end_matches('/');
}
let path_lower = path.to_lowercase();
for seg in path_lower.split('/') {
if seg == pattern_lower {
return true;
}
if let Some(rest) = seg.strip_prefix(pattern_lower) {
if rest.is_empty()
|| rest.starts_with('.')
|| rest.starts_with('-')
|| rest.starts_with('_')
{
return true;
}
}
if let Some(rest) = seg.strip_suffix(pattern_lower) {
if rest.is_empty() || rest.ends_with('.') || rest.ends_with('-') || rest.ends_with('_')
{
return true;
}
}
}
false
}
pub(crate) fn arch_weight(arch: &str) -> u8 {
match arch {
"amd64" | "x86_64" | "aarch64" => 0,
"noarch" | "all" => 1,
"i386" | "armhf" | "armel" => 2,
_ => 3,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FetchOutcome {
Online,
LocalFallback(String),
}
pub fn brief_network_reason(err: &str) -> String {
let s = err;
if s.contains("timed out") {
return "响应超时".into();
}
if s.contains("unexpected end of file") {
return "连接中断".into();
}
if s.contains("connection refused") {
return "连接被拒绝".into();
}
if s.contains("dns error") || s.contains("failed to lookup") || s.contains("resolve") {
return "DNS 解析失败".into();
}
if s.contains("status code") {
if let Some(pos) = s.find("status code ") {
let rest = &s[pos + "status code ".len()..];
let code: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
if !code.is_empty() {
let hint = match code.as_str() {
"401" | "403" => "(认证失败)",
"404" => "(路径不存在)",
"5" if code.starts_with('5') => "(服务端错误)",
_ => "",
};
return format!("HTTP {}{}", code, hint);
}
}
}
s.chars().take(50).collect()
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct RefreshStats {
pub total: usize,
pub online: usize,
pub fallback: usize,
pub failed: usize,
}
impl RefreshStats {
pub fn unsuccessful(&self) -> usize {
self.fallback + self.failed
}
pub fn all_online(&self) -> bool {
self.total > 0 && self.unsuccessful() == 0
}
}
#[derive(Debug, Clone)]
pub struct RefreshReport {
pub stats: RefreshStats,
pub package_count: usize,
}
pub struct FetchSummary {
pub packages: Vec<PkgMetadata>,
pub stats: RefreshStats,
pub package_count: usize,
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Arc, Mutex};
#[test]
fn lock_recover_survives_poison() {
let mutex = Arc::new(Mutex::new(7u32));
let cloned = Arc::clone(&mutex);
let _ = std::thread::spawn(move || {
let _guard = cloned.lock().unwrap();
panic!("intentional poison");
})
.join();
assert!(mutex.is_poisoned());
assert_eq!(*lock_recover(&mutex), 7);
}
#[test]
fn arch_weight_orders_primary_over_generic() {
assert_eq!(arch_weight("amd64"), 0);
assert_eq!(arch_weight("x86_64"), 0);
assert_eq!(arch_weight("noarch"), 1);
assert_eq!(arch_weight("all"), 1);
assert_eq!(arch_weight("i386"), 2);
assert_eq!(arch_weight("riscv64"), 3);
}
#[test]
fn shared_path_and_noise_helpers() {
assert!(path_segment_match("/usr/bin/unzip", "unzip"));
assert!(path_segment_match("/usr/bin/unzip-bin", "unzip"));
assert!(!path_segment_match("/usr/bin/unzip2", "unzip"));
assert!(path_segment_match("/usr/bin/lunzip", "/usr/bin/lunzip"));
assert!(has_noise_extension("/usr/share/icons/a.png"));
assert!(!has_noise_extension("/usr/bin/pngview"));
assert!(is_noise_ext_bytes(b"/usr/share/x.PNG"));
}
}