tokmd-scan 1.10.0

Source code scanning adapter (Tokei wrapper) for tokmd.
Documentation
use std::fs;
use std::io;
use std::path::{Component, Path, PathBuf};

use super::{PathViolation, ValidatedRoot};

#[derive(Debug, Clone)]
pub(crate) struct BoundedPath {
    relative: PathBuf,
    canonical: PathBuf,
}

impl BoundedPath {
    pub(crate) fn existing_relative(
        root: &ValidatedRoot,
        relative: &Path,
    ) -> Result<Self, PathViolation> {
        let normalized = normalize_bounded_relative_path(relative)?;
        let candidate = root.canonical().join(&normalized);
        let canonical = canonicalize_existing(&candidate)?;
        ensure_under_root(root, &canonical)?;

        Ok(Self {
            relative: normalized,
            canonical,
        })
    }

    pub(crate) fn existing_child(
        root: &ValidatedRoot,
        child: &Path,
    ) -> Result<Self, PathViolation> {
        let canonical = canonicalize_existing(child)?;
        ensure_under_root(root, &canonical)?;
        let relative = canonical
            .strip_prefix(root.canonical())
            .map(Path::to_path_buf)
            .unwrap_or_else(|_| child.file_name().map(PathBuf::from).unwrap_or_default());

        if relative.as_os_str().is_empty() {
            return Err(PathViolation::Empty);
        }

        Ok(Self {
            relative,
            canonical,
        })
    }

    pub(crate) fn relative(&self) -> &Path {
        &self.relative
    }

    pub(crate) fn canonical(&self) -> &Path {
        &self.canonical
    }
}

pub(crate) fn normalize_bounded_relative_path(path: &Path) -> Result<PathBuf, PathViolation> {
    if path.as_os_str().is_empty() {
        return Err(PathViolation::Empty);
    }

    let mut normalized = PathBuf::new();
    for component in path.components() {
        match component {
            Component::Normal(segment) => normalized.push(segment),
            Component::CurDir => {}
            Component::ParentDir => return Err(PathViolation::ParentTraversal(path.to_path_buf())),
            Component::RootDir | Component::Prefix(_) => {
                return Err(PathViolation::Absolute(path.to_path_buf()));
            }
        }
    }

    if normalized.as_os_str().is_empty() {
        return Err(PathViolation::Empty);
    }

    Ok(normalized)
}

fn canonicalize_existing(path: &Path) -> Result<PathBuf, PathViolation> {
    match fs::canonicalize(path) {
        Ok(canonical) => Ok(canonical),
        Err(source) if source.kind() == io::ErrorKind::NotFound => {
            match fs::symlink_metadata(path) {
                Err(meta_err) if meta_err.kind() == io::ErrorKind::NotFound => {
                    Err(PathViolation::Missing(path.to_path_buf()))
                }
                Ok(_) => Err(PathViolation::CanonicalizeFailed {
                    path: path.to_path_buf(),
                    source,
                }),
                Err(source) => Err(PathViolation::CanonicalizeFailed {
                    path: path.to_path_buf(),
                    source,
                }),
            }
        }
        Err(source) => Err(PathViolation::CanonicalizeFailed {
            path: path.to_path_buf(),
            source,
        }),
    }
}

fn ensure_under_root(root: &ValidatedRoot, canonical: &Path) -> Result<(), PathViolation> {
    if canonical.starts_with(root.canonical()) {
        Ok(())
    } else {
        Err(PathViolation::RootEscape {
            root: root.canonical().to_path_buf(),
            path: canonical.to_path_buf(),
        })
    }
}