//
// pandora: Syd's Dump Inspector & Profile Writer
// glob.rs: Accumulate paths into glob(3p) rules
//
// Copyright (c) 2021, 2024, 2025, 2026 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

#![forbid(unsafe_code)]

use std::{
    borrow::Cow,
    ffi::OsString,
    os::unix::ffi::OsStringExt,
    path::{Path, PathBuf},
};

use base16ct::mixed;
use nix::errno::Errno;

use crate::{PandoraMap, PandoraPathList, PandoraSet};

pub(crate) fn hex_decode(hex: &[u8]) -> Result<Vec<u8>, Errno> {
    let len = (hex.len() / 2)
        .checked_add(hex.len() % 2)
        .ok_or(Errno::EOVERFLOW)?;

    let mut buf = Vec::new();
    buf.try_reserve(len).or(Err(Errno::ENOMEM))?;
    buf.resize(len, 0);

    mixed::decode(hex, &mut buf).map_err(|e| match e {
        base16ct::Error::InvalidLength => Errno::EINVAL,
        base16ct::Error::InvalidEncoding => Errno::EILSEQ,
    })?;

    Ok(buf)
}

// Thresholds for ephemeral path detection.
const TMP_SUFFIX_MIN_LEN: usize = 6;
const TMP_SUFFIX_MIN_CLASSES: u8 = 2;
const TMP_SUFFIX_MIN_ENTROPY: f64 = 1.5;
const TMP_DIGIT_MIN_LEN: usize = 5;
const TMP_FULL_MIN_LEN: usize = 6;
const TMP_FULL_MIN_CLASSES: u8 = 3;
const TMP_FULL_MIN_DIGITS: usize = 2;

// Shannon entropy H(X) = -sum(p_i * log2(p_i)) in bits per character.
fn entropy(s: &str) -> f64 {
    let len = s.len() as f64;
    if len == 0.0 {
        return 0.0;
    }
    let mut freq = [0u32; 256];
    for &b in s.as_bytes() {
        freq[b as usize] += 1;
    }
    freq.iter()
        .filter(|&&c| c > 0)
        .map(|&c| {
            let p = c as f64 / len;
            -p * p.log2()
        })
        .sum()
}

// Count distinct character classes: {lowercase, uppercase, digit}.
fn char_classes(s: &str) -> u8 {
    let lo = s.bytes().any(|b| b.is_ascii_lowercase()) as u8;
    let up = s.bytes().any(|b| b.is_ascii_uppercase()) as u8;
    let di = s.bytes().any(|b| b.is_ascii_digit()) as u8;
    lo + up + di
}

// Escape glob metacharacters so a literal string matches itself.
fn glob_escape(s: &str) -> Cow<'_, str> {
    if !s.contains(['*', '?', '[', ']', '\\']) {
        return Cow::Borrowed(s);
    }
    let mut out = String::with_capacity(s.len() + 4);
    for c in s.chars() {
        if matches!(c, '*' | '?' | '[' | ']' | '\\') {
            out.push('\\');
        }
        out.push(c);
    }
    Cow::Owned(out)
}

// True if the string contains an unescaped glob metacharacter.
fn has_glob_meta(s: &str) -> bool {
    let bytes = s.as_bytes();
    let mut i = 0;
    while i < bytes.len() {
        match bytes[i] {
            b'\\' => i += 2,
            b'*' | b'?' | b'[' => return true,
            _ => i += 1,
        }
    }
    false
}

// True for a non-empty all-ASCII-digit string: PIDs, TIDs, inodes.
fn is_ascii_digits(s: &str) -> bool {
    !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit())
}

// Version tag: letters followed by a short trailing digit run,
// e.g. "pandora2", "python3", never a random mkstemp(3) suffix.
fn is_version_suffix(s: &str) -> bool {
    let digits = s.bytes().rev().take_while(|b| b.is_ascii_digit()).count();
    (1..=2).contains(&digits)
        && s.len() - digits >= 3
        && s.bytes()
            .take(s.len() - digits)
            .all(|b| b.is_ascii_alphabetic())
}

// Classify a path component as ephemeral via entropy and character class analysis.
pub(crate) fn is_tmp_comp(name: &str) -> bool {
    if is_ascii_digits(name) {
        return true;
    }
    if let Some(pos) = name.rfind(['.', '-', '_']) {
        let suffix = &name[pos + 1..];
        let versioned = name.as_bytes()[pos] == b'.' && is_version_suffix(suffix);
        if suffix.len() >= TMP_SUFFIX_MIN_LEN
            && !versioned
            && entropy(suffix) >= TMP_SUFFIX_MIN_ENTROPY
            && char_classes(name) >= TMP_SUFFIX_MIN_CLASSES
        {
            return true;
        }
        return suffix.len() >= TMP_DIGIT_MIN_LEN && is_ascii_digits(suffix);
    }
    name.len() >= TMP_FULL_MIN_LEN
        && !is_version_suffix(name)
        && char_classes(name) >= TMP_FULL_MIN_CLASSES
        && name.bytes().filter(|b| b.is_ascii_digit()).count() >= TMP_FULL_MIN_DIGITS
}

// Normalize /proc/<pid>/... paths to /proc for lock rules.
fn normpath_proc(path: &str) -> Option<String> {
    if !path.starts_with("/proc/") {
        return None;
    }
    let components: Vec<&str> = path.split('/').collect();
    if components.len() >= 3 && is_ascii_digits(components[2]) {
        return Some(
            if components[2] == "1" {
                "/proc/1"
            } else {
                "/proc"
            }
            .to_string(),
        );
    }
    None
}

// Promote path to the parent of its topmost ephemeral component.
pub(crate) fn resolve_tmp_comp(path: &str) -> Option<String> {
    if let Some(s) = normpath_proc(path) {
        return Some(s);
    }
    let root_end = tmp_root_end(path, &PandoraSet::default())
        .or_else(|| path.strip_prefix("/proc/").map(|_| "/proc".len()))?;
    let mut offset = root_end;
    for name in path[root_end..].split('/') {
        if !name.is_empty() && is_tmp_comp(name) {
            return Some(path[..offset - 1].to_string());
        }
        offset += name.len() + 1;
    }
    None
}

// Return the parent directory of an absolute path.
// For `/usr/lib/libc.so.6`, returns `/usr/lib`.
// For `/tmp`, returns `/`.
pub(crate) fn parent_path(path: &str) -> String {
    match path.rfind('/') {
        Some(i) if i > 0 => path[..i].to_string(),
        _ => "/".to_string(),
    }
}

// Strip the last path component, returning the parent directory.
// Returns None for root or single-component paths.
fn parent_dir(path: &str) -> Option<&str> {
    match path.rfind('/') {
        Some(i) if i > 0 => Some(&path[..i]),
        _ => None,
    }
}

// Whether a dir_paths entry has caps that imply child creation/deletion.
fn has_mutation_caps(caps: &PandoraSet<String>) -> bool {
    caps.contains("mkdir") || caps.contains("create") || caps.contains("delete")
}

// Promote path to highest ancestor whose parent holds mutation caps.
// Walks up the parent chain, skipping ancestors not in dir_paths.
pub(crate) fn promote_to_parent(
    path: &str,
    dir_paths: &PandoraMap<String, PandoraSet<String>>,
) -> Option<String> {
    let mut best: Option<String> = None;
    let mut current = path;
    while let Some(parent) = parent_dir(current) {
        if let Some(parent_caps) = dir_paths.get(parent) {
            if has_mutation_caps(parent_caps) {
                best = Some(parent.to_string());
            }
        }
        current = parent;
    }
    best
}

// Find the nearest ancestor wildcard (path/***) entry in a map.
// Used by dedup_paths to locate covering wildcards.
fn find_ancestor_wildcard(
    path: &str,
    map: &PandoraMap<String, PandoraSet<String>>,
) -> Option<String> {
    let base = path.strip_suffix("/***").unwrap_or(path);
    let mut current = base;
    while let Some(ancestor) = parent_dir(current) {
        let wildcard = format!("{ancestor}/***");
        if map.contains_key(&wildcard) {
            return Some(wildcard);
        }
        current = ancestor;
    }
    // The root wildcard covers every absolute path but itself.
    if path != "/***" && path.starts_with('/') && map.contains_key("/***") {
        return Some("/***".to_string());
    }
    None
}

// Ephemeral rule key:
// Glob markers anywhere, or an ephemeral basename below a volatile root.
fn is_ephemeral_basename(key: &str, name: &str) -> bool {
    has_glob_meta(name)
        || (tmp_root_end(key, &PandoraSet::default()).is_some() && is_tmp_comp(name))
}

// Minimum number of mutated children to trigger parent/*** consolidation.
const CONSOLIDATE_MUT_THRESHOLD: usize = 10;

// Collapse ephemeral siblings and mutation-heavy groups under parent/***.
pub(crate) fn consolidate_sibling_paths(
    paths: PandoraMap<String, PandoraSet<String>>,
) -> PandoraMap<String, PandoraSet<String>> {
    let mut cur = paths;
    loop {
        let len = cur.len();
        cur = consolidate_pass(cur);
        if cur.len() == len {
            return cur;
        }
    }
}

fn consolidate_pass(
    paths: PandoraMap<String, PandoraSet<String>>,
) -> PandoraMap<String, PandoraSet<String>> {
    let mut buckets: PandoraMap<String, PandoraPathList> = PandoraMap::default();
    let mut non_path: PandoraPathList = Vec::new();

    for (key, caps) in paths {
        if !key.starts_with('/') {
            non_path.push((key, caps));
            continue;
        }
        let stripped = key.strip_suffix("/***").unwrap_or(&key);
        if let Some(i) = stripped.rfind('/') {
            if i > 0 {
                buckets
                    .entry(stripped[..i].to_string())
                    .or_default()
                    .push((key, caps));
            } else {
                non_path.push((key, caps));
            }
        } else {
            non_path.push((key, caps));
        }
    }

    let mut out: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
    for (key, caps) in non_path {
        out.entry(key).or_default().extend(caps);
    }

    for (parent, children) in buckets {
        if children.len() < 3 {
            for (key, caps) in children {
                out.entry(key).or_default().extend(caps);
            }
            continue;
        }

        // Many mutated files under one parent is likely generated content.
        let mut_count = children
            .iter()
            .filter(|(_, caps)| caps.contains("create") || caps.contains("delete"))
            .count();
        if mut_count >= CONSOLIDATE_MUT_THRESHOLD {
            let collapsed = format!("{parent}/***");
            let entry = out.entry(collapsed).or_default();
            for (_key, caps) in children {
                entry.extend(caps);
            }
            continue;
        }

        // Per-child ephemeral classification.
        let mut ephemeral: PandoraPathList = Vec::new();
        let mut stable: PandoraPathList = Vec::new();
        for (key, caps) in children {
            let stripped = key.strip_suffix("/***").unwrap_or(&key);
            let basename = Path::new(stripped)
                .file_name()
                .and_then(|n| n.to_str())
                .unwrap_or("");
            if is_ephemeral_basename(stripped, basename) {
                ephemeral.push((key, caps));
            } else {
                stable.push((key, caps));
            }
        }

        for (key, caps) in stable {
            out.entry(key).or_default().extend(caps);
        }
        if ephemeral.len() >= 3 {
            let collapsed = format!("{parent}/***");
            let entry = out.entry(collapsed).or_default();
            for (_key, caps) in ephemeral {
                entry.extend(caps);
            }
        } else {
            for (key, caps) in ephemeral {
                out.entry(key).or_default().extend(caps);
            }
        }
    }

    out
}

// Merge entries into their nearest ancestor wildcard rule and drop
// fully covered duplicates.
pub(crate) fn dedup_paths(
    paths: PandoraMap<String, PandoraSet<String>>,
) -> PandoraMap<String, PandoraSet<String>> {
    let mut map = paths;

    // Pass 1: fold filesystem entries into their ancestor wildcard.
    // Network entries (sockets) keep individual rules.
    let mergeable: Vec<(String, String)> = map
        .iter()
        .filter(|(_, caps)| !caps.iter().any(|c| c.starts_with("net/")))
        .filter_map(|(path, _)| {
            let anc = find_ancestor_wildcard(path, &map)?;
            Some((path.clone(), anc))
        })
        .collect();
    for (child, ancestor) in mergeable {
        let caps = map.remove(&child).unwrap_or_default();
        map.entry(ancestor).or_default().extend(caps);
    }

    // Pass 2: drop entries fully covered by an ancestor wildcard.
    let covered: Vec<String> = map
        .iter()
        .filter(|(path, caps)| {
            find_ancestor_wildcard(path, &map)
                .and_then(|anc| map.get(&anc).map(|ac| caps.is_subset(ac)))
                .unwrap_or(false)
        })
        .map(|(path, _)| path.clone())
        .collect();
    for key in covered {
        map.remove(&key);
    }
    map
}

// Truncate an absolute path to at most `limit` concrete components.
// Components past the cut, glob components included, are dropped:
// the trailing /*** covers them.
fn limit_path(path: &str, limit: u8) -> Cow<'_, str> {
    if limit == 0 || !path.starts_with('/') {
        return Cow::Borrowed(path);
    }
    let parts: Vec<&str> = path
        .split('/')
        .filter(|&x| !x.is_empty() && x != "***")
        .collect();
    let concrete = parts.iter().filter(|x| !has_glob_meta(x)).count();
    if (limit as usize) >= concrete {
        return Cow::Borrowed(path);
    }
    let mut kept = Vec::new();
    let mut count = 0usize;
    for &p in &parts {
        if !has_glob_meta(p) {
            count += 1;
            if count > limit as usize {
                break;
            }
        }
        kept.push(p);
    }
    format!("/{}/***", kept.join("/")).into()
}

// Truncate a directory path to at most `limit` concrete components.
pub(crate) fn limit_dir_path(path: &str, limit: u8) -> Cow<'_, str> {
    if limit == 0 || !path.starts_with('/') {
        return Cow::Borrowed(path);
    }
    let mut count = 0u8;
    for (i, &b) in path.as_bytes().iter().enumerate() {
        if b == b'/' && i > 0 {
            count += 1;
            if count >= limit {
                return Cow::Owned(path[..i].to_string());
            }
        }
    }
    Cow::Borrowed(path)
}

// Apply glob transformations then enforce the path limit.
// Unreduced paths are escaped: profile rules are glob patterns.
pub(crate) fn process_path<'a>(
    path: &'a str,
    limit: u8,
    roots: &PandoraSet<String>,
) -> Cow<'a, str> {
    if path == "/" {
        return Cow::Borrowed(path);
    }
    match path2glob(path, roots).map(|g| g.into_owned()) {
        Some(g) => limit_path(&g, limit).into_owned().into(),
        None => match glob_escape(path) {
            Cow::Borrowed(p) if p.starts_with('/') => limit_path(p, limit),
            Cow::Borrowed(p) => Cow::Borrowed(p),
            Cow::Owned(p) if p.starts_with('/') => Cow::Owned(limit_path(&p, limit).into_owned()),
            Cow::Owned(p) => Cow::Owned(p),
        },
    }
}

// Maximum sibling lock rules under one parent before collapsing to
// the parent directory: landlock(7) rules are recursive.
const LOCK_WIDE_THRESHOLD: usize = 32;

// Collapse parents with too many child rules.
pub(crate) fn lock_wide_collapse(
    paths: PandoraMap<String, PandoraSet<String>>,
) -> PandoraMap<String, PandoraSet<String>> {
    let mut cur = paths;
    loop {
        let mut counts: PandoraMap<&str, usize> = PandoraMap::default();
        for path in cur.keys() {
            if let Some(parent) = parent_dir(path) {
                *counts.entry(parent).or_default() += 1;
            }
        }
        let wide: PandoraSet<String> = counts
            .into_iter()
            .filter(|&(_, n)| n > LOCK_WIDE_THRESHOLD)
            .map(|(parent, _)| parent.to_string())
            .collect();
        if wide.is_empty() {
            return cur;
        }
        let mut next: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        for (path, caps) in cur {
            let key = match parent_dir(&path) {
                Some(parent) if wide.contains(parent) => parent.to_string(),
                _ => path,
            };
            next.entry(key).or_default().extend(caps);
        }
        cur = next;
    }
}

// Merge and consolidate lock paths: normalize, limit, consolidate, deduplicate.
pub(crate) fn merge_lock_paths(
    file_paths: PandoraMap<String, PandoraSet<String>>,
    dir_paths: PandoraMap<String, PandoraSet<String>>,
    limit: u8,
) -> PandoraMap<String, PandoraSet<String>> {
    let mut out: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();

    for (path, caps) in file_paths {
        let is_virtual =
            caps.contains("create") || (path.starts_with("/proc/") && path.contains(":["));
        let key = if is_virtual {
            parent_dir(&path).map(str::to_string).unwrap_or(path)
        } else {
            path
        };

        if caps.contains("exec") || caps.contains("ioctl") {
            out.entry(key).or_default().extend(caps);
            continue;
        }

        let key = resolve_tmp_comp(&key).unwrap_or(key);
        let key = promote_to_parent(&key, &dir_paths).unwrap_or(key);
        let limited = limit_dir_path(&key, limit);
        let key = if limited.as_ref() != key {
            promote_to_parent(&limited, &dir_paths).unwrap_or_else(|| limited.into_owned())
        } else {
            limited.into_owned()
        };
        out.entry(key).or_default().extend(caps);
    }

    for (path, caps) in &dir_paths {
        let key = resolve_tmp_comp(path).unwrap_or_else(|| path.clone());
        let key = promote_to_parent(&key, &dir_paths).unwrap_or(key);
        let key = limit_dir_path(&key, limit).into_owned();
        out.entry(key).or_default().extend(caps.iter().cloned());
    }

    // landlock(7) takes real paths and directory rules are recursive:
    // fold parent/*** wildcards back to the parent path itself.
    let mut lock: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
    for (key, caps) in consolidate_sibling_paths(out) {
        let key = match key.strip_suffix("/***") {
            Some(parent) if !parent.is_empty() => parent.to_string(),
            _ => key,
        };
        lock.entry(key).or_default().extend(caps);
    }
    lock_wide_collapse(lock)
}

// Format a FixedBitSet of ports as a list of ranges.
//
// Consecutive ports are collapsed, e.g: [80, 81, 82, 443] becomes ["80-82", "443"].
pub(crate) fn format_port_ranges(ports: &fixedbitset::FixedBitSet) -> Vec<String> {
    let mut ranges = Vec::new();
    let mut start: Option<usize> = None;
    let mut end: usize = 0;

    for port in ports.ones() {
        match start {
            None => {
                start = Some(port);
                end = port;
            }
            Some(_) if port == end + 1 => {
                end = port;
            }
            Some(s) => {
                if s == end {
                    ranges.push(format!("{s}"));
                } else {
                    ranges.push(format!("{s}-{end}"));
                }
                start = Some(port);
                end = port;
            }
        }
    }

    if let Some(s) = start {
        if s == end {
            ranges.push(format!("{s}"));
        } else {
            ranges.push(format!("{s}-{end}"));
        }
    }

    ranges
}

// Possibly decode a hex path. If hex decode fails, return it as-is.
pub(crate) fn path2dehex(path: &str) -> PathBuf {
    if let Ok(path_decoded) = hex_decode(path.as_bytes()) {
        OsString::from_vec(path_decoded).into()
    } else {
        path.into()
    }
}

// Glob foo:[N] pseudo-path patterns in fd/ns components.
fn mkglob_proc_fdns(path: &str) -> String {
    let components: Vec<&str> = path.split('/').collect();
    let mut out = String::new();
    for (idx, component) in components.iter().enumerate() {
        if idx > 0 {
            out.push('/');
        }
        let parts: Vec<&str> = component.split(':').collect();
        if parts.len() == 2 && parts[1].starts_with('[') && parts[1].ends_with(']') {
            let inner = &parts[1][1..parts[1].len() - 1];
            if is_ascii_digits(inner) {
                // pipe:[5303077] -> pipe:\[[0-9]*\]
                out.push_str(&format!("{}:\\[[0-9]*\\]", parts[0]));
            } else {
                // anon_inode:[eventfd] -> anon_inode:*
                out.push_str(&format!("{}:*", parts[0]));
            }
        } else {
            out.push_str(component);
        }
    }
    out
}

// procfs directories whose numeric children are unstable identifiers.
const PROC_DIGIT_PARENTS: &[&str] = &["task", "fd", "fdinfo"];

// Join procfs tail components with a '/' prefix, reducing numeric
// components that follow a task/fd/fdinfo component to [0-9]*.
fn glob_proc_tail(segments: &[&str]) -> String {
    let mut out = String::new();
    let mut prev: Option<&str> = None;
    for &segment in segments {
        out.push('/');
        if prev.is_some_and(|p| PROC_DIGIT_PARENTS.contains(&p)) && is_ascii_digits(segment) {
            out.push_str("[0-9]*");
        } else {
            out.push_str(segment);
        }
        prev = Some(segment);
    }
    out
}

// Glob /proc/$pid/... paths, replacing PIDs, TIDs and fd numbers with [0-9]*.
fn mkglob_proc_pid(components: &[&str]) -> Option<String> {
    if components.len() < 3 || !is_ascii_digits(components[2]) {
        return None;
    }
    let rest: Vec<&str> = components[3..]
        .iter()
        .copied()
        .filter(|segment| !segment.is_empty())
        .collect();
    let pid = if components[2] == "1" {
        "/proc/1"
    } else {
        "/proc/[0-9]*"
    };
    Some(format!("{pid}{}", glob_proc_tail(&rest)))
}

// Glob /proc/self/... and /proc/thread-self/... paths.
fn mkglob_proc_self(components: &[&str]) -> Option<String> {
    if components.len() < 3 || (components[2] != "self" && components[2] != "thread-self") {
        return None;
    }
    let prefix = format!("/proc/{}", components[2]);
    let rest: Vec<&str> = components[3..]
        .iter()
        .copied()
        .filter(|segment| !segment.is_empty())
        .collect();
    Some(format!("{prefix}{}", glob_proc_tail(&rest)))
}

// Glob /dev/pts/N and /dev/tty* paths.
fn mkglob_dev_tty(path: &str) -> Option<String> {
    if path == "/dev/tty" {
        Some("/dev/tty".into())
    } else if path.starts_with("/dev/tty") {
        Some("/dev/tty*".into())
    } else if let Some(rest) = path.strip_prefix("/dev/pts/") {
        if rest.starts_with(|c: char| c.is_ascii_digit()) {
            Some("/dev/pts/[0-9]*".into())
        } else {
            None
        }
    } else {
        None
    }
}

// Glob abstract sockets: @cuda-uvmfd--1-63797 -> @cuda-uvmfd--*
// and @gecko-crash-helper-pipe.462275 -> @gecko-crash-helper-pipe.*
fn mkglob_sock_abs(path: &str) -> Option<String> {
    // CUDA pattern: @..--<digits-and-punctuation>
    if path.starts_with('@') {
        if let Some(dashdash) = path.rfind("--") {
            let after = &path[dashdash + 2..];
            if !after.is_empty()
                && after
                    .chars()
                    .all(|c| c.is_ascii_digit() || c.is_ascii_punctuation())
            {
                return Some(format!("{}--*", glob_escape(&path[..dashdash])));
            }
        }
    }
    // Gecko pattern: @gecko-...<.pid>, suffix must carry a digit.
    if path.starts_with("@gecko-") {
        if let Some(dot_pos) = path.rfind('.') {
            let suffix = &path[dot_pos + 1..];
            if !suffix.is_empty()
                && suffix.chars().all(|c| c.is_ascii_alphanumeric())
                && suffix.chars().any(|c| c.is_ascii_digit())
            {
                return Some(format!("{}*", glob_escape(&path[..dot_pos + 1])));
            }
        }
    }
    None
}

// Glob generic ephemeral filesystem paths by replacing ephemeral
// components with smart prefix-based globs.
fn mkglob_tmp(path: &str, roots: &PandoraSet<String>) -> Option<String> {
    if !path.starts_with('/') {
        return None;
    }
    let root_end = tmp_root_end(path, roots)?;
    let (root, rest) = path.split_at(root_end);

    let mut out = String::new();
    match root.strip_prefix("/run/user/") {
        Some(comp) if is_ascii_digits(comp) => out.push_str("/run/user/[0-9]*"),
        _ => {
            for segment in root.split('/').filter(|s| !s.is_empty()) {
                out.push('/');
                out.push_str(&glob_escape(segment));
            }
        }
    }

    let mut changed = false;
    for segment in rest.split('/').filter(|s| !s.is_empty()) {
        out.push('/');
        if is_tmp_comp(segment) {
            changed = true;
            if is_ascii_digits(segment) {
                out.push_str("[0-9]*");
            } else if let Some(sep) = segment.rfind(['.', '-', '_']) {
                out.push_str(&glob_escape(&segment[..sep + 1]));
                out.push('*');
            } else {
                out.push('*');
            }
        } else {
            out.push_str(&glob_escape(segment));
        }
    }

    if changed {
        out.push_str("/***");
        Some(out)
    } else {
        None
    }
}

// Volatile root directories.
const TMP_ROOTS: &[&str] = &["/tmp", "/var/tmp", "/dev/shm", "/dev/pts"];

// Return end offset of shortest volatile root prefix of path.
fn tmp_root_end(path: &str, roots: &PandoraSet<String>) -> Option<usize> {
    fn boundary(path: &str, end: usize) -> bool {
        path.len() == end || path.as_bytes()[end] == b'/'
    }
    let mut best: Option<usize> = None;
    for root in TMP_ROOTS
        .iter()
        .copied()
        .chain(roots.iter().map(|r| r.as_str()))
    {
        if root.len() > 1
            && path.starts_with(root)
            && boundary(path, root.len())
            && best.map_or(true, |b| root.len() < b)
        {
            best = Some(root.len());
        }
    }
    if let Some(rest) = path.strip_prefix("/run/user/") {
        if let Some(comp) = rest.split('/').next().filter(|c| !c.is_empty()) {
            let end = "/run/user/".len() + comp.len();
            if best.map_or(true, |b| end < b) {
                best = Some(end);
            }
        }
    }
    best
}

// Minimum sibling observations before a component is deemed variable.
const SUPPORT_MIN_SIBLINGS: usize = 2;

#[derive(Default)]
struct SupportNode {
    children: PandoraMap<String, SupportNode>,
    renames: PandoraMap<String, String>,
}

fn support_insert(node: &mut SupportNode, comps: &[&str]) {
    if let Some((first, rest)) = comps.split_first() {
        support_insert(node.children.entry(first.to_string()).or_default(), rest);
    }
}

fn support_merge(dst: &mut SupportNode, src: SupportNode) {
    for (name, child) in src.children {
        match dst.children.entry(name) {
            std::collections::hash_map::Entry::Occupied(mut e) => support_merge(e.get_mut(), child),
            std::collections::hash_map::Entry::Vacant(e) => {
                e.insert(child);
            }
        }
    }
}

// Mask maximal digit runs, e.g. "obj_123.o" -> "obj_\u{0}.o".
fn digit_template(name: &str) -> Option<String> {
    if !name.bytes().any(|b| b.is_ascii_digit()) {
        return None;
    }
    let mut template = String::with_capacity(name.len());
    let mut in_run = false;
    for c in name.chars() {
        if c.is_ascii_digit() {
            if !in_run {
                template.push('\u{0}');
                in_run = true;
            }
        } else {
            in_run = false;
            template.push(c);
        }
    }
    Some(template)
}

fn digit_pattern(template: &str) -> String {
    let mut pattern = String::with_capacity(template.len() + 8);
    for c in template.chars() {
        if c == '\u{0}' {
            pattern.push_str("[0-9]*");
        } else {
            if matches!(c, '*' | '?' | '[' | ']' | '\\') {
                pattern.push('\\');
            }
            pattern.push(c);
        }
    }
    pattern
}

fn support_renames(names: &[&String]) -> PandoraMap<String, String> {
    let mut renames: PandoraMap<String, String> = PandoraMap::default();

    let mut templates: PandoraMap<String, Vec<&str>> = PandoraMap::default();
    for name in names {
        if let Some(template) = digit_template(name) {
            templates.entry(template).or_default().push(name);
        }
    }
    for (template, members) in templates {
        if members.len() >= SUPPORT_MIN_SIBLINGS {
            let pattern = digit_pattern(&template);
            for m in members {
                renames.insert(m.to_string(), pattern.clone());
            }
        }
    }

    let mut stems: PandoraMap<&str, Vec<&str>> = PandoraMap::default();
    for name in names {
        if renames.contains_key(name.as_str()) {
            continue;
        }
        if let Some(pos) = name.rfind(['.', '-', '_']) {
            stems.entry(&name[..pos + 1]).or_default().push(name);
        }
    }
    for (stem, members) in stems {
        fn suffix<'a>(m: &'a str, stem: &str) -> &'a str {
            &m[stem.len()..]
        }
        let mut lens: PandoraMap<usize, Vec<&str>> = PandoraMap::default();
        for m in &members {
            let sfx = suffix(m, stem);
            if sfx.len() >= TMP_SUFFIX_MIN_LEN && sfx.bytes().all(|b| b.is_ascii_alphanumeric()) {
                lens.entry(sfx.len()).or_default().push(m);
            }
        }
        for (len, group) in lens {
            let digit_bearing = group
                .iter()
                .filter(|m| suffix(m, stem).bytes().any(|b| b.is_ascii_digit()))
                .count();
            if group.len() >= SUPPORT_MIN_SIBLINGS && digit_bearing >= SUPPORT_MIN_SIBLINGS {
                let pattern = format!("{}{}", glob_escape(stem), "?".repeat(len));
                for m in group {
                    renames.insert(m.to_string(), pattern.clone());
                }
            }
        }
    }
    renames
}

fn support_transform(node: &mut SupportNode) {
    let names: Vec<&String> = node.children.keys().collect();
    let renames = support_renames(&names);
    for (orig, pattern) in &renames {
        let child = node.children.remove(orig).unwrap_or_default();
        match node.children.entry(pattern.clone()) {
            std::collections::hash_map::Entry::Occupied(mut e) => support_merge(e.get_mut(), child),
            std::collections::hash_map::Entry::Vacant(e) => {
                e.insert(child);
            }
        }
    }
    node.renames = renames;
    for child in node.children.values_mut() {
        support_transform(child);
    }
}

fn support_rewrite(root: &SupportNode, path: &str) -> Option<String> {
    let mut node = root;
    let mut out = String::new();
    let mut changed = false;
    for comp in path.split('/').filter(|c| !c.is_empty()) {
        out.push('/');
        match node.renames.get(comp) {
            Some(pattern) => {
                changed = true;
                out.push_str(pattern);
                node = node.children.get(pattern)?;
            }
            None => {
                out.push_str(&glob_escape(comp));
                node = node.children.get(comp)?;
            }
        }
    }
    changed.then_some(out)
}

fn support_eligible(path: &str) -> bool {
    if !path.starts_with('/') || has_glob_meta(path) {
        return false;
    }
    !["/proc", "/sys", "/dev"].iter().any(|root| {
        path.strip_prefix(root)
            .is_some_and(|rest| rest.is_empty() || rest.starts_with('/'))
    })
}

pub(crate) fn support_globs(
    magic: &PandoraMap<String, PandoraSet<String>>,
) -> PandoraMap<String, String> {
    let mut root = SupportNode::default();
    let eligible: Vec<&String> = magic.keys().filter(|p| support_eligible(p)).collect();
    for path in &eligible {
        let comps: Vec<&str> = path.split('/').filter(|c| !c.is_empty()).collect();
        support_insert(&mut root, &comps);
    }
    support_transform(&mut root);
    let mut out: PandoraMap<String, String> = PandoraMap::default();
    for path in eligible {
        if let Some(glob) = support_rewrite(&root, path) {
            out.insert(path.clone(), glob);
        }
    }
    out
}

// Reduce raw magic paths to rule patterns.
pub(crate) fn reduce_magic(
    magic: PandoraMap<String, PandoraSet<String>>,
    limit: u8,
) -> PandoraMap<String, PandoraSet<String>> {
    let roots = mutation_roots(&magic);
    let support = support_globs(&magic);
    let mut reduced: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
    for (path, caps) in magic {
        let reduced_key = has_glob_meta(&path);
        let (net, fs): (PandoraSet<String>, PandoraSet<String>) =
            caps.into_iter().partition(|c| c.starts_with("net/"));
        if !net.is_empty() {
            let key = if reduced_key {
                path.clone()
            } else {
                process_path(&path, u8::MAX, &roots).into_owned()
            };
            reduced.entry(key).or_default().extend(net);
        }
        if !fs.is_empty() {
            let key = if reduced_key {
                limit_path(&path, limit).into_owned()
            } else {
                match support.get(&path) {
                    Some(glob) => limit_path(glob, limit).into_owned(),
                    None => process_path(&path, limit, &roots).into_owned(),
                }
            };
            reduced.entry(key).or_default().extend(fs);
        }
    }
    reduced
}

pub(crate) fn reduce_eager(path: &str, limit: u8) -> Option<String> {
    let none = PandoraSet::default();
    path2glob(path, &none).map(|glob| limit_path(&glob, limit).into_owned())
}

pub(crate) fn mutation_roots(paths: &PandoraMap<String, PandoraSet<String>>) -> PandoraSet<String> {
    let mut roots = PandoraSet::default();
    for (path, caps) in paths {
        if !path.starts_with('/') {
            continue;
        }
        if has_glob_meta(path) {
            continue;
        }
        if ["create", "delete", "rename", "mkdir", "rmdir"]
            .iter()
            .any(|c| caps.contains(*c))
        {
            let parent = parent_path(path);
            if parent != "/" {
                roots.insert(parent);
            }
        }
    }
    roots
}

// If path is known to map to a standard glob, return it. Otherwise return None.
pub(crate) fn path2glob<'a>(path: &'a str, roots: &PandoraSet<String>) -> Option<Cow<'a, str>> {
    if !matches!(path.chars().next(), Some('/') | Some('@') | Some('!')) {
        // SAFETY: hex-encoded untrusted path, return as is.
        return Some(Cow::Borrowed(path));
    }

    let decoded = path2dehex(path);
    let path_str = decoded.to_string_lossy();

    // /proc paths: replace PIDs, TIDs, fd numbers with globs.
    if path_str.starts_with("/proc/") {
        let components: Vec<&str> = path_str.split('/').collect();
        if let Some(globbed) =
            mkglob_proc_pid(&components).or_else(|| mkglob_proc_self(&components))
        {
            // Post-process fd/ns pseudo-path components.
            let result = if globbed.contains("/fd/") || globbed.contains("/ns/") {
                mkglob_proc_fdns(&globbed)
            } else {
                globbed
            };
            return Some(Cow::Owned(result));
        }
    }

    // Memory file descriptors.
    if path_str.starts_with("!memfd:") {
        return Some(Cow::Owned("!memfd:**".into()));
    }
    if path_str.starts_with("!memfd-hugetlb:") {
        return Some(Cow::Owned("!memfd-hugetlb:**".into()));
    }

    // /dev paths.
    if path_str.starts_with("/dev/") {
        if let Some(result) = mkglob_dev_tty(&path_str) {
            return Some(Cow::Owned(result));
        }
    }

    // Abstract sockets and Gecko pipes.
    if let Some(result) = mkglob_sock_abs(&path_str) {
        return Some(Cow::Owned(result));
    }

    // Generic ephemeral path components.
    if let Some(result) = mkglob_tmp(&path_str, roots) {
        return Some(Cow::Owned(result));
    }

    // No glob pattern matched.
    None
}

// Replace UID/GID path components with ${SYD_UID}/${SYD_GID}.
// Must run before process_path to prevent is_tmp_comp from globbing digits.
pub(crate) fn subst_id_with(path: &str, uid: &str, gid: &str) -> String {
    let uid_mid = format!("/{uid}/");
    let gid_mid = format!("/{gid}/");
    let uid_tail = format!("/{uid}");
    let gid_tail = format!("/{gid}");

    let needs_uid = uid != "0" && (path.contains(&uid_mid) || path.ends_with(&uid_tail));
    let needs_gid =
        gid != "0" && gid != uid && (path.contains(&gid_mid) || path.ends_with(&gid_tail));

    if !needs_uid && !needs_gid {
        return path.to_string();
    }

    // Adjacent occurrences overlap on the slash: replace to fixpoint.
    fn replace_all(mut s: String, from: &str, to: &str) -> String {
        loop {
            let next = s.replace(from, to);
            if next == s {
                return s;
            }
            s = next;
        }
    }

    let mut result = path.to_string();
    if needs_uid {
        result = replace_all(result, &uid_mid, "/${SYD_UID}/");
        if result.ends_with(&uid_tail) {
            result.truncate(result.len() - uid_tail.len());
            result.push_str("/${SYD_UID}");
        }
    }
    if needs_gid {
        result = replace_all(result, &gid_mid, "/${SYD_GID}/");
        if result.ends_with(&gid_tail) {
            result.truncate(result.len() - gid_tail.len());
            result.push_str("/${SYD_GID}");
        }
    }
    result
}

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

    #[test]
    fn test_hex_decode_1() {
        assert_eq!(hex_decode(b"737964").unwrap(), b"syd");
        assert_eq!(hex_decode(b"00ff").unwrap(), vec![0u8, 255]);
        assert!(hex_decode(b"zz").is_err());
        assert!(hex_decode(b"0").is_err());
    }

    const TMP_COMP_CASES: &[(&str, bool)] = &[
        ("1", true),
        ("42", true),
        ("999", true),
        ("12345", true),
        ("131037", true),
        ("4194304", true),
        ("t004_hXEy5U", true),
        ("tmp.aB3xZ9", true),
        ("data-Kf8mQ2pR", true),
        ("sess_a1b2c3", true),
        ("t004_bvkdem", true),
        ("aB3xZ9", true),
        ("Kf8mQ2pR", true),
        ("file.12345", true),
        ("log-20260314", true),
        ("cache.001", false),
        ("hostname", false),
        ("hosts", false),
        ("status", false),
        ("passwd", false),
        ("resolv.conf", false),
        ("nsswitch.conf", false),
        ("pid", false),
        ("ns", false),
        ("self", false),
        ("task", false),
        ("fd", false),
        ("attr", false),
        ("smack", false),
        ("apparmor", false),
        ("net", false),
        ("sys", false),
        ("libc.so.6", false),
        ("ld-linux-x86-64.so.2", false),
        ("libacl.so.1.1.2302", false),
        ("libattr.so.1.1.2502", false),
        ("libncursesw.so.6.5", false),
        ("libreadline.so.8.3", false),
        ("t031_src", false),
        ("t031_dst", false),
        ("t032_a", false),
        ("t032_b", false),
        ("t033_out", false),
        ("src", false),
        ("dst", false),
        ("", false),
        ("a", false),
        ("ab", false),
        ("abc", false),
        ("ld.so.cache", false),
        ("ab_cd", false),
        ("ab_cde", false),
        ("X_123", false),
        ("X_12345", true),
        ("foo.so.123", false),
        ("foo.so.12345", true),
        ("libX11.so.6", false),
        ("libQt6Core.so.6", false),
        ("libLLVM-19.so", false),
        ("en_US.UTF-8", false),
        ("C.utf8", false),
        ("config-6.1.0-amd64", false),
        ("MP3Gain", false),
        ("Word2Vec", false),
        ("x8Z9q2ab", true),
        ("tmpXXXXXX", false),
        ("pandora2", false),
        ("syd.pandora2", false),
        ("sydbox2", false),
        ("gtk4.0", false),
        ("python3.11", false),
        ("x86_64-pc-linux-gnu", false),
        ("libgcc_s.so.1", false),
        ("libstdc++.so.6.0.36", false),
        ("libglib-2.0.so.0.8800.3", false),
        ("libharfbuzz.so.0.61232.0", false),
        ("16", true),
        ("t106_jhhd13", true),
        ("t106_KN5awK", true),
        ("t106_awKfz3", true),
        ("data-jhhd13", true),
        ("tmp-abcdef12", true),
        ("backup.abcdef12", false),
    ];

    const PATH2GLOB_CASES: &[(&str, Option<&str>)] = &[
        ("/proc/1/status", Some("/proc/1/status")),
        ("/proc/42/status", Some("/proc/[0-9]*/status")),
        ("/proc/131037/status", Some("/proc/[0-9]*/status")),
        ("/proc/1/maps", Some("/proc/1/maps")),
        ("/proc/1/cmdline", Some("/proc/1/cmdline")),
        ("/proc/42/comm", Some("/proc/[0-9]*/comm")),
        ("/proc/999/oom_score", Some("/proc/[0-9]*/oom_score")),
        ("/proc/1/cgroup", Some("/proc/1/cgroup")),
        ("/proc/1/mountinfo", Some("/proc/1/mountinfo")),
        ("/proc/1/stat", Some("/proc/1/stat")),
        ("/proc/1/fd/0", Some("/proc/1/fd/[0-9]*")),
        ("/proc/42/fd/3", Some("/proc/[0-9]*/fd/[0-9]*")),
        ("/proc/131037/fd/255", Some("/proc/[0-9]*/fd/[0-9]*")),
        ("/proc/1/fd/1024", Some("/proc/1/fd/[0-9]*")),
        ("/proc/1/ns/pid", Some("/proc/1/ns/pid")),
        ("/proc/42/ns/net", Some("/proc/[0-9]*/ns/net")),
        ("/proc/1/ns/mnt", Some("/proc/1/ns/mnt")),
        ("/proc/1/ns/user", Some("/proc/1/ns/user")),
        ("/proc/1/ns/ipc", Some("/proc/1/ns/ipc")),
        ("/proc/1/ns/uts", Some("/proc/1/ns/uts")),
        ("/proc/1/task/1/status", Some("/proc/1/task/[0-9]*/status")),
        (
            "/proc/42/task/131037/status",
            Some("/proc/[0-9]*/task/[0-9]*/status"),
        ),
        (
            "/proc/42/task/131037/comm",
            Some("/proc/[0-9]*/task/[0-9]*/comm"),
        ),
        ("/proc/1/task/1/fd/0", Some("/proc/1/task/[0-9]*/fd/[0-9]*")),
        (
            "/proc/42/task/131037/fd/3",
            Some("/proc/[0-9]*/task/[0-9]*/fd/[0-9]*"),
        ),
        ("/proc/1/task/1/ns/pid", Some("/proc/1/task/[0-9]*/ns/pid")),
        (
            "/proc/42/task/131037/ns/net",
            Some("/proc/[0-9]*/task/[0-9]*/ns/net"),
        ),
        ("/proc/self/status", Some("/proc/self/status")),
        ("/proc/self/maps", Some("/proc/self/maps")),
        ("/proc/self/cmdline", Some("/proc/self/cmdline")),
        ("/proc/self/comm", Some("/proc/self/comm")),
        ("/proc/self/exe", Some("/proc/self/exe")),
        ("/proc/self/cgroup", Some("/proc/self/cgroup")),
        ("/proc/self/mountinfo", Some("/proc/self/mountinfo")),
        ("/proc/self/attr/current", Some("/proc/self/attr/current")),
        ("/proc/self/ns/pid", Some("/proc/self/ns/pid")),
        ("/proc/self/ns/net", Some("/proc/self/ns/net")),
        ("/proc/self/ns/mnt", Some("/proc/self/ns/mnt")),
        ("/proc/self/net/tcp", Some("/proc/self/net/tcp")),
        ("/proc/self/net/unix", Some("/proc/self/net/unix")),
        ("/proc/self/fd/0", Some("/proc/self/fd/[0-9]*")),
        ("/proc/self/fd/3", Some("/proc/self/fd/[0-9]*")),
        ("/proc/self/fd/255", Some("/proc/self/fd/[0-9]*")),
        ("/proc/self/fdinfo/0", Some("/proc/self/fdinfo/[0-9]*")),
        ("/proc/self/fdinfo/42", Some("/proc/self/fdinfo/[0-9]*")),
        (
            "/proc/self/task/131037/status",
            Some("/proc/self/task/[0-9]*/status"),
        ),
        (
            "/proc/self/task/1/comm",
            Some("/proc/self/task/[0-9]*/comm"),
        ),
        (
            "/proc/self/task/999/maps",
            Some("/proc/self/task/[0-9]*/maps"),
        ),
        (
            "/proc/self/task/131037/fd/3",
            Some("/proc/self/task/[0-9]*/fd/[0-9]*"),
        ),
        (
            "/proc/self/task/1/fd/0",
            Some("/proc/self/task/[0-9]*/fd/[0-9]*"),
        ),
        (
            "/proc/self/task/131037/fdinfo/3",
            Some("/proc/self/task/[0-9]*/fdinfo/[0-9]*"),
        ),
        ("/proc/thread-self/status", Some("/proc/thread-self/status")),
        ("/proc/thread-self/comm", Some("/proc/thread-self/comm")),
        (
            "/proc/thread-self/fd/0",
            Some("/proc/thread-self/fd/[0-9]*"),
        ),
        (
            "/proc/thread-self/fd/42",
            Some("/proc/thread-self/fd/[0-9]*"),
        ),
        (
            "/proc/thread-self/fdinfo/3",
            Some("/proc/thread-self/fdinfo/[0-9]*"),
        ),
        ("/proc/thread-self/ns/pid", Some("/proc/thread-self/ns/pid")),
        (
            "/proc/thread-self/attr/current",
            Some("/proc/thread-self/attr/current"),
        ),
        ("/dev/pts/0", Some("/dev/pts/[0-9]*")),
        ("/dev/pts/1", Some("/dev/pts/[0-9]*")),
        ("/dev/pts/34", Some("/dev/pts/[0-9]*")),
        ("/dev/pts/255", Some("/dev/pts/[0-9]*")),
        ("/dev/pts/ptmx", None),
        ("/dev/tty", Some("/dev/tty")),
        ("/dev/tty0", Some("/dev/tty*")),
        ("/dev/tty1", Some("/dev/tty*")),
        ("/dev/ttyS0", Some("/dev/tty*")),
        ("/dev/ttyACM0", Some("/dev/tty*")),
        ("/dev/ttyUSB0", Some("/dev/tty*")),
        ("!memfd:foo", Some("!memfd:**")),
        ("!memfd:", Some("!memfd:**")),
        ("!memfd:jit-code", Some("!memfd:**")),
        ("!memfd:pulseaudio", Some("!memfd:**")),
        ("!memfd-hugetlb:huge", Some("!memfd-hugetlb:**")),
        ("!memfd-hugetlb:", Some("!memfd-hugetlb:**")),
        ("@cuda-uvmfd--1-63797", Some("@cuda-uvmfd--*")),
        ("@cuda-uvmfd--0-12345", Some("@cuda-uvmfd--*")),
        ("@cuda-uvmfd--42-99999", Some("@cuda-uvmfd--*")),
        (
            "@gecko-crash-helper-pipe.462275",
            Some("@gecko-crash-helper-pipe.*"),
        ),
        (
            "@gecko-crash-helper-pipe.1",
            Some("@gecko-crash-helper-pipe.*"),
        ),
        ("@gecko-ipc-pipe.12345", Some("@gecko-ipc-pipe.*")),
        (
            "2f6574632f686f73746e616d65",
            Some("2f6574632f686f73746e616d65"),
        ),
        ("deadbeef", Some("deadbeef")),
        ("0123456789abcdef", Some("0123456789abcdef")),
        ("/etc/hostname", None),
        ("/etc/hosts", None),
        ("/etc/resolv.conf", None),
        ("/etc/nsswitch.conf", None),
        ("/etc/ld.so.cache", None),
        ("/usr/lib/libc.so.6", None),
        ("/usr/lib/ld-linux-x86-64.so.2", None),
        ("/usr/bin/cat", None),
        ("/bin/sh", None),
        ("/dev/null", None),
        ("/dev/zero", None),
        ("/dev/urandom", None),
        ("/tmp/stable_name", None),
        ("/var/log/syslog", None),
        ("/home/user/.bashrc", None),
        ("/proc/stat", None),
        ("/proc/meminfo", None),
        ("/proc/cpuinfo", None),
        ("/proc/version", None),
        ("/proc/filesystems", None),
        ("/proc/mounts", None),
        ("@dbus-session", None),
        ("@/tmp/dbus-xyz", None),
        ("/proc/1", Some("/proc/1")),
        ("/proc/10", Some("/proc/[0-9]*")),
        ("/proc/131037", Some("/proc/[0-9]*")),
        ("/proc/1/fd", Some("/proc/1/fd")),
        ("/proc/42/ns", Some("/proc/[0-9]*/ns")),
        ("/proc/1/task/1", Some("/proc/1/task/[0-9]*")),
        ("/proc/42/task/131037", Some("/proc/[0-9]*/task/[0-9]*")),
        ("/proc/self", Some("/proc/self")),
        ("/proc/self/", Some("/proc/self")),
        ("/proc/thread-self", Some("/proc/thread-self")),
        ("/proc/thread-self/", Some("/proc/thread-self")),
        ("/proc/self/task/131037", Some("/proc/self/task/[0-9]*")),
        ("/proc/self/task/self", Some("/proc/self/task/self")),
        (
            "/proc/1/ns/pid:[4026531836]",
            Some("/proc/1/ns/pid:\\[[0-9]*\\]"),
        ),
        (
            "/proc/42/ns/net:[4026531992]",
            Some("/proc/[0-9]*/ns/net:\\[[0-9]*\\]"),
        ),
        (
            "/proc/1/task/1/ns/mnt:[4026531840]",
            Some("/proc/1/task/[0-9]*/ns/mnt:\\[[0-9]*\\]"),
        ),
        ("/tmp/tmpXXXXXX", None),
        (
            "/run/user/1000/sess_a1b2c3d4e5f6",
            Some("/run/user/[0-9]*/sess_*/***"),
        ),
        ("/dev/pts", None),
        ("/dev/pts/", None),
        ("/dev/tty", Some("/dev/tty")),
        ("!memfd:wayland-shm", Some("!memfd:**")),
        ("!memfd:mesa-shader", Some("!memfd:**")),
        ("!memfd:v8-wasm", Some("!memfd:**")),
        ("@gecko-ipc-pipe.1", Some("@gecko-ipc-pipe.*")),
        (
            "@gecko-crash-server-pipe.999999",
            Some("@gecko-crash-server-pipe.*"),
        ),
        ("@cuda-uvmfd--0-0", Some("@cuda-uvmfd--*")),
        ("/sys/class/net", None),
        ("/sys/kernel/mm/transparent_hugepage/enabled", None),
        ("/run/dbus/system_bus_socket", None),
        ("/usr/share/locale/en_US/LC_MESSAGES/libc.mo", None),
        ("/usr/share/zoneinfo/UTC", None),
        ("/etc/localtime", None),
        ("/etc/passwd", None),
        ("/etc/group", None),
        ("/etc/shadow", None),
        ("/proc/sys/kernel/hostname", None),
        ("/proc/sys/net/core/somaxconn", None),
        ("/proc/pressure/cpu", None),
        ("/proc/net/tcp", None),
        ("/proc/net/unix", None),
        ("/proc/bus/pci/devices", None),
        ("/proc/self/oom_adj", Some("/proc/self/oom_adj")),
        ("/proc/self/oom_score_adj", Some("/proc/self/oom_score_adj")),
        ("/proc/self/smaps", Some("/proc/self/smaps")),
        ("/proc/self/smaps_rollup", Some("/proc/self/smaps_rollup")),
        ("/proc/self/pagemap", Some("/proc/self/pagemap")),
        ("/proc/self/wchan", Some("/proc/self/wchan")),
        ("/proc/self/net/tcp6", Some("/proc/self/net/tcp6")),
        ("/proc/self/net/udp", Some("/proc/self/net/udp")),
        (
            "/proc/self/attr/apparmor/current",
            Some("/proc/self/attr/apparmor/current"),
        ),
        (
            "/proc/thread-self/attr/apparmor/current",
            Some("/proc/thread-self/attr/apparmor/current"),
        ),
        (
            "/proc/thread-self/net/tcp",
            Some("/proc/thread-self/net/tcp"),
        ),
        (
            "/proc/thread-self/task/42/ns/pid",
            Some("/proc/thread-self/task/[0-9]*/ns/pid"),
        ),
        (
            "/proc/thread-self/task/42/fd/5",
            Some("/proc/thread-self/task/[0-9]*/fd/[0-9]*"),
        ),
        (
            "/proc/thread-self/task/42/fdinfo/5",
            Some("/proc/thread-self/task/[0-9]*/fdinfo/[0-9]*"),
        ),
        (
            "/proc/self/fd/pipe:[5303077]",
            Some("/proc/self/fd/pipe:\\[[0-9]*\\]"),
        ),
        (
            "/proc/self/fd/socket:[12345]",
            Some("/proc/self/fd/socket:\\[[0-9]*\\]"),
        ),
        (
            "/proc/1/fd/pipe:[999]",
            Some("/proc/1/fd/pipe:\\[[0-9]*\\]"),
        ),
        (
            "/proc/self/fd/anon_inode:[eventfd]",
            Some("/proc/self/fd/anon_inode:*"),
        ),
        (
            "/proc/self/fd/anon_inode:[eventpoll]",
            Some("/proc/self/fd/anon_inode:*"),
        ),
        ("/proc/self/fd/3/sub", Some("/proc/self/fd/[0-9]*/sub")),
        ("/proc/1/fd/7/sub", Some("/proc/1/fd/[0-9]*/sub")),
        (
            "/proc/1/task/5/fd/3/sub",
            Some("/proc/1/task/[0-9]*/fd/[0-9]*/sub"),
        ),
        (
            "/proc/self/fdinfo/3/sub",
            Some("/proc/self/fdinfo/[0-9]*/sub"),
        ),
        ("/proc/\u{00BE}/status", None),
        ("/proc/self/fd/\u{0663}", Some("/proc/self/fd/\u{0663}")),
        ("/proc/1/", Some("/proc/1")),
        ("/proc/1//status", Some("/proc/1/status")),
        (
            "/proc/self/fd/anon_inode:[]",
            Some("/proc/self/fd/anon_inode:*"),
        ),
        ("/usr/lib/libX11.so.6", None),
        ("/usr/lib/gcc/x86_64-pc-linux-gnu/16/libgcc_s.so.1", None),
        ("/usr/lib/gcc/x86_64-pc-linux-gnu/16/include/stddef.h", None),
        ("/usr/lib/libQt6Core.so.6", None),
        ("/usr/lib/locale/C.utf8/LC_CTYPE", None),
        ("/usr/share/X11/locale/en_US.UTF-8/XLC_LOCALE", None),
        ("/usr/bin/MP3Gain", None),
        ("/opt/Word2Vec", None),
        ("/tmp/x8Z9q2ab", Some("/tmp/*/***")),
        (
            "/tmp/build-aB3xZ9/out.log",
            Some("/tmp/build-*/out.log/***"),
        ),
        ("/tmp/foo[1]/x8Z9q2ab", Some("/tmp/foo\\[1\\]/*/***")),
        ("/tmp/a[b]_x8Z9q2cd", Some("/tmp/a\\[b\\]_*/***")),
        ("@gecko-media-plugins.sock", None),
        ("/home/user/syd.pandora2/target/debug", None),
        ("/home/user/syd.pandora2/target/debug/t004_hXEy5U", None),
        ("/", None),
        ("/proc/1/fd/a:b:c", Some("/proc/1/fd/a:b:c")),
        ("/proc/self/fd/x:y", Some("/proc/self/fd/x:y")),
        ("/usr/share/applications/org.gnome.Nautilus.desktop", None),
        ("/home/user/.mozilla/firefox/Firefox.profile", None),
        ("/home/user/My-Documents", None),
        ("/home/user/projects/demo-Kf8mQ2pR/main.rs", None),
        ("/tmpx/x8Z9q2ab", None),
        ("/run/user", None),
        ("/run/user/", None),
        ("/run/user/1000", None),
        ("/tmp", None),
        ("/var/tmp", None),
        ("/tmp/131037", Some("/tmp/[0-9]*/***")),
        (
            "/var/tmp/build-aB3xZ9/out.log",
            Some("/var/tmp/build-*/out.log/***"),
        ),
        (
            "/dev/shm/pulse-Kf8mQ2pR/pid",
            Some("/dev/shm/pulse-*/pid/***"),
        ),
        (
            "/run/user/${SYD_UID}/doc/x8Z9q2ab",
            Some("/run/user/${SYD_UID}/doc/*/***"),
        ),
    ];

    fn glob2regex(glob: &str) -> fancy_regex::Regex {
        fn push_lit(re: &mut String, c: char) {
            if c.is_ascii() && !c.is_ascii_alphanumeric() {
                re.push('\\');
            }
            re.push(c);
        }
        let (body, tail) = match glob.strip_suffix("/***") {
            Some(pre) => (pre, "(/.*)?"),
            None => (glob, ""),
        };
        assert!(!body.contains("***"));
        let mut re = String::from("^");
        let chars: Vec<char> = body.chars().collect();
        let mut i = 0;
        while i < chars.len() {
            match chars[i] {
                '\\' => {
                    i += 1;
                    assert!(i < chars.len());
                    push_lit(&mut re, chars[i]);
                }
                '[' => {
                    re.push('[');
                    i += 1;
                    while i < chars.len() && chars[i] != ']' {
                        re.push(chars[i]);
                        i += 1;
                    }
                    assert!(i < chars.len());
                    re.push(']');
                }
                '*' => {
                    if i + 1 < chars.len() && chars[i + 1] == '*' {
                        re.push_str(".*");
                        i += 1;
                    } else {
                        re.push_str("[^/]*");
                    }
                }
                '?' => re.push_str("[^/]"),
                c => push_lit(&mut re, c),
            }
            i += 1;
        }
        re.push_str(tail);
        re.push('$');
        fancy_regex::Regex::new(&re).unwrap()
    }

    fn glob_matches(glob: &str, path: &str) -> bool {
        glob2regex(glob).is_match(path).unwrap()
    }

    fn canonical(path: &str) -> String {
        if !path.starts_with('/') {
            return path.to_string();
        }
        let mut out = String::with_capacity(path.len());
        for comp in path.split('/').filter(|c| !c.is_empty()) {
            out.push('/');
            out.push_str(comp);
        }
        if out.is_empty() {
            out.push('/');
        }
        out
    }

    fn bitset(ports: &[usize]) -> fixedbitset::FixedBitSet {
        let mut set = fixedbitset::FixedBitSet::with_capacity(65536);
        for &p in ports {
            set.insert(p);
        }
        set
    }

    fn caps(names: &[&str]) -> PandoraSet<String> {
        names.iter().map(|s| s.to_string()).collect()
    }

    fn no_roots() -> PandoraSet<String> {
        PandoraSet::default()
    }

    #[test]
    fn test_entropy_1() {
        assert_eq!(entropy(""), 0.0);
        assert_eq!(entropy("aaaa"), 0.0);
        assert_eq!(entropy("zzzzzzzzz"), 0.0);
    }

    #[test]
    fn test_entropy_2() {
        assert!((entropy("ab") - 1.0).abs() < 0.01);
        assert!((entropy("abcd") - 2.0).abs() < 0.01);
    }

    #[test]
    fn test_entropy_3() {
        let bits = entropy("aab");
        assert!(bits > 0.9 && bits < 0.95);
    }

    #[test]
    fn test_char_classes_1() {
        assert_eq!(char_classes(""), 0);
        assert_eq!(char_classes("abc"), 1);
        assert_eq!(char_classes("ABC"), 1);
        assert_eq!(char_classes("123"), 1);
        assert_eq!(char_classes("aB"), 2);
        assert_eq!(char_classes("a1"), 2);
        assert_eq!(char_classes("A1"), 2);
        assert_eq!(char_classes("aB3"), 3);
        assert_eq!(char_classes("_-."), 0);
    }

    #[test]
    fn test_glob_escape_1() {
        assert_eq!(glob_escape("plain"), "plain");
        assert_eq!(glob_escape("a[b]"), "a\\[b\\]");
        assert_eq!(glob_escape("a*b?c"), "a\\*b\\?c");
        assert_eq!(glob_escape("a\\b"), "a\\\\b");
    }

    #[test]
    fn test_has_glob_meta_1() {
        assert!(has_glob_meta("[0-9]*"));
        assert!(has_glob_meta("sess_*"));
        assert!(has_glob_meta("a?b"));
        assert!(!has_glob_meta("plain"));
        assert!(!has_glob_meta("a\\*b"));
        assert!(!has_glob_meta("a\\[b\\]"));
    }

    #[test]
    fn test_is_tmp_comp_1() {
        for &(input, expected) in TMP_COMP_CASES {
            assert_eq!(is_tmp_comp(input), expected);
        }
    }

    #[test]
    fn test_is_tmp_comp_2() {
        for shape in 0..729u32 {
            let suffix = shape_suffix(shape);
            let lower = suffix.bytes().all(|b| b.is_ascii_lowercase());
            assert!(is_tmp_comp(&format!("t106_{suffix}")));
            assert_eq!(is_tmp_comp(&format!("data-{suffix}")), !lower);
        }
    }

    #[test]
    fn test_normpath_proc_1() {
        assert_eq!(normpath_proc("/proc/1/status"), Some("/proc/1".into()));
        assert_eq!(normpath_proc("/proc/42/status"), Some("/proc".into()));
        assert_eq!(normpath_proc("/proc/131037/fd/3"), Some("/proc".into()));
    }

    #[test]
    fn test_normpath_proc_2() {
        assert_eq!(normpath_proc("/proc/self/status"), None);
        assert_eq!(normpath_proc("/proc/self/task/131037/status"), None);
    }

    #[test]
    fn test_normpath_proc_3() {
        assert_eq!(normpath_proc("/etc/hostname"), None);
        assert_eq!(normpath_proc("/tmp/foo"), None);
    }

    #[test]
    fn test_resolve_tmp_comp_1() {
        assert_eq!(resolve_tmp_comp("/proc/42/status"), Some("/proc".into()));
        assert_eq!(resolve_tmp_comp("/proc/131037/fd/3"), Some("/proc".into()));
    }

    #[test]
    fn test_resolve_tmp_comp_2() {
        assert_eq!(
            resolve_tmp_comp("/proc/self/task/131037/status"),
            Some("/proc/self/task".into())
        );
    }

    #[test]
    fn test_resolve_tmp_comp_3() {
        assert_eq!(resolve_tmp_comp("/etc/hostname"), None);
        assert_eq!(resolve_tmp_comp("/usr/lib/libc.so.6"), None);
        assert_eq!(resolve_tmp_comp("/proc/self/status"), None);
    }

    #[test]
    fn test_resolve_tmp_comp_4() {
        assert_eq!(resolve_tmp_comp("/tmp/sess_a1b2c3"), Some("/tmp".into()));
    }

    #[test]
    fn test_resolve_tmp_comp_5() {
        assert_eq!(
            resolve_tmp_comp("/tmp/x8Z9q2ab/y7W3r1cd/file"),
            Some("/tmp".into())
        );
        assert_eq!(
            resolve_tmp_comp("/var/tmp/build-aB3xZ9/sub/data-Kf8mQ2pR/leaf"),
            Some("/var/tmp".into())
        );
        assert_eq!(resolve_tmp_comp("/tmp/sess_a1b2c3/x"), Some("/tmp".into()));
    }

    #[test]
    fn test_resolve_tmp_comp_6() {
        assert_eq!(resolve_tmp_comp("/131037"), None);
        assert_eq!(resolve_tmp_comp("/131037/cwd"), None);
    }

    #[test]
    fn test_resolve_tmp_comp_7() {
        assert_eq!(
            resolve_tmp_comp("/home/user/syd.pandora2/target/debug"),
            None
        );
        assert_eq!(
            resolve_tmp_comp("/home/user/syd.pandora2/target/debug/t004_hXEy5U"),
            None
        );
    }

    #[test]
    fn test_resolve_tmp_comp_8() {
        assert_eq!(
            resolve_tmp_comp("/dev/shm/sem.x8Z9q2ab"),
            Some("/dev/shm".into())
        );
        assert_eq!(
            resolve_tmp_comp("/var/tmp/build-aB3xZ9/leaf"),
            Some("/var/tmp".into())
        );
        assert_eq!(resolve_tmp_comp("/home/user/data-Kf8mQ2pR/leaf"), None);
        assert_eq!(
            resolve_tmp_comp("/run/user/1000/doc/x8Z9q2ab"),
            Some("/run/user/1000/doc".into())
        );
        assert_eq!(resolve_tmp_comp("/dev/pts/33"), Some("/dev/pts".into()));
    }

    #[test]
    fn test_resolve_tmp_comp_9() {
        assert_eq!(resolve_tmp_comp("/proc/1/status"), Some("/proc/1".into()));
        assert_eq!(resolve_tmp_comp("/proc/1/fd/3"), Some("/proc/1".into()));
        assert_eq!(resolve_tmp_comp("/proc/10/status"), Some("/proc".into()));
    }

    #[test]
    fn test_is_version_suffix_1() {
        assert!(is_version_suffix("pandora2"));
        assert!(is_version_suffix("python3"));
        assert!(is_version_suffix("OpenGL32"));
        assert!(!is_version_suffix("bvkdem"));
        assert!(!is_version_suffix("a1b2c3"));
        assert!(!is_version_suffix("hXEy5U"));
        assert!(!is_version_suffix("abc123"));
        assert!(!is_version_suffix("ab1"));
        assert!(!is_version_suffix("12345"));
        assert!(!is_version_suffix(""));
    }

    #[test]
    fn test_parent_path_1() {
        assert_eq!(parent_path("/usr/lib/libc.so.6"), "/usr/lib");
        assert_eq!(parent_path("/tmp"), "/");
        assert_eq!(parent_path("relative"), "/");
    }

    #[test]
    fn test_promote_to_parent_1() {
        let mut dp: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        dp.entry("/tmp".into()).or_default().insert("delete".into());
        assert_eq!(promote_to_parent("/tmp/t033_out", &dp), Some("/tmp".into()));
    }

    #[test]
    fn test_promote_to_parent_2() {
        let mut dp: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        dp.entry("/tmp".into()).or_default().insert("read".into());
        assert_eq!(promote_to_parent("/tmp/file", &dp), None);
    }

    #[test]
    fn test_promote_to_parent_3() {
        let mut dp: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        dp.entry("/tmp".into()).or_default().insert("mkdir".into());
        dp.entry("/tmp/a".into())
            .or_default()
            .insert("create".into());
        assert_eq!(promote_to_parent("/tmp/a/b/c", &dp), Some("/tmp".into()));
    }

    #[test]
    fn test_consolidate_sibling_paths_1() {
        let mut map: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        map.insert("/tmp/sess_a1b2c3".into(), caps(&["read"]));
        map.insert("/tmp/sess_d4e5f6".into(), caps(&["write"]));
        map.insert("/tmp/sess_g7h8i9".into(), caps(&["read"]));
        map.insert("/tmp/stable".into(), caps(&["read"]));
        let out = consolidate_sibling_paths(map);
        assert_eq!(out.get("/tmp/***"), Some(&caps(&["read", "write"])));
        assert!(out.contains_key("/tmp/stable"));
        assert!(!out.contains_key("/tmp/sess_a1b2c3"));
    }

    #[test]
    fn test_consolidate_sibling_paths_2() {
        let mut map: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        map.insert("/tmp/sess_a1b2c3".into(), caps(&["read"]));
        map.insert("/tmp/sess_d4e5f6".into(), caps(&["read"]));
        let out = consolidate_sibling_paths(map);
        assert!(out.contains_key("/tmp/sess_a1b2c3"));
        assert!(out.contains_key("/tmp/sess_d4e5f6"));
        assert!(!out.contains_key("/tmp/***"));
    }

    #[test]
    fn test_consolidate_sibling_paths_3() {
        let mut map: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        for i in 0..10 {
            map.insert(format!("/build/out/file{i}"), caps(&["create"]));
        }
        let out = consolidate_sibling_paths(map);
        assert_eq!(out.get("/build/out/***"), Some(&caps(&["create"])));
        assert_eq!(out.len(), 1);
    }

    #[test]
    fn test_consolidate_sibling_paths_4() {
        let mut map: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        map.insert("@sock.a1b2c3".into(), caps(&["net/bind"]));
        map.insert("tcp!127.0.0.1!80".into(), caps(&["net/connect"]));
        let out = consolidate_sibling_paths(map);
        assert!(out.contains_key("@sock.a1b2c3"));
        assert!(out.contains_key("tcp!127.0.0.1!80"));
    }

    #[test]
    fn test_consolidate_sibling_paths_5() {
        let mut map: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        map.insert("/***".into(), caps(&["read"]));
        let out = consolidate_sibling_paths(map);
        assert_eq!(out.get("/***"), Some(&caps(&["read"])));
    }

    #[test]
    fn test_consolidate_sibling_paths_6() {
        let mut map: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        map.insert("/tmp/sess_a1b2c3".into(), caps(&["read"]));
        map.insert("/tmp/stable_one".into(), caps(&["read"]));
        map.insert("/tmp/stable_two".into(), caps(&["read"]));
        map.insert("/tmp/stable_three".into(), caps(&["read"]));
        let out = consolidate_sibling_paths(map);
        assert!(out.contains_key("/tmp/sess_a1b2c3"));
        assert!(out.contains_key("/tmp/stable_one"));
        assert!(!out.contains_key("/tmp/***"));
    }

    #[test]
    fn test_consolidate_sibling_paths_7() {
        let mut map: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        for d in ["sess_a1b2c3", "sess_d4e5f6", "sess_g7h8i9"] {
            for f in ["data-Kf8mQ2pR", "data-Xy7Zw3Qa", "data-Mn2Pq8Rt"] {
                map.insert(format!("/tmp/{d}/{f}"), caps(&["read"]));
            }
        }
        let out = consolidate_sibling_paths(map);
        assert_eq!(out.get("/tmp/***"), Some(&caps(&["read"])));
        assert_eq!(out.len(), 1);
    }

    #[test]
    fn test_consolidate_sibling_paths_8() {
        let mut map: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        for d in ["sess_a1b2c3", "sess_d4e5f6", "sess_g7h8i9"] {
            for f in ["data-Kf8mQ2pR", "data-Xy7Zw3Qa", "data-Mn2Pq8Rt"] {
                map.insert(format!("/tmp/{d}/{f}"), caps(&["read"]));
            }
        }
        map.insert("/etc/hostname".into(), caps(&["read"]));
        map.insert("@sock.a1b2c3".into(), caps(&["net/bind"]));
        let once = consolidate_sibling_paths(map);
        assert_eq!(consolidate_sibling_paths(once.clone()), once);
    }

    #[test]
    fn test_consolidate_sibling_paths_9() {
        let mut map: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        for n in [
            "org.gnome.Nautilus.desktop",
            "org.gnome.Totem.desktop",
            "org.gnome.Eog.desktop",
        ] {
            map.insert(format!("/usr/share/applications/{n}"), caps(&["read"]));
        }
        let out = consolidate_sibling_paths(map);
        assert_eq!(out.len(), 3);
        assert!(!out.contains_key("/usr/share/applications/***"));
    }

    #[test]
    fn test_dedup_paths_1() {
        let mut map: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        map.insert("/tmp/***".into(), caps(&["read"]));
        map.insert("/tmp/build/out".into(), caps(&["write"]));
        let out = dedup_paths(map);
        assert_eq!(out.get("/tmp/***"), Some(&caps(&["read", "write"])));
        assert!(!out.contains_key("/tmp/build/out"));
    }

    #[test]
    fn test_dedup_paths_2() {
        let mut map: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        map.insert("/tmp/***".into(), caps(&["read"]));
        map.insert("/tmp/my.sock".into(), caps(&["net/connect"]));
        let out = dedup_paths(map);
        assert!(out.contains_key("/tmp/my.sock"));
    }

    #[test]
    fn test_dedup_paths_3() {
        let mut map: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        map.insert("/tmp/***".into(), caps(&["read", "net/connect"]));
        map.insert("/tmp/my.sock".into(), caps(&["net/connect"]));
        let out = dedup_paths(map);
        assert!(!out.contains_key("/tmp/my.sock"));
        assert!(out.contains_key("/tmp/***"));
    }

    #[test]
    fn test_dedup_paths_4() {
        let mut map: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        map.insert("/***".into(), caps(&["read"]));
        map.insert("/etc/foo".into(), caps(&["read"]));
        map.insert("/etc".into(), caps(&["read"]));
        let out = dedup_paths(map);
        assert_eq!(out.len(), 1);
        assert_eq!(out.get("/***"), Some(&caps(&["read"])));
    }

    fn support_map(paths: &[&str]) -> PandoraMap<String, PandoraSet<String>> {
        let mut map: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        for p in paths {
            map.insert(p.to_string(), caps(&["read"]));
        }
        map
    }

    #[test]
    fn test_support_globs_1() {
        let out = support_globs(&support_map(&[
            "/w/t106_jhhd13",
            "/w/t106_KN5awK",
            "/w/only_ab12cd",
        ]));
        assert_eq!(
            out.get("/w/t106_jhhd13").map(String::as_str),
            Some("/w/t106_??????")
        );
        assert_eq!(
            out.get("/w/t106_KN5awK").map(String::as_str),
            Some("/w/t106_??????")
        );
        assert_eq!(out.get("/w/only_ab12cd"), None);
    }

    #[test]
    fn test_support_globs_2() {
        let out = support_globs(&support_map(&[
            "/spool/cache.001",
            "/spool/cache.002",
            "/data/1000",
            "/data/1001",
            "/w/log.12345",
            "/w/log.999",
        ]));
        assert_eq!(
            out.get("/spool/cache.001").map(String::as_str),
            Some("/spool/cache.[0-9]*")
        );
        assert_eq!(
            out.get("/data/1001").map(String::as_str),
            Some("/data/[0-9]*")
        );
        assert_eq!(
            out.get("/w/log.999").map(String::as_str),
            Some("/w/log.[0-9]*")
        );
    }

    #[test]
    fn test_support_globs_3() {
        let out = support_globs(&support_map(&[
            "/etc/ld.so.cache",
            "/etc/ld.so.conf",
            "/etc/config.backup",
            "/etc/config.update",
            "/etc/a.conf",
            "/etc/b.conf",
            "/etc/hostname",
            "/etc/hosts",
            "/w/t031_src",
            "/w/t031_dst",
        ]));
        assert!(out.is_empty());
    }

    #[test]
    fn test_support_globs_4() {
        let out = support_globs(&support_map(&[
            "/srv/sess_a1b2c3/data",
            "/srv/sess_a1b2c3/notes",
            "/srv/sess_d4e5f6/data",
            "/srv/sess_d4e5f6/logs",
            "/srv/sess_a1b2c3",
        ]));
        assert_eq!(
            out.get("/srv/sess_a1b2c3/notes").map(String::as_str),
            Some("/srv/sess_??????/notes")
        );
        assert_eq!(
            out.get("/srv/sess_d4e5f6/logs").map(String::as_str),
            Some("/srv/sess_??????/logs")
        );
        assert_eq!(
            out.get("/srv/sess_a1b2c3/data").map(String::as_str),
            Some("/srv/sess_??????/data")
        );
        assert_eq!(
            out.get("/srv/sess_d4e5f6/data").map(String::as_str),
            Some("/srv/sess_??????/data")
        );
        assert_eq!(
            out.get("/srv/sess_a1b2c3").map(String::as_str),
            Some("/srv/sess_??????")
        );
    }

    #[test]
    fn test_support_globs_5() {
        let out = support_globs(&support_map(&[
            "/w[1]/t106_abc123",
            "/w[1]/t106_def456",
            "/w/a[b]_x1y2z3",
            "/w/a[b]_p4q5r6",
        ]));
        assert!(out.is_empty());
    }

    #[test]
    fn test_support_globs_6() {
        let out = support_globs(&support_map(&["/w/t106_hXEy5U", "/w/t106_bvkdem"]));
        assert!(out.is_empty());
        let out = support_globs(&support_map(&[
            "/w/t106_ab12cd",
            "/w/t106_ef34gh",
            "/w/t106_bvkdem",
        ]));
        assert_eq!(
            out.get("/w/t106_bvkdem").map(String::as_str),
            Some("/w/t106_??????")
        );
    }

    #[test]
    fn test_support_globs_7() {
        let out = support_globs(&support_map(&[
            "/dev/pts/1",
            "/dev/pts/2",
            "/sys/class/hwmon/hwmon1",
            "/sys/class/hwmon/hwmon2",
            "/proc/123",
            "/proc/456",
            "@sock.123",
            "@sock.456",
        ]));
        assert!(out.is_empty());
    }

    #[test]
    fn test_support_globs_8() {
        let out = support_globs(&support_map(&[
            "/w/t106_ab12cd",
            "/w/t106_ef345h",
            "/w/t106_de45",
        ]));
        assert_eq!(
            out.get("/w/t106_ab12cd").map(String::as_str),
            Some("/w/t106_??????")
        );
        assert_eq!(out.get("/w/t106_de45"), None);
    }

    #[test]
    fn test_support_globs_9() {
        let corpus = support_map(&[
            "/w/t106_jhhd13",
            "/w/t106_KN5awK",
            "/spool/cache.001",
            "/spool/cache.002",
            "/data/1000",
            "/data/1001",
            "/srv/sess_a1b2c3/data",
            "/srv/sess_d4e5f6/data",
            "/w/t9_abc123",
            "/w/t9_def456",
            "/w/log.12345",
            "/w/log.999",
        ]);
        let out = support_globs(&corpus);
        assert_eq!(out.len(), 12);
        for (path, glob) in &out {
            assert!(glob_matches(glob, path));
        }
    }

    #[test]
    fn test_support_rewrite_1() {
        let mut node = SupportNode::default();
        node.renames.insert("a".into(), "?".into());
        assert_eq!(support_rewrite(&node, "/a"), None);
        assert_eq!(support_rewrite(&node, "/b"), None);
    }

    #[test]
    fn test_support_globs_11() {
        let out = support_globs(&support_map(&[
            "/w/obj_000001.o",
            "/w/obj_000002.o",
            "/w/v1_2.bin",
            "/w/v3_4.bin",
            "/w/plain.txt",
            "/w/other.txt",
        ]));
        assert_eq!(
            out.get("/w/obj_000001.o").map(String::as_str),
            Some("/w/obj_[0-9]*.o")
        );
        assert_eq!(
            out.get("/w/v3_4.bin").map(String::as_str),
            Some("/w/v[0-9]*_[0-9]*.bin")
        );
        assert_eq!(out.get("/w/plain.txt"), None);
        assert_eq!(out.get("/w/other.txt"), None);
        let out = support_globs(&support_map(&["/w/v]1.o", "/w/v]2.o"]));
        assert_eq!(
            out.get("/w/v]1.o").map(String::as_str),
            Some("/w/v\\][0-9]*.o")
        );
    }

    #[test]
    fn test_support_globs_10() {
        let mut map = support_map(&["/w/t9_ab12cd", "/w/t9_ef34gh"]);
        map.insert("/tmp/job_*/***".into(), caps(&["create"]));
        let out = support_globs(&map);
        assert_eq!(out.len(), 2);
        assert_eq!(out.get("/tmp/job_*/***"), None);
    }

    #[test]
    fn test_reduce_magic_1() {
        let mut magic: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        magic.insert("/a/b/c/d/t064.sock".into(), caps(&["net/bind", "delete"]));
        magic.insert("tcp!1.2.3.4!80".into(), caps(&["net/connect"]));
        magic.insert("@gecko-crash-helper-pipe.*".into(), caps(&["net/connect"]));
        let out = reduce_magic(magic, 3);
        assert!(out
            .get("/a/b/c/d/t064.sock")
            .is_some_and(|c| c.contains("net/bind") && !c.contains("delete")));
        assert!(out
            .get("/a/b/c/***")
            .is_some_and(|c| c.contains("delete") && !c.contains("net/bind")));
        assert_eq!(out.get("tcp!1.2.3.4!80"), Some(&caps(&["net/connect"])));
        assert_eq!(
            out.get("@gecko-crash-helper-pipe.*"),
            Some(&caps(&["net/connect"]))
        );
    }

    #[test]
    fn test_reduce_magic_2() {
        let mut magic: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        magic.insert("/srv/app/t9_ab12cd".into(), caps(&["write"]));
        magic.insert("/srv/app/t9_ef34gh".into(), caps(&["read"]));
        let out = reduce_magic(magic, 0);
        assert_eq!(
            out.get("/srv/app/t9_??????"),
            Some(&caps(&["read", "write"]))
        );
        assert_eq!(out.len(), 1);
        let mut magic: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        magic.insert("/a/b/c/d/t9_ab12cd".into(), caps(&["write"]));
        magic.insert("/a/b/c/d/t9_ef34gh".into(), caps(&["read"]));
        let out = reduce_magic(magic, 2);
        assert_eq!(out.get("/a/b/***"), Some(&caps(&["read", "write"])));
        assert_eq!(out.len(), 1);
    }

    #[test]
    fn test_reduce_magic_3() {
        let mut magic: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        magic.insert("/dev/pts/5".into(), caps(&["write"]));
        let out = reduce_magic(magic, 3);
        assert!(out.contains_key("/dev/pts/[0-9]*"));
    }

    #[test]
    fn test_reduce_magic_4() {
        let mut magic: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        magic.insert("/tmp/job_*/***".into(), caps(&["create", "delete"]));
        magic.insert("/proc/self/fd/[0-9]*".into(), caps(&["read"]));
        magic.insert("/srv/app/t9_ab12cd".into(), caps(&["write"]));
        magic.insert("/srv/app/t9_ef34gh".into(), caps(&["read"]));
        magic.insert("/etc/hostname".into(), caps(&["read"]));
        magic.insert("/a/b/c/d/e".into(), caps(&["read"]));
        magic.insert("tcp!1.2.3.4!80".into(), caps(&["net/connect"]));
        let once = reduce_magic(magic, 3);
        assert_eq!(
            once.get("/tmp/job_*/***"),
            Some(&caps(&["create", "delete"]))
        );
        assert_eq!(once.get("/proc/self/fd/[0-9]*"), Some(&caps(&["read"])));
        let twice = reduce_magic(once.clone(), 3);
        assert_eq!(twice, once);
    }

    #[test]
    fn test_dedup_paths_5() {
        let accesses: &[(&str, &str)] = &[
            ("/usr/lib/libc.so.6", "read"),
            ("/usr/lib/ld-linux-x86-64.so.2", "read"),
            ("/usr/lib/libX11.so.6", "read"),
            ("/etc/ld.so.cache", "read"),
            ("/etc/resolv.conf", "read"),
            ("/home/user/syd.pandora2/target/debug/t004", "exec"),
            ("/home/user/syd.pandora2/target/debug/t004_hXEy5U", "write"),
            ("/home/user/syd.pandora2/target/debug/t004_aQ3zW9", "create"),
            ("/home/user/syd.pandora2/target/debug/t004_pL7xK2", "delete"),
            ("/tmp/sess_a1b2c3", "read"),
            ("/tmp/sess_d4e5f6", "read"),
            ("/tmp/sess_g7h8i9", "read"),
            ("/proc/131037/status", "read"),
            ("/proc/self/task/131037/fd/3", "read"),
            ("/dev/pts/28", "write"),
            ("/dev/null", "write"),
        ];
        for limit in [0u8, 3] {
            let mut raw: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
            for &(path, cap) in accesses {
                raw.entry(path.to_string())
                    .or_default()
                    .insert(cap.to_string());
            }
            let map = reduce_magic(raw, limit);
            let out = dedup_paths(consolidate_sibling_paths(map));
            for &(path, cap) in accesses {
                assert!(out
                    .iter()
                    .any(|(k, v)| v.contains(cap) && glob_matches(k, path)));
            }
        }
    }

    #[test]
    fn test_dedup_paths_6() {
        let mut map: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        for d in ["sess_a1b2c3", "sess_d4e5f6", "sess_g7h8i9"] {
            for f in ["data-Kf8mQ2pR", "data-Xy7Zw3Qa", "data-Mn2Pq8Rt"] {
                map.insert(format!("/tmp/{d}/{f}"), caps(&["read"]));
            }
        }
        let once = dedup_paths(consolidate_sibling_paths(map));
        assert_eq!(dedup_paths(consolidate_sibling_paths(once.clone())), once);
        let mut next = once.clone();
        next.insert("/tmp/sess_zz9Yx8Ww/data-Qq1Rr2Tt".into(), caps(&["write"]));
        let merged = dedup_paths(consolidate_sibling_paths(next));
        assert_eq!(merged.get("/tmp/***"), Some(&caps(&["read", "write"])));
        assert_eq!(merged.len(), 1);
    }

    #[test]
    fn test_limit_path_1() {
        assert_eq!(limit_path("/a/b/c/d", 3), "/a/b/c/***");
        assert_eq!(limit_path("/a/b/c/d", 4), "/a/b/c/d");
        assert_eq!(limit_path("/a/b/c/d", 0), "/a/b/c/d");
        assert_eq!(limit_path("a/b/c/d", 1), "a/b/c/d");
        assert_eq!(limit_path("/a/b/c/d/***", 3), "/a/b/c/***");
    }

    #[test]
    fn test_limit_path_2() {
        assert_eq!(limit_path("/a/b/c/d/[0-9]*", 2), "/a/b/***");
        assert_eq!(
            limit_path("/run/user/[0-9]*/a/b/c", 3),
            "/run/user/[0-9]*/a/***"
        );
        assert_eq!(
            limit_path("/run/user/[0-9]*/sess_*/***", 3),
            "/run/user/[0-9]*/sess_*/***"
        );
    }

    #[test]
    fn test_limit_path_3() {
        assert_eq!(limit_path("/a/b\\*/c/d", 2), "/a/b\\*/***");
        assert_eq!(
            limit_path(
                "/usr/lib/gcc/x86_64-pc-linux-gnu/[0-9]*/libgcc_s.so.1/***",
                3
            ),
            "/usr/lib/gcc/***"
        );
    }

    #[test]
    fn test_limit_dir_path_1() {
        assert_eq!(limit_dir_path("/a/b/c/d", 2), "/a/b");
        assert_eq!(limit_dir_path("/a/b", 2), "/a/b");
        assert_eq!(limit_dir_path("/a/b", 0), "/a/b");
        assert_eq!(limit_dir_path("a/b/c", 1), "a/b/c");
    }

    #[test]
    fn test_process_path_1() {
        assert_eq!(process_path("/", 3, &no_roots()), "/");
        assert_eq!(
            process_path("@dbus-session", 1, &no_roots()),
            "@dbus-session"
        );
        assert_eq!(
            process_path("2f6574632f686f73746e616d65", 1, &no_roots()),
            "2f6574632f686f73746e616d65"
        );
    }

    #[test]
    fn test_process_path_2() {
        assert_eq!(
            process_path("/tmp/foo[1]", 0, &no_roots()),
            "/tmp/foo\\[1\\]"
        );
        assert_eq!(process_path("/tmp/a*b", 0, &no_roots()), "/tmp/a\\*b");
        assert_eq!(process_path("/tmp/a?b", 0, &no_roots()), "/tmp/a\\?b");
        assert_eq!(process_path("/tmp/a\\b", 0, &no_roots()), "/tmp/a\\\\b");
        assert_eq!(process_path("@sock[1]", 0, &no_roots()), "@sock\\[1\\]");
    }

    #[test]
    fn test_process_path_4() {
        let path = "/usr/lib/gcc/x86_64-pc-linux-gnu/16/libgcc_s.so.1";
        assert_eq!(process_path(path, 3, &no_roots()), "/usr/lib/gcc/***");
        assert_eq!(process_path(path, 0, &no_roots()), path);
        assert_ne!(
            process_path(path, 3, &no_roots()),
            "/usr/lib/gcc/[0-9]*/***"
        );
    }

    #[test]
    fn test_process_path_3() {
        for &(input, _) in PATH2GLOB_CASES {
            let path = &canonical(input);
            for limit in [0u8, 1, 2, 3, 8, u8::MAX] {
                let glob = process_path(path, limit, &no_roots());
                assert!(glob_matches(&glob, path));
            }
        }
    }

    #[test]
    fn test_merge_lock_paths_1() {
        let mut fp: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        fp.insert("/tmp/mybinary".into(), caps(&["read", "exec"]));
        let mut dp: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        dp.entry("/tmp".into()).or_default().insert("delete".into());
        let merged = merge_lock_paths(fp, dp, 0);
        assert!(merged.contains_key("/tmp/mybinary"));
    }

    #[test]
    fn test_merge_lock_paths_2() {
        let mut fp: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        fp.entry("/tmp/t031_src".into())
            .or_default()
            .insert("wpath".into());
        fp.entry("/tmp/t031_dst".into())
            .or_default()
            .insert("wpath".into());
        let dp: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        let merged = merge_lock_paths(fp, dp, 0);
        assert!(merged.contains_key("/tmp/t031_src"));
        assert!(merged.contains_key("/tmp/t031_dst"));
    }

    #[test]
    fn test_merge_lock_paths_3() {
        let mut fp: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        fp.entry("/etc/hostname".into())
            .or_default()
            .insert("read".into());
        fp.entry("/etc/hosts".into())
            .or_default()
            .insert("read".into());
        let dp: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        let merged = merge_lock_paths(fp, dp, 0);
        assert!(merged.contains_key("/etc/hostname"));
        assert!(merged.contains_key("/etc/hosts"));
    }

    #[test]
    fn test_merge_lock_paths_4() {
        let mut fp: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        fp.entry("/proc/42/status".into())
            .or_default()
            .insert("read".into());
        let dp: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        let merged = merge_lock_paths(fp, dp, 0);
        assert!(merged.contains_key("/proc"));
        assert!(!merged.contains_key("/proc/42/status"));
    }

    #[test]
    fn test_merge_lock_paths_5() {
        let mut fp: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        fp.entry("/tmp/t033_out".into())
            .or_default()
            .insert("wpath".into());
        let mut dp: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        dp.entry("/tmp".into()).or_default().insert("delete".into());
        let merged = merge_lock_paths(fp, dp, 0);
        assert!(merged.contains_key("/tmp"));
        assert!(!merged.contains_key("/tmp/t033_out"));
    }

    #[test]
    fn test_merge_lock_paths_6() {
        let mut fp: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        fp.entry("/tmp/t031_src".into())
            .or_default()
            .insert("wpath".into());
        fp.entry("/tmp/t031_dst".into())
            .or_default()
            .insert("read".into());
        let dp: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        let merged = merge_lock_paths(fp, dp, 0);
        assert!(merged.contains_key("/tmp/t031_src"));
        assert!(merged.contains_key("/tmp/t031_dst"));
    }

    #[test]
    fn test_merge_lock_paths_7() {
        let mut fp: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        fp.entry("/proc/self/status".into())
            .or_default()
            .insert("read".into());
        let dp: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        let merged = merge_lock_paths(fp, dp, 0);
        assert!(merged.contains_key("/proc/self/status"));
        assert!(!merged.contains_key("/proc"));
    }

    #[test]
    fn test_merge_lock_paths_8() {
        let mut fp: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        fp.entry("/tmp/my.sock".into())
            .or_default()
            .insert("connect".into());
        let mut dp: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        dp.entry("/tmp".into()).or_default().insert("delete".into());
        let merged = merge_lock_paths(fp, dp, 0);
        assert!(merged.get("/tmp").is_some_and(|c| c.contains("connect")));
        assert!(!merged.contains_key("/tmp/my.sock"));
    }

    #[test]
    fn test_merge_lock_paths_9() {
        let mut fp: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        for i in 0..12 {
            fp.entry(format!("/tmp/proj/dir{i}/file"))
                .or_default()
                .insert("create".into());
        }
        let dp: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        let merged = merge_lock_paths(fp, dp, 0);
        for path in merged.keys() {
            assert!(!path.contains('*'));
        }
        assert!(merged.contains_key("/tmp/proj"));
    }

    #[test]
    fn test_merge_lock_paths_10() {
        let mut fp: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        fp.entry("/etc/foo".into())
            .or_default()
            .insert("read".into());
        let mut dp: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        dp.entry("/etc".into()).or_default().insert("read".into());
        let merged = merge_lock_paths(fp, dp, 0);
        assert!(merged.contains_key("/etc"));
        assert!(merged.contains_key("/etc/foo"));
    }

    #[test]
    fn test_merge_lock_paths_11() {
        let mut fp: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        fp.entry("/a/b/c/d/e".into())
            .or_default()
            .insert("read".into());
        let dp: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        let merged = merge_lock_paths(fp, dp, 2);
        assert!(merged.contains_key("/a/b"));
        assert!(!merged.contains_key("/a/b/c/d/e"));
    }

    #[test]
    fn test_merge_lock_paths_12() {
        let mut fp: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        fp.entry("/etc/foo".into())
            .or_default()
            .insert("read".into());
        let mut dp: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        dp.entry("/".into()).or_default().insert("read".into());
        let merged = merge_lock_paths(fp, dp, 0);
        assert!(merged.contains_key("/"));
        assert!(merged.contains_key("/etc/foo"));
    }

    #[test]
    fn test_merge_lock_paths_13() {
        let t = "/home/user/syd.pandora2/target/debug";
        let mut fp: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        let mut caps13 = PandoraSet::default();
        for c in ["wpath", "read", "create", "delete"] {
            caps13.insert(c.to_string());
        }
        fp.insert(format!("{t}/t004_hXEy5U"), caps13);
        let mut xcaps = PandoraSet::default();
        xcaps.insert("read".to_string());
        xcaps.insert("exec".to_string());
        fp.insert(format!("{t}/t004"), xcaps);
        let mut dp: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        dp.entry(t.to_string()).or_default().insert("create".into());
        dp.entry(t.to_string()).or_default().insert("delete".into());
        let merged = merge_lock_paths(fp, dp, 0);
        assert!(merged
            .get(t)
            .is_some_and(|c| c.contains("wpath") && c.contains("read")));
        assert!(!merged.contains_key("/home/user"));
        assert!(!merged.contains_key("/home"));
        assert!(merged.contains_key(&format!("{t}/t004")));
    }

    #[test]
    fn test_merge_lock_paths_14() {
        let mut dp: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        for d in [
            "/",
            "/proc",
            "/proc/self",
            "/proc/self/attr",
            "/proc/self/fd",
            "/proc/self/net",
            "/proc/self/ns",
            "/proc/self/task",
            "/proc/sys",
            "/proc/sys/kernel",
            "/proc/sys/net",
            "/proc/sys/fs",
            "/proc/bus",
            "/proc/fs",
            "/proc/tty",
        ] {
            dp.entry(d.to_string())
                .or_default()
                .insert("readdir".into());
        }
        let fp: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        let merged = merge_lock_paths(fp, dp.clone(), 0);
        for d in dp.keys() {
            assert!(merged.contains_key(d));
        }
    }

    #[test]
    fn test_merge_lock_paths_15() {
        let t = "/home/user/syd.pandora2/target/debug";
        let mut fp: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        fp.entry(format!("{t}/data-Kf8mQ2pR"))
            .or_default()
            .insert("read".into());
        fp.entry("/etc/ld.so.cache".into())
            .or_default()
            .insert("read".into());
        fp.entry("/usr/lib/libc.so.6".into())
            .or_default()
            .insert("read".into());
        let mut dp: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        dp.entry("/".into()).or_default().insert("readdir".into());
        dp.entry(t.to_string()).or_default().insert("create".into());
        let inputs: Vec<String> = fp.keys().chain(dp.keys()).cloned().collect();
        let merged = merge_lock_paths(fp, dp, 3);
        for key in merged.keys() {
            assert!(inputs
                .iter()
                .any(|i| i == key || i.starts_with(&format!("{key}/")) || key == "/"));
        }
        for input in &inputs {
            assert!(merged
                .keys()
                .any(|k| k == input || input.starts_with(&format!("{k}/")) || k == "/"));
        }
    }

    #[test]
    fn test_format_port_ranges_1() {
        assert!(format_port_ranges(&bitset(&[])).is_empty());
        assert_eq!(format_port_ranges(&bitset(&[80])), vec!["80"]);
        assert_eq!(
            format_port_ranges(&bitset(&[80, 81, 82, 443])),
            vec!["80-82", "443"]
        );
        assert_eq!(format_port_ranges(&bitset(&[1, 3, 5])), vec!["1", "3", "5"]);
        assert_eq!(
            format_port_ranges(&bitset(&[0, 1, 65535])),
            vec!["0-1", "65535"]
        );
        assert_eq!(format_port_ranges(&bitset(&[10, 11])), vec!["10-11"]);
    }

    #[test]
    fn test_path2dehex_1() {
        assert_eq!(
            path2dehex("2f6574632f686f73746e616d65"),
            PathBuf::from("/etc/hostname")
        );
        assert_eq!(path2dehex("/etc/hostname"), PathBuf::from("/etc/hostname"));
        assert_eq!(path2dehex("zz"), PathBuf::from("zz"));
    }

    #[test]
    fn test_mkglob_sock_abs_1() {
        assert_eq!(
            mkglob_sock_abs("@cuda-uvmfd--1-63797"),
            Some("@cuda-uvmfd--*".into())
        );
        assert_eq!(
            mkglob_sock_abs("@gecko-crash-helper-pipe.462275"),
            Some("@gecko-crash-helper-pipe.*".into())
        );
        assert_eq!(mkglob_sock_abs("@gecko-media-plugins.sock"), None);
        assert_eq!(mkglob_sock_abs("/tmp/@gecko-foo.123"), None);
        assert_eq!(mkglob_sock_abs("@dbus-session"), None);
        assert_eq!(mkglob_sock_abs("@cuda-uvmfd--"), None);
        assert_eq!(mkglob_sock_abs("@gecko-nodot"), None);
    }

    #[test]
    fn test_path2glob_1() {
        for &(input, expected) in PATH2GLOB_CASES {
            let result = path2glob(input, &no_roots()).map(|c| c.into_owned());
            let expected = expected.map(|s| s.to_string());
            assert_eq!(result, expected);
        }
    }

    fn shape_suffix(shape: u32) -> String {
        let lower = ['a', 'b', 'c', 'd', 'e', 'f'];
        let upper = ['G', 'H', 'J', 'K', 'M', 'N'];
        let digit = ['1', '2', '3', '4', '5', '6'];
        let mut s = String::new();
        let mut n = shape;
        for i in 0..6 {
            s.push(match n % 3 {
                0 => lower[i],
                1 => upper[i],
                _ => digit[i],
            });
            n /= 3;
        }
        s
    }

    #[test]
    fn test_mutation_roots_1() {
        let mut map: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        map.insert("/w/t004_a".into(), caps(&["create", "write"]));
        map.insert("/w/t004_b".into(), caps(&["delete"]));
        map.insert("/x/y/z".into(), caps(&["rename"]));
        map.insert("/m/scratch".into(), caps(&["mkdir", "rmdir"]));
        map.insert("/etc/hosts".into(), caps(&["read"]));
        map.insert("/top".into(), caps(&["create"]));
        map.insert("tcp!127.0.0.1!80".into(), caps(&["net/connect"]));
        let roots = mutation_roots(&map);
        assert!(roots.contains("/w"));
        assert!(roots.contains("/x/y"));
        assert!(roots.contains("/m"));
        assert!(!roots.contains("/etc"));
        assert!(!roots.contains("/"));
        assert_eq!(roots.len(), 3);
    }

    #[test]
    fn test_path2glob_4() {
        let mut roots = no_roots();
        roots.insert("/home/user/syd.pandora2/target/debug".into());
        roots.insert("/home/user/data-Kf8mQ2pR".into());
        assert_eq!(
            path2glob("/home/user/syd.pandora2/target/debug/t004_hXEy5U", &roots).as_deref(),
            Some("/home/user/syd.pandora2/target/debug/t004_*/***")
        );
        assert_eq!(
            path2glob("/home/user/data-Kf8mQ2pR/x8Z9q2ab", &roots).as_deref(),
            Some("/home/user/data-Kf8mQ2pR/*/***")
        );
        assert_eq!(
            path2glob("/home/user/data-Kf8mQ2pR/x8Z9q2ab", &no_roots()),
            None
        );
        assert_eq!(
            path2glob("/home/user/syd.pandora2/target/debug", &roots),
            None
        );
        assert_eq!(path2glob("/home/user/other/x8Z9q2ab", &roots), None);
        assert_eq!(
            path2glob("/home/user/data-Kf8mQ2pRx/x8Z9q2ab", &roots),
            None
        );
    }

    #[test]
    fn test_path2glob_5() {
        let mut roots = no_roots();
        roots.insert("/tmp/t106_hzX08K".into());
        assert_eq!(
            path2glob("/tmp/t106_hzX08K", &roots).as_deref(),
            Some("/tmp/t106_*/***")
        );
        assert_eq!(
            path2glob("/tmp/t106_hzX08K/target", &roots).as_deref(),
            Some("/tmp/t106_*/target/***")
        );
        let mut short = no_roots();
        short.insert("/run/user".into());
        assert_eq!(
            path2glob("/run/user/1000/x8Z9q2ab", &short).as_deref(),
            Some("/run/user/[0-9]*/*/***")
        );
    }

    #[test]
    fn test_path2glob_2() {
        for &(input, _) in PATH2GLOB_CASES {
            let path = &canonical(input);
            if let Some(glob) = path2glob(path, &no_roots()) {
                assert!(glob_matches(&glob, path));
            }
        }
    }

    #[test]
    fn test_path2glob_3() {
        for shape in 0..729u32 {
            let s1 = shape_suffix(shape);
            let s2 = shape_suffix((shape + 364) % 729);
            let path = format!("/tmp/t106_{s1}");
            let glob = path2glob(&path, &no_roots()).unwrap();
            assert!(glob_matches(&glob, &format!("/tmp/t106_{s2}")));
        }
    }

    #[test]
    fn test_glob2regex_1() {
        assert!(glob_matches("/a/*", "/a/b"));
        assert!(glob_matches("/a/*", "/a/"));
        assert!(!glob_matches("/a/*", "/a/b/c"));
        assert!(glob_matches("/a/**", "/a/b/c"));
        assert!(glob_matches("/a/***", "/a"));
        assert!(glob_matches("/a/***", "/a/b/c"));
        assert!(!glob_matches("/a/***", "/ab"));
        assert!(glob_matches("/dev/pts/[0-9]*", "/dev/pts/42"));
        assert!(!glob_matches("/dev/pts/[0-9]*", "/dev/pts/ptmx"));
        assert!(glob_matches("pipe:\\[[0-9]*\\]", "pipe:[123]"));
        assert!(!glob_matches("pipe:\\[[0-9]*\\]", "pipe:x123x"));
        assert!(glob_matches("/tmp/foo\\[1\\]", "/tmp/foo[1]"));
        assert!(!glob_matches("/tmp/foo[1]", "/tmp/foo[1]"));
        assert!(glob_matches("/a/?", "/a/b"));
        assert!(!glob_matches("/a/?", "/a/bc"));
    }

    #[test]
    fn test_lock_wide_collapse_1() {
        let mut map: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        for i in 0..40 {
            map.insert(format!("/usr/share/doc/pkg{i}"), caps(&["read"]));
        }
        map.insert("/etc/hostname".into(), caps(&["read"]));
        let out = lock_wide_collapse(map);
        assert_eq!(out.get("/usr/share/doc"), Some(&caps(&["read"])));
        assert!(out.contains_key("/etc/hostname"));
        assert_eq!(out.len(), 2);
    }

    #[test]
    fn test_lock_wide_collapse_2() {
        let mut map: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        for i in 0..10 {
            map.insert(format!("/etc/conf{i}"), caps(&["read"]));
        }
        let out = lock_wide_collapse(map.clone());
        assert_eq!(out, map);
    }

    #[test]
    fn test_merge_lock_paths_16() {
        let mut fp: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        for i in 0..40 {
            fp.insert(format!("/usr/share/doc/pkg{i}/README"), caps(&["read"]));
        }
        let dp: PandoraMap<String, PandoraSet<String>> = PandoraMap::default();
        let merged = merge_lock_paths(fp, dp, 4);
        assert_eq!(merged.get("/usr/share/doc"), Some(&caps(&["read"])));
        assert_eq!(merged.len(), 1);
    }

    #[test]
    fn test_subst_id_with_1() {
        assert_eq!(
            subst_id_with("/run/user/1000/bus", "1000", "1000"),
            "/run/user/${SYD_UID}/bus"
        );
        assert_eq!(
            subst_id_with("/run/user/1000", "1000", "1000"),
            "/run/user/${SYD_UID}"
        );
        assert_eq!(
            subst_id_with("/sys/fs/cgroup/100/x", "1000", "100"),
            "/sys/fs/cgroup/${SYD_GID}/x"
        );
        assert_eq!(
            subst_id_with("/sys/fs/cgroup/100", "1000", "100"),
            "/sys/fs/cgroup/${SYD_GID}"
        );
    }

    #[test]
    fn test_subst_id_with_2() {
        assert_eq!(subst_id_with("/run/user/0", "0", "0"), "/run/user/0");
        assert_eq!(
            subst_id_with("/tmp/10001/x", "1000", "1000"),
            "/tmp/10001/x"
        );
        assert_eq!(subst_id_with("/tmp/plain", "1000", "1000"), "/tmp/plain");
    }

    #[test]
    fn test_subst_id_with_3() {
        assert_eq!(
            subst_id_with("/a/1000/1000/b", "1000", "1000"),
            "/a/${SYD_UID}/${SYD_UID}/b"
        );
        assert_eq!(
            subst_id_with("/a/1000/1000", "1000", "1000"),
            "/a/${SYD_UID}/${SYD_UID}"
        );
    }
}
