nix-index 0.1.10

Nix (package manager) indexing primitives
Documentation
//! Data types for working with trees of files.
//!
//! The main type here is `FileTree` which represents
//! such as the file listing for a store path.
use std::collections::HashMap;
use std::io::{self, Write};
use std::str::{self, FromStr};

use clap::builder::PossibleValue;
use clap::ValueEnum;
use memchr::memchr;
use serde::{Deserialize, Serialize};
use serde_bytes::ByteBuf;

use crate::frcode;

/// This enum represents a single node in a file tree.
///
/// The type is generic over the contents of a directory node,
/// because we want to use this enum to represent both a flat
/// structure where a directory only stores some meta-information about itself
/// (such as the number of children) and full file trees, where a
/// directory contains all the child nodes.
///
/// Note that file nodes by themselves do not have names. Names are given
/// to file nodes by the parent directory, which has a map of entry names to
/// file nodes.
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
pub enum FileNode<T> {
    /// A regular file. This is the normal kind of file which is
    /// neither a directory not a symlink.
    Regular {
        /// The size of this file, in bytes.
        size: u64,
        /// Whether or not this file has the `executable` bit set.
        executable: bool,
    },
    /// A symbolic link that points to another file path.
    Symlink {
        /// The path that this symlink points to.
        target: ByteBuf,
    },
    /// A directory. It usually has a mapping of names to child nodes (in
    /// the case of a fill tree), but we also support a reduced form where
    /// we only store the number of entries in the directory.
    Directory {
        /// The size of a directory is the number of children it contains.
        size: u64,

        /// The contents of this directory. These are generic, as explained
        /// in the documentation for this type.
        contents: T,
    },
}

/// The type of a file.
///
/// This mirrors the variants of `FileNode`, but without storing
/// data in each variant.
///
/// An exception to this is the `executable` field for the regular type.
/// This is needed since we present `regular` and `executable` files as different
/// to the user, so we need a way to represent both types.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum FileType {
    Regular { executable: bool },
    Directory,
    Symlink,
}

impl ValueEnum for FileType {
    fn value_variants<'a>() -> &'a [Self] {
        &[
            FileType::Regular { executable: false },
            FileType::Regular { executable: true },
            FileType::Directory,
            FileType::Symlink,
        ]
    }

    fn to_possible_value(&self) -> Option<PossibleValue> {
        match self {
            FileType::Regular { executable: false } => Some(PossibleValue::new("r")),
            FileType::Regular { executable: true } => Some(PossibleValue::new("x")),
            FileType::Directory => Some(PossibleValue::new("d")),
            FileType::Symlink => Some(PossibleValue::new("s")),
        }
    }
}

impl FromStr for FileType {
    type Err = &'static str;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "r" => Ok(FileType::Regular { executable: false }),
            "x" => Ok(FileType::Regular { executable: true }),
            "d" => Ok(FileType::Directory),
            "s" => Ok(FileType::Symlink),
            _ => Err("invalid file type"),
        }
    }
}

/// This lists all file types that can currently be represented.
pub const ALL_FILE_TYPES: &[FileType] = &[
    FileType::Regular { executable: true },
    FileType::Regular { executable: false },
    FileType::Directory,
    FileType::Symlink,
];

impl<T> FileNode<T> {
    /// Split this node into a node without contents and optionally the contents themselves,
    /// if the node was a directory.
    pub fn split_contents(&self) -> (FileNode<()>, Option<&T>) {
        use self::FileNode::*;
        match *self {
            Regular { size, executable } => (Regular { size, executable }, None),
            Symlink { ref target } => (
                Symlink {
                    target: target.clone(),
                },
                None,
            ),
            Directory { size, ref contents } => (Directory { size, contents: () }, Some(contents)),
        }
    }

    /// Return the type of this file.
    pub fn get_type(&self) -> FileType {
        match *self {
            FileNode::Regular { executable, .. } => FileType::Regular { executable },
            FileNode::Directory { .. } => FileType::Directory,
            FileNode::Symlink { .. } => FileType::Symlink,
        }
    }
}

impl FileNode<()> {
    fn encode<W: Write>(&self, encoder: &mut frcode::Encoder<W>) -> io::Result<()> {
        use self::FileNode::*;
        match *self {
            Regular { executable, size } => {
                let e = if executable { "x" } else { "r" };
                encoder.write_meta(format!("{}{}", size, e).as_bytes())?;
            }
            Symlink { ref target } => {
                encoder.write_meta(target)?;
                encoder.write_meta(b"s")?;
            }
            Directory { size, contents: () } => {
                encoder.write_meta(format!("{}d", size).as_bytes())?;
            }
        }
        Ok(())
    }

    pub fn decode(buf: &[u8]) -> Option<Self> {
        use self::FileNode::*;
        buf.split_last().and_then(|(kind, buf)| match *kind {
            b'x' | b'r' => {
                let executable = *kind == b'x';
                str::from_utf8(buf)
                    .ok()
                    .and_then(|s| s.parse().ok())
                    .map(|size| Regular { executable, size })
            }
            b's' => Some(Symlink {
                target: ByteBuf::from(buf),
            }),
            b'd' => str::from_utf8(buf)
                .ok()
                .and_then(|s| s.parse().ok())
                .map(|size| Directory { size, contents: () }),
            _ => None,
        })
    }
}

/// This type represents a full tree of files.
///
/// A *file tree* is a *file node* where each directory contains
/// the tree for its children.
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
pub struct FileTree(FileNode<HashMap<ByteBuf, FileTree>>);

/// An entry in a file tree is a path to a node paired with that node.
///
/// If the entry refers to a directory, it only stores information about that
/// directory itself. It does not contain the children of the directory.
pub struct FileTreeEntry {
    pub path: Vec<u8>,
    pub node: FileNode<()>,
}

impl FileTreeEntry {
    pub fn encode<W: Write>(self, encoder: &mut frcode::Encoder<W>) -> io::Result<()> {
        self.node.encode(encoder)?;
        encoder.write_path(self.path)?;
        Ok(())
    }

    pub fn decode(buf: &[u8]) -> Option<FileTreeEntry> {
        memchr(b'\0', buf).and_then(|sep| {
            let path = &buf[(sep + 1)..];
            let node = &buf[0..sep];
            FileNode::decode(node).map(|node| FileTreeEntry {
                path: path.to_vec(),
                node,
            })
        })
    }
}

impl FileTree {
    pub fn regular(size: u64, executable: bool) -> Self {
        FileTree(FileNode::Regular { size, executable })
    }

    pub fn symlink(target: ByteBuf) -> Self {
        FileTree(FileNode::Symlink { target })
    }

    pub fn directory(entries: HashMap<ByteBuf, FileTree>) -> Self {
        FileTree(FileNode::Directory {
            size: entries.len() as u64,
            contents: entries,
        })
    }

    pub fn to_list(&self, filter_prefix: &[u8]) -> Vec<FileTreeEntry> {
        let mut result = Vec::new();

        let mut stack = Vec::with_capacity(16);
        stack.push((Vec::new(), self));

        while let Some(entry) = stack.pop() {
            let path = entry.0;
            let FileTree(current) = entry.1;
            let (node, contents) = current.split_contents();
            if let Some(entries) = contents {
                let mut entries = entries.iter().collect::<Vec<_>>();
                entries.sort_by(|a, b| Ord::cmp(a.0, b.0));
                for (name, entry) in entries {
                    let mut path = path.clone();
                    path.push(b'/');
                    path.extend_from_slice(name);
                    stack.push((path, entry));
                }
            }
            if path.starts_with(filter_prefix) {
                result.push(FileTreeEntry { path, node });
            }
        }
        result
    }
}