timefs 0.1.0

Mount a Git repository as a read-only filesystem.
Documentation
//! Shared path-resolution helpers for the filesystem namespace.

use crate::git::GitReference;

/// A ref namespace exposed beneath `refs/`.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RefNamespace {
    /// `refs/heads/*`
    Heads,
    /// `refs/tags/*`
    Tags,
    /// `refs/remotes/*`
    Remotes,
}

impl RefNamespace {
    /// Parse a top-level namespace component.
    pub fn from_component(component: &[u8]) -> Option<Self> {
        match component {
            b"heads" => Some(Self::Heads),
            b"tags" => Some(Self::Tags),
            b"remotes" => Some(Self::Remotes),
            _ => None,
        }
    }

    /// Return the full ref prefix for this namespace.
    pub fn prefix(self) -> &'static [u8] {
        match self {
            Self::Heads => b"refs/heads/",
            Self::Tags => b"refs/tags/",
            Self::Remotes => b"refs/remotes/",
        }
    }

    /// Build a full reference name from a relative path.
    pub fn full_name(self, relative: &[u8]) -> Vec<u8> {
        let mut full_name = self.prefix().to_vec();
        full_name.extend_from_slice(relative);
        full_name
    }

    /// Build a prefix that matches descendants of `relative`.
    pub fn prefix_with_relative(self, relative: &[u8]) -> Vec<u8> {
        let mut prefix = self.full_name(relative);
        prefix.push(b'/');
        prefix
    }
}

/// A greedy path candidate and how many components it consumed.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ConsumedPathSpec {
    /// Number of path components consumed by this candidate.
    pub consumed: usize,
    /// The joined path text to try.
    pub spec: Vec<u8>,
}

/// Join path components using `/`.
pub fn join_path_components(components: &[Vec<u8>]) -> Vec<u8> {
    let mut joined = Vec::new();
    for (index, component) in components.iter().enumerate() {
        if index > 0 {
            joined.push(b'/');
        }
        joined.extend_from_slice(component);
    }
    joined
}

/// Return greedy revision candidates from longest to shortest.
pub fn revision_candidates(components: &[Vec<u8>]) -> Vec<ConsumedPathSpec> {
    (1..=components.len())
        .rev()
        .map(|consumed| ConsumedPathSpec {
            consumed,
            spec: join_path_components(&components[..consumed]),
        })
        .collect()
}

/// Return greedy reference candidates from longest to shortest.
pub fn reference_candidates(
    namespace: RefNamespace,
    components: &[Vec<u8>],
) -> Vec<ConsumedPathSpec> {
    revision_candidates(components)
        .into_iter()
        .map(|candidate| ConsumedPathSpec {
            consumed: candidate.consumed,
            spec: namespace.full_name(&candidate.spec),
        })
        .collect()
}

/// Return the logical revision name for a browsable ref.
pub fn reference_revision_name(reference: &GitReference) -> Option<&[u8]> {
    reference
        .full_name
        .strip_prefix(b"refs/heads/")
        .or_else(|| reference.full_name.strip_prefix(b"refs/tags/"))
        .or_else(|| reference.full_name.strip_prefix(b"refs/remotes/"))
}

/// Return `true` if `spec` looks like a full or abbreviated hexadecimal object id.
pub fn is_hex_revision_spec(spec: &[u8]) -> bool {
    spec.len() >= 4 && spec.iter().all(u8::is_ascii_hexdigit)
}

/// Return `true` if a tree entry name is safe to expose as a FUSE node.
pub fn is_valid_tree_entry_name(name: &[u8]) -> bool {
    !(name.contains(&0) || name.contains(&b'/') || name == b"." || name == b"..")
}

/// Split raw path bytes on `/`, preserving empty leading or trailing components.
pub fn split_path_bytes(path: &[u8]) -> Vec<Vec<u8>> {
    path.split(|byte| *byte == b'/')
        .map(<[u8]>::to_vec)
        .collect()
}

#[cfg(test)]
mod tests {
    use super::{
        is_hex_revision_spec, is_valid_tree_entry_name, join_path_components, reference_candidates,
        revision_candidates, split_path_bytes, RefNamespace,
    };

    #[test]
    fn revision_candidates_are_greedy() {
        let components = vec![b"feature".to_vec(), b"x".to_vec(), b"README.md".to_vec()];
        assert_eq!(
            revision_candidates(&components)
                .into_iter()
                .map(|candidate| candidate.spec)
                .collect::<Vec<_>>(),
            vec![
                b"feature/x/README.md".to_vec(),
                b"feature/x".to_vec(),
                b"feature".to_vec(),
            ]
        );
    }

    #[test]
    fn reference_candidates_include_the_namespace_prefix() {
        let components = vec![b"feature".to_vec(), b"x".to_vec()];
        assert_eq!(
            reference_candidates(RefNamespace::Heads, &components)
                .into_iter()
                .map(|candidate| candidate.spec)
                .collect::<Vec<_>>(),
            vec![
                b"refs/heads/feature/x".to_vec(),
                b"refs/heads/feature".to_vec()
            ]
        );
    }

    #[test]
    fn basic_path_helpers_match_expected_rules() {
        assert_eq!(
            join_path_components(&[b"refs".to_vec(), b"heads".to_vec(), b"main".to_vec()]),
            b"refs/heads/main".to_vec()
        );
        assert_eq!(
            split_path_bytes(b"refs/heads/main"),
            vec![b"refs".to_vec(), b"heads".to_vec(), b"main".to_vec()]
        );
        assert!(is_hex_revision_spec(b"deadbeef"));
        assert!(!is_hex_revision_spec(b"HEAD"));
        assert!(is_valid_tree_entry_name(b"README.md"));
        assert!(!is_valid_tree_entry_name(b"."));
        assert!(!is_valid_tree_entry_name(b"dir/name"));
    }
}