patholog 1.0.0

Diagnose, explain, clean, and safely repair PATH behavior.
Documentation
use crate::model::{PathVariable, PlatformMode, ShellKind};
use crate::normalize::first_unique_entries;
use crate::path_env::parse_path;
use crate::platform::resolve_platform_rules;
use crate::policy::PathPolicy;

#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct CleanedPath {
    entries: Vec<String>,
    separator: char,
}

impl CleanedPath {
    pub(crate) fn raw_path(&self) -> String {
        self.entries.join(&self.separator.to_string())
    }
}

#[cfg(any(test, feature = "fuzzing"))]
pub(crate) fn clean_path(
    path_value: &str,
    platform_mode: PlatformMode,
    pathext: Option<&str>,
) -> String {
    clean_path_with_policy(
        path_value,
        platform_mode,
        pathext,
        PathVariable::Path,
        &PathPolicy::default(),
    )
}

pub(crate) fn clean_path_with_policy(
    path_value: &str,
    platform_mode: PlatformMode,
    pathext: Option<&str>,
    variable: PathVariable,
    policy: &PathPolicy,
) -> String {
    clean_with_policy(path_value, platform_mode, pathext, variable, policy).raw_path()
}

#[cfg(any(test, feature = "fuzzing"))]
pub(crate) fn clean_export(
    path_value: &str,
    platform_mode: PlatformMode,
    pathext: Option<&str>,
    shell: ShellKind,
) -> String {
    clean_export_with_policy(
        path_value,
        platform_mode,
        pathext,
        shell,
        PathVariable::Path,
        &PathPolicy::default(),
    )
}

pub(crate) fn clean_export_with_policy(
    path_value: &str,
    platform_mode: PlatformMode,
    pathext: Option<&str>,
    shell: ShellKind,
    variable: PathVariable,
    policy: &PathPolicy,
) -> String {
    format_clean_export(
        &clean_with_policy(path_value, platform_mode, pathext, variable, policy),
        shell,
        variable,
    )
}

pub(crate) fn clean_with_policy(
    path_value: &str,
    platform_mode: PlatformMode,
    pathext: Option<&str>,
    variable: PathVariable,
    policy: &PathPolicy,
) -> CleanedPath {
    let rules = resolve_platform_rules(platform_mode, pathext);
    let policy = policy.compile(platform_mode, pathext);
    let entries = parse_path(path_value, platform_mode, pathext);
    let entries = entries
        .into_iter()
        .filter(|entry| !policy.matches_entry(entry))
        .collect::<Vec<_>>();
    let entries = cleaned_entries(&entries, variable);
    CleanedPath {
        entries,
        separator: rules.separator,
    }
}

fn cleaned_entries(entries: &[crate::model::PathEntry], variable: PathVariable) -> Vec<String> {
    match variable {
        PathVariable::Path => first_unique_entries(entries)
            .into_iter()
            .map(|entry| entry.raw)
            .collect(),
        PathVariable::Manpath => first_unique_manpath_entries(entries),
    }
}

fn first_unique_manpath_entries(entries: &[crate::model::PathEntry]) -> Vec<String> {
    let mut output = Vec::new();
    let mut seen = Vec::<String>::new();
    for entry in entries {
        if entry.is_empty {
            output.push(entry.raw.clone());
            continue;
        }
        if seen.contains(&entry.comparison_key) {
            continue;
        }
        seen.push(entry.comparison_key.clone());
        output.push(entry.raw.clone());
    }
    output
}

pub(crate) fn format_clean_export(
    cleaned: &CleanedPath,
    shell: ShellKind,
    variable: PathVariable,
) -> String {
    match shell {
        ShellKind::Bash | ShellKind::Zsh => {
            format!(
                "export {}={}",
                variable.env_name(),
                posix_single_quote(&cleaned.raw_path())
            )
        }
        ShellKind::Fish => format_fish_export(&cleaned.entries, variable),
        ShellKind::Pwsh => {
            format!(
                "$env:{} = {}",
                variable.powershell_env_name(),
                powershell_single_quote(&cleaned.raw_path())
            )
        }
    }
}

fn format_fish_export(entries: &[String], variable: PathVariable) -> String {
    let quoted_entries = entries
        .iter()
        .map(|entry| fish_single_quote(entry))
        .collect::<Vec<_>>();
    if quoted_entries.is_empty() {
        format!("set -gx {}", variable.env_name())
    } else {
        format!(
            "set -gx {} {}",
            variable.env_name(),
            quoted_entries.join(" ")
        )
    }
}

fn posix_single_quote(value: &str) -> String {
    format!("'{}'", value.replace('\'', "'\\''"))
}

fn fish_single_quote(value: &str) -> String {
    let mut quoted = String::from("'");
    for character in value.chars() {
        match character {
            '\\' => quoted.push_str("\\\\"),
            '\'' => quoted.push_str("\\'"),
            _ => quoted.push(character),
        }
    }
    quoted.push('\'');
    quoted
}

fn powershell_single_quote(value: &str) -> String {
    format!("'{}'", value.replace('\'', "''"))
}

#[cfg(test)]
#[path = "clean/tests.rs"]
mod tests;