weavatrix-scan 0.4.6

Deterministic, safe repository scanner for code intelligence
Documentation
use super::repository::RepositoryMatcher;
use super::{
    IgnoreError, IgnoreLayer, IgnoreRules, RuleSet, SourceRank, build_child_rules,
    normalized_evidence_location, source_evidence,
};
use crate::error::{Error, Result};
use crate::path::normalized_relative_path;
use crate::report::{IgnoreSourceEvidence, IgnoreSourceKind, ScanWarning};
use crate::walk_types::{ErrorPolicy, WalkEntry};
use std::fs;
use std::io;
use std::path::{Component, Path, PathBuf};
use std::sync::Arc;

impl RepositoryMatcher {
    pub(super) fn prepare_directory_inner(&mut self, absolute: &Path) -> Result<()> {
        let ignore_files = self.ignore_files.clone();
        self.prepare_directory_inner_with_files(absolute, &ignore_files)
    }

    pub(crate) fn prepare_directory_from_entries(
        &mut self,
        absolute: &Path,
        entries: &[WalkEntry],
    ) -> Result<()> {
        let ignore_files = self
            .ignore_files
            .iter()
            .filter(|name| {
                entries
                    .iter()
                    .any(|entry| entry.file_name() == name.as_str())
            })
            .cloned()
            .collect::<Vec<_>>();
        self.prepare_directory_inner_with_files(absolute, &ignore_files)
    }

    fn prepare_directory_inner_with_files(
        &mut self,
        absolute: &Path,
        ignore_files: &[String],
    ) -> Result<()> {
        if self.directories.contains_key(absolute) {
            return Ok(());
        }
        let relative = absolute.strip_prefix(&self.match_root).map_err(|_| {
            Error::io(
                absolute,
                io::Error::new(
                    io::ErrorKind::InvalidInput,
                    "directory escapes matcher root",
                ),
            )
        })?;
        let inherited = if relative.as_os_str().is_empty() {
            self.base_rules.clone()
        } else {
            let parent = absolute.parent().unwrap_or(&self.match_root).to_path_buf();
            self.prepare_directory_inner(&parent)?;
            self.directories
                .get(&parent)
                .cloned()
                .unwrap_or_else(|| self.base_rules.clone())
        };
        let base = normalized_relative_path(relative);
        let (rules, errors, evidence) = build_child_rules(
            absolute,
            &base,
            ignore_files,
            self.case_insensitive,
            &inherited,
            &self.match_root,
        );
        self.handle_errors(errors)?;
        self.sources.extend(evidence);
        self.directories.insert(absolute.to_path_buf(), rules);
        Ok(())
    }

    pub(super) fn absolute_path(&self, path: &Path) -> Result<PathBuf> {
        let escapes = path
            .components()
            .any(|component| component == Component::ParentDir);
        if escapes || (!path.is_absolute() && path.components().any(is_root_component)) {
            return Err(Error::io(
                path,
                io::Error::new(
                    io::ErrorKind::InvalidInput,
                    "path contains traversal components",
                ),
            ));
        }
        Ok(if path.is_absolute() {
            path.to_path_buf()
        } else {
            self.scan_root.join(path)
        })
    }

    pub(super) fn ensure_scan_scope(&self, path: &Path) -> Result<()> {
        if path.starts_with(&self.scan_root) {
            return Ok(());
        }
        Err(Error::io(
            path,
            io::Error::new(io::ErrorKind::InvalidInput, "path escapes scan root"),
        ))
    }

    pub(super) fn load_static_source(
        &mut self,
        path: &Path,
        base: &str,
        rank: SourceRank,
        kind: IgnoreSourceKind,
        location: &str,
    ) -> Result<()> {
        let (rules, errors, evidence) = add_rule_file(
            &self.base_rules,
            path,
            base,
            rank,
            kind,
            location,
            self.case_insensitive,
        );
        self.handle_errors(errors)?;
        self.base_rules = rules;
        self.sources.extend(evidence);
        Ok(())
    }

    pub(super) fn handle_errors(&mut self, errors: Vec<IgnoreError>) -> Result<()> {
        for source in errors {
            if self.error_policy == ErrorPolicy::Abort {
                return Err(Error::io(
                    &source.path,
                    io::Error::new(source.kind(), source.to_string()),
                ));
            }
            self.warnings.push(ScanWarning {
                relative: Some(normalized_evidence_location(&source.path, &self.match_root)),
                message: format!("could not load ignore rules: {source}"),
            });
        }
        Ok(())
    }
}

pub(super) fn find_repository_root(start: &Path) -> Option<PathBuf> {
    start
        .ancestors()
        .find(|candidate| candidate.join(".git").exists())
        .map(Path::to_path_buf)
}

pub(super) fn add_rule_file(
    inherited: &IgnoreRules,
    path: &Path,
    base: &str,
    rank: SourceRank,
    kind: IgnoreSourceKind,
    location: &str,
    case_insensitive: bool,
) -> (IgnoreRules, Vec<IgnoreError>, Vec<IgnoreSourceEvidence>) {
    let bytes = match fs::read(path) {
        Ok(bytes) => bytes,
        Err(error) if error.kind() == io::ErrorKind::NotFound => {
            return (inherited.clone(), Vec::new(), Vec::new());
        }
        Err(error) => {
            return (
                inherited.clone(),
                vec![IgnoreError {
                    kind: error.kind(),
                    path: path.to_path_buf(),
                    message: error.to_string(),
                }],
                Vec::new(),
            );
        }
    };
    let mut rules = RuleSet::default();
    let mut errors = Vec::new();
    super::parser::parse_file_bytes(path, &bytes, case_insensitive, &mut rules, &mut errors);
    let mut result = inherited.clone();
    if !rules.rules.is_empty() {
        let index = rank.index();
        result.layers[index] = Some(Arc::new(IgnoreLayer {
            base: base.to_owned(),
            rules,
            parent: result.layers[index].clone(),
        }));
    }
    (
        result,
        errors,
        vec![source_evidence(kind, location.to_owned(), &bytes)],
    )
}

fn is_root_component(component: Component<'_>) -> bool {
    matches!(component, Component::RootDir | Component::Prefix(_))
}