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;
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
pub enum FileNode<T> {
Regular {
size: u64,
executable: bool,
},
Symlink {
target: ByteBuf,
},
Directory {
size: u64,
contents: T,
},
}
#[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"),
}
}
}
pub const ALL_FILE_TYPES: &[FileType] = &[
FileType::Regular { executable: true },
FileType::Regular { executable: false },
FileType::Directory,
FileType::Symlink,
];
impl<T> FileNode<T> {
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)),
}
}
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,
})
}
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
pub struct FileTree(FileNode<HashMap<ByteBuf, FileTree>>);
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
}
}