cargo-reclaim 0.2.2

Safe Cargo cleanup for target directories, stale artifacts, and Cargo home caches
Documentation
use std::ffi::OsStr;
use std::path::{Component, Path};

use crate::model::ArtifactClass;

pub fn classify_target_relative_path(path: impl AsRef<Path>) -> ArtifactClass {
    Classifier.classify_target_relative_path(path)
}

#[derive(Debug, Clone, Copy, Default)]
pub struct Classifier;

impl Classifier {
    pub fn classify_target_relative_path(self, path: impl AsRef<Path>) -> ArtifactClass {
        let Some(components) = normalize_components(path.as_ref()) else {
            return ArtifactClass::Unknown;
        };

        if components.is_empty() {
            return ArtifactClass::Unknown;
        }

        let file_artifact_class = classify_file_artifact(&components);
        if file_artifact_class != ArtifactClass::Unknown {
            return file_artifact_class;
        }

        classify_named_directory(&components).unwrap_or(ArtifactClass::Unknown)
    }
}

fn normalize_components(path: &Path) -> Option<Vec<&OsStr>> {
    let mut components = Vec::new();

    for component in path.components() {
        match component {
            Component::CurDir => {}
            Component::Normal(component) => components.push(component),
            Component::ParentDir | Component::RootDir | Component::Prefix(_) => return None,
        }
    }

    Some(components)
}

fn classify_named_directory(components: &[&OsStr]) -> Option<ArtifactClass> {
    let index = cargo_artifact_dir_index(components)?;
    let component = components.get(index)?;

    if *component == "build" {
        return (index > 0 && components.len() > index + 1).then_some(ArtifactClass::BuildScripts);
    }

    if *component == "incremental" {
        Some(ArtifactClass::Incremental)
    } else if *component == "deps" {
        Some(ArtifactClass::Deps)
    } else if *component == ".fingerprint"
        || *component == "fingerprint"
        || *component == ".rustdoc_fingerprint"
    {
        Some(ArtifactClass::Fingerprint)
    } else if *component == "doc" || *component == "docs" {
        Some(ArtifactClass::Docs)
    } else if *component == "package" {
        Some(ArtifactClass::Package)
    } else if *component == "timings" || *component == "cargo-timings" {
        Some(ArtifactClass::Timings)
    } else if *component == "tmp" || *component == "sqlx-tmp" {
        Some(ArtifactClass::Tmp)
    } else {
        None
    }
}

fn cargo_artifact_dir_index(components: &[&OsStr]) -> Option<usize> {
    if components.is_empty() {
        return None;
    }

    if is_known_artifact_dir(components[0]) {
        return Some(0);
    }

    if components.len() >= 2
        && is_profile_root(components[0])
        && is_known_artifact_dir(components[1])
    {
        return Some(1);
    }

    if components.len() >= 3
        && is_target_triple(components[0])
        && is_profile_root(components[1])
        && is_known_artifact_dir(components[2])
    {
        return Some(2);
    }

    None
}

fn classify_file_artifact(components: &[&OsStr]) -> ArtifactClass {
    let extension = file_extension(components);

    if is_deps_output_location(components) {
        return ArtifactClass::DepsOutput;
    }

    if is_known_intermediate_file_location(components) {
        if extension.is_some_and(|extension| extension == "d") {
            return ArtifactClass::DepInfo;
        }

        if extension.is_some_and(|extension| extension == "o" || extension == "obj") {
            return ArtifactClass::ObjectMetadata;
        }
    }

    if is_final_output_location(components) {
        match extension {
            Some(extension) if extension == "rlib" => return ArtifactClass::FinalRlib,
            Some(extension) if extension == "wasm" => return ArtifactClass::FinalWasm,
            Some(extension) if has_dynamic_or_static_library_extension(extension) => {
                return ArtifactClass::FinalLibrary;
            }
            Some(extension) if extension == "exe" => return ArtifactClass::FinalExecutable,
            None => return ArtifactClass::FinalExecutable,
            _ => {}
        }
    }

    ArtifactClass::Unknown
}

fn is_known_intermediate_file_location(components: &[&OsStr]) -> bool {
    match components {
        [profile, dir, _] => is_profile_root(profile) && *dir == "deps",
        [triple, profile, dir, _] => {
            is_target_triple(triple) && is_profile_root(profile) && *dir == "deps"
        }
        _ => false,
    }
}

fn is_final_output_location(components: &[&OsStr]) -> bool {
    match components {
        [profile, file_name] => {
            is_default_profile_root(profile) && is_plausible_profile_root_final_output(file_name)
        }
        [first, second, third] => {
            (is_target_triple(first)
                && is_default_profile_root(second)
                && is_plausible_profile_root_final_output(third))
                || (is_default_profile_root(first)
                    && *second == "examples"
                    && is_plausible_nested_final_output(third))
        }
        [triple, profile, dir, file_name] => {
            is_target_triple(triple)
                && is_default_profile_root(profile)
                && *dir == "examples"
                && is_plausible_nested_final_output(file_name)
        }
        _ => false,
    }
}

fn is_deps_output_location(components: &[&OsStr]) -> bool {
    match components {
        [profile, dir, _] => is_profile_root(profile) && *dir == "deps",
        [triple, profile, dir, _] => {
            is_target_triple(triple) && is_profile_root(profile) && *dir == "deps"
        }
        _ => false,
    }
}

fn is_plausible_profile_root_final_output(file_name: &OsStr) -> bool {
    is_plausible_nested_final_output(file_name) && !is_known_profile_support_entry(file_name)
}

fn is_plausible_nested_final_output(file_name: &OsStr) -> bool {
    !is_known_final_output_support_entry(file_name)
}

fn is_known_profile_support_entry(component: &OsStr) -> bool {
    is_known_final_output_support_entry(component) || component == "examples"
}

fn is_known_final_output_support_entry(component: &OsStr) -> bool {
    is_known_artifact_dir(component) || component == ".cargo-lock"
}

fn is_known_artifact_dir(component: &OsStr) -> bool {
    component == "incremental"
        || component == "deps"
        || component == "build"
        || component == ".fingerprint"
        || component == "fingerprint"
        || component == ".rustdoc_fingerprint"
        || component == "doc"
        || component == "docs"
        || component == "package"
        || component == "timings"
        || component == "cargo-timings"
        || component == "tmp"
        || component == "sqlx-tmp"
}

fn is_profile_root(component: &OsStr) -> bool {
    !is_known_artifact_dir(component)
        && component != "target"
        && component
            .to_str()
            .is_some_and(|component| !component.is_empty() && !component.contains('.'))
}

fn is_default_profile_root(component: &OsStr) -> bool {
    component == "debug" || component == "release"
}

fn is_target_triple(component: &OsStr) -> bool {
    component
        .to_str()
        .is_some_and(|component| component.matches('-').count() >= 2)
}

fn has_dynamic_or_static_library_extension(extension: &OsStr) -> bool {
    extension == "a"
        || extension == "so"
        || extension == "dylib"
        || extension == "dll"
        || extension == "lib"
}

fn file_extension<'a>(components: &'a [&'a OsStr]) -> Option<&'a OsStr> {
    file_name(components).and_then(|file_name| Path::new(file_name).extension())
}

fn file_name<'a>(components: &'a [&'a OsStr]) -> Option<&'a OsStr> {
    components.last().copied()
}