semdiff-core 0.1.1

Core traversal, diff calculation, and reporting traits for semdiff.
Documentation
use crate::{LeafTraverse, NodeTraverse, TraversalNode};
use memmap2::Mmap;
use mime::Mime;
use std::fs::File;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use thiserror::Error;

#[derive(Debug, Clone)]
pub struct FileLeaf {
    pub name: String,
    pub kind: Mime,
    pub content: Arc<Mmap>,
}

impl LeafTraverse for FileLeaf {
    fn name(&self) -> &str {
        &self.name
    }
}

#[derive(Debug, Error)]
pub enum FsTreeError {
    #[error("failed to read directory: {0}")]
    ReadDir(#[source] io::Error),
    #[error("failed to read file metadata: {0}")]
    Metadata(#[source] io::Error),
    #[error("failed to open file: {0}")]
    Open(io::Error),
}

#[derive(Clone, Debug)]
pub struct FsNode {
    abs_path: PathBuf,
    name: String,
}

impl FsNode {
    pub fn new_root(path: PathBuf) -> FsNode {
        FsNode {
            abs_path: path,
            name: "".to_owned(),
        }
    }

    fn new(abs_path: PathBuf, name: String) -> Self {
        Self { abs_path, name }
    }
}

impl NodeTraverse for FsNode {
    type Leaf = FileLeaf;

    type TraverseError = FsTreeError;

    fn name(&self) -> &str {
        &self.name
    }

    fn children(
        &mut self,
    ) -> Result<impl Iterator<Item = Result<TraversalNode<Self, Self::Leaf>, Self::TraverseError>>, Self::TraverseError>
    {
        let entries = match std::fs::read_dir(&self.abs_path) {
            Ok(entries) => entries,
            Err(err) => return Err(FsTreeError::ReadDir(err)),
        };

        Ok(entries.map(|entry| {
            let entry = entry.map_err(FsTreeError::ReadDir)?;
            let file_type = entry.file_type().map_err(FsTreeError::Metadata)?;
            let name = entry.file_name();
            let abs_path = entry.path();
            let name = name.to_string_lossy().into_owned();
            if file_type.is_dir() {
                Ok(TraversalNode::Node(FsNode::new(abs_path, name)))
            } else {
                let file = File::open(entry.path()).map_err(FsTreeError::Open)?;
                let content = unsafe { Mmap::map(&file) }.map_err(FsTreeError::Open)?;
                let kind = detect_file_kind(&abs_path, &content);
                let leaf = FileLeaf {
                    name,
                    kind,
                    content: Arc::new(content),
                };
                Ok(TraversalNode::Leaf(leaf))
            }
        }))
    }
}

fn detect_file_kind(path: &Path, body: &[u8]) -> Mime {
    if let Some(kind) = infer::get(body)
        && let Ok(mime) = kind.mime_type().parse()
    {
        mime
    } else if let Some(mime) = mime_guess::from_path(path).first() {
        mime
    } else {
        mime::APPLICATION_OCTET_STREAM
    }
}