pkq 0.2.5

Unified package query CLI for DEB/RPM Linux distros (dpkg/apt, rpm/dnf)
Documentation
use std::path::PathBuf;

const USRMERGE_MAP: &[(&str, &str)] = &[
    ("/bin/", "/usr/bin/"),
    ("/sbin/", "/usr/sbin/"),
    ("/lib/", "/usr/lib/"),
    ("/lib32/", "/usr/lib32/"),
    ("/lib64/", "/usr/lib64/"),
    ("/libx32/", "/usr/libx32/"),
];

fn canonicalize_path(path: &str) -> String {
    if let Ok(resolved) = std::fs::canonicalize(path) {
        return resolved.to_string_lossy().to_string();
    }
    path.to_string()
}

fn apply_usrmerge(path: &str) -> String {
    for (old, new) in USRMERGE_MAP {
        if path == &old[..old.len() - 1] {
            return new[..new.len() - 1].to_string();
        }
        if let Some(rest) = path.strip_prefix(old) {
            return format!("{}{}", new, rest);
        }
    }
    path.to_string()
}

pub fn normalize_path(path: &str) -> Vec<String> {
    let mut candidates = Vec::new();
    let abs = if path.starts_with('/') {
        path.to_string()
    } else {
        match std::fs::canonicalize(path) {
            Ok(p) => p.to_string_lossy().to_string(),
            Err(_) => {
                let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"));
                format!("{}/{}", cwd.to_string_lossy(), path)
            }
        }
    };
    let canonical = canonicalize_path(&abs);
    candidates.push(canonical.clone());
    let usrmerged = apply_usrmerge(&canonical);
    if usrmerged != canonical {
        candidates.push(usrmerged);
    }
    let reverse_usrmerge = reverse_usrmerge(&canonical);
    if let Some(r) = reverse_usrmerge {
        if r != canonical && !candidates.contains(&r) {
            candidates.push(r);
        }
    }
    candidates
}

fn reverse_usrmerge(path: &str) -> Option<String> {
    for (old, new) in USRMERGE_MAP {
        if let Some(rest) = path.strip_prefix(new) {
            return Some(format!("{}{}", old, rest));
        }
    }
    None
}

pub fn contains_glob(pattern: &str) -> bool {
    pattern.contains('*') || pattern.contains('?') || pattern.contains('[')
}

pub fn glob_to_regex(glob: &str) -> String {
    let mut result = String::with_capacity(glob.len() * 2);
    for c in glob.chars() {
        match c {
            '*' => result.push_str(".*"),
            '?' => result.push('.'),
            '.' | '+' | '(' | ')' | '|' | '^' | '$' | '{' | '}' | '\\' => {
                result.push('\\');
                result.push(c);
            }
            _ => result.push(c),
        }
    }
    result
}

pub fn glob_match(pattern: &str, text: &str) -> bool {
    glob_match_impl(pattern.as_bytes(), text.as_bytes())
}

fn glob_match_impl(pattern: &[u8], text: &[u8]) -> bool {
    let mut pi = 0;
    let mut ti = 0;
    let mut star_pi = None;
    let mut star_ti = 0;

    while ti < text.len() {
        if pi < pattern.len() {
            match pattern[pi] {
                b'*' => {
                    star_pi = Some(pi);
                    star_ti = ti;
                    pi += 1;
                    continue;
                }
                b'?' => {
                    pi += 1;
                    ti += 1;
                    continue;
                }
                c if c == text[ti] => {
                    pi += 1;
                    ti += 1;
                    continue;
                }
                _ => {}
            }
        }
        if let Some(sp) = star_pi {
            pi = sp + 1;
            star_ti += 1;
            ti = star_ti;
        } else {
            return false;
        }
    }

    while pi < pattern.len() && pattern[pi] == b'*' {
        pi += 1;
    }

    pi == pattern.len()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_glob_basic() {
        assert!(glob_match("*", "anything"));
        assert!(glob_match("*/bin/unzip", "/usr/bin/unzip"));
        assert!(glob_match("*/bin/unzip", "/bin/unzip"));
        assert!(!glob_match("*/bin/zip", "/usr/bin/unzip"));
        assert!(glob_match("/usr/bin/?nzip", "/usr/bin/unzip"));
        assert!(glob_match("/usr/bin/un*", "/usr/bin/unzip"));
        assert!(!glob_match("/usr/bin/un*", "/usr/bin/zip"));
    }

    #[test]
    fn test_usrmerge() {
        assert_eq!(apply_usrmerge("/bin/unzip"), "/usr/bin/unzip");
        assert_eq!(apply_usrmerge("/sbin/ip"), "/usr/sbin/ip");
        assert_eq!(
            apply_usrmerge("/lib/x86_64-linux-gnu/libc.so.6"),
            "/usr/lib/x86_64-linux-gnu/libc.so.6"
        );
        assert_eq!(apply_usrmerge("/usr/bin/unzip"), "/usr/bin/unzip");
    }

    #[test]
    fn test_contains_glob() {
        assert!(contains_glob("*/bin/unzip"));
        assert!(contains_glob("/usr/bin/?nzip"));
        assert!(!contains_glob("/usr/bin/unzip"));
    }

    #[test]
    fn test_glob_to_regex_escapes() {
        assert_eq!(glob_to_regex("a.b"), "a\\.b");
        assert_eq!(glob_to_regex("*"), ".*");
        assert_eq!(glob_to_regex("?"), ".");
        assert_eq!(glob_to_regex("/usr/bin/un*"), "/usr/bin/un.*");
    }
}