use std::ffi::OsString;
use std::io::Result as IoResult;
use std::path::{
Path,
PathBuf
};
use serde::{Serialize, Deserialize};
use strum::EnumString;
use tokio::fs;
use super::{
EventType,
DynamicEventFilter,
PeerEventFilter,
PeerEventTypeFilter,
NodeFilter,
DaemonEvent,
handle::ParsedHandler,
handle::HandlerScript
};
use crate::peer::NodeID;
use crate::util::*;
#[derive(Clone, Debug, EnumString, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum DirectoryType {
V1
}
enum ResolvedEntry {
Directory {
name: String,
path: PathBuf
},
File {
name: String,
path: PathBuf
},
SymlinkedFile {
name: String,
path: PathBuf
},
}
pub struct EntryToParse {
components: Vec<String>,
file_path: PathBuf
}
struct EntryParser<'a, R: Fn(&str) -> Option<NodeID>> {
resolver: &'a R,
path: PathBuf,
name: String,
ev_type: Option<EventType>,
node_filter: Option<ParsedNodeFilter>,
tag: Option<String>,
daemon_filter: Option<DaemonEvent>,
peer_ev_filter: Option<PeerEventTypeFilter>
}
enum ParsedNodeFilter {
Id(NodeID),
Name(String)
}
pub async fn get_all_entries(dir: &Path) -> IoResult<Vec<EntryToParse>> {
let mut entries = Vec::<EntryToParse>::new();
let mut stack = Vec::<String>::new();
let mut dir_stack = Vec::<fs::ReadDir>::new();
dir_stack.push(fs::read_dir(dir).await?);
'dirs: while let Some(mut dir_iter) = dir_stack.pop() {
while let Some(dir_entry) = dir_iter.next_entry().await? {
match ResolvedEntry::resolve(dir_entry).await? {
ResolvedEntry::Directory{name, path} => {
stack.push(name);
dir_stack.push(dir_iter);
dir_stack.push(fs::read_dir(&path).await?);
continue 'dirs;
},
ResolvedEntry::File{name, path} => entries.push(
EntryToParse::new(&stack, name, path)
),
ResolvedEntry::SymlinkedFile{name, path} => entries.push(
EntryToParse::new(&stack, name, path)
)
}
}
stack.pop();
}
Ok(entries)
}
pub fn parse_all<R>(resolver: &R, entries: Vec<EntryToParse>)
-> Vec<Result<ParsedHandler, String>>
where R: Fn(&str) -> Option<NodeID>
{
entries
.into_iter()
.map(|e| EntryParser::parse(resolver, e))
.collect()
}
impl std::fmt::Display for ParsedNodeFilter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Id(node_id) => write!(f, "@[{node_id}]"),
Self::Name(name) => write!(f, "@{name}")
}
}
}
impl<'a, R: Fn(&str) -> Option<NodeID>> EntryParser<'a, R> {
pub fn parse(resolver: &'a R, entry: EntryToParse)
-> Result<ParsedHandler, String>
{
let mut parser = Self::new(resolver, entry.file_path);
parser.build(entry.components)?;
parser.finish()
}
fn new(resolver: &'a R, path: PathBuf) -> Self {
Self {
resolver,
path,
name: String::new(),
ev_type: None,
node_filter: None,
tag: None,
daemon_filter: None,
peer_ev_filter: None
}
}
fn build(&mut self, components: Vec<String>) -> Result<(), String> {
for comp in components {
self.push(comp)?
}
let dot = self.name.pop();
assert_eq!(dot, Some('.'));
Ok(())
}
fn finish(mut self) -> Result<ParsedHandler, String> {
let node_filter = self.resolve_node_filter()?;
let script = HandlerScript {
name: self.name,
path: self.path
};
match self.ev_type.expect("ev_type always set in build()") {
EventType::Daemon => Ok(ParsedHandler::Daemon {
script,
filter: self.daemon_filter
}),
EventType::Dynamic => {
Ok(ParsedHandler::Dynamic {
script,
filter: DynamicEventFilter::simple(node_filter, self.tag)
})
},
EventType::Peer => {
let peer_filter = self.peer_ev_filter.unwrap_or(
PeerEventTypeFilter::Any
);
Ok(ParsedHandler::Peer{
script,
filter: PeerEventFilter::new(node_filter, peer_filter)
})
}
}
}
fn resolve_node_filter(&mut self) -> Result<NodeFilter, String> {
match self.node_filter.take() {
Some(ParsedNodeFilter::Id(id)) => Ok(Some(id)),
Some(ParsedNodeFilter::Name(name)) => (self.resolver)(&name)
.map(Some)
.ok_or_else(|| format!("unknown node {:?}", &name)),
None => Ok(None)
}
}
fn push(&mut self, next: String) -> Result<(), String> {
if next.is_empty() { self.error("invalid empty component", &next)? }
self.name.push_str(&next);
self.name.push('.');
if next.starts_with('#') { Ok(()) }
else if let Some(node_filter) = Self::get_node_filter(&next)? {
if self.node_filter.is_some() {
self.error("only one node filter allowed", &next)?
}
if let Some(EventType::Daemon) = self.ev_type {
self.error("node filter invalid for daemon events", &next)?
}
self.node_filter.replace(node_filter);
Ok(())
}
else { self.inner_parse(next) }
}
fn inner_parse(&mut self, next: String) -> Result<(), String> {
match self.ev_type {
Some(EventType::Peer) if self.peer_ev_filter.is_some() => {
self.error("invalid trailing path component", &next)?;
}
Some(EventType::Daemon) if self.daemon_filter.is_some() => {
self.error("invalid trailing path component", &next)?;
},
Some(EventType::Daemon) => match next.parse::<DaemonEvent>() {
Ok(filter) => self.daemon_filter = Some(filter),
Err(err) => self.error(&err.to_string(), &next)?,
},
Some(EventType::Dynamic) => {
match next.as_str() {
"daemon" |
"peer" => self.error("invalid event type filter", &next)?,
tag_chunk => self.push_tag_chunk(tag_chunk)
}
},
Some(EventType::Peer) => {
self.peer_ev_filter.replace(match next.as_str() {
"seen" => PeerEventTypeFilter::Seen,
"new_address" => PeerEventTypeFilter::NewAddress,
"status_changed" => PeerEventTypeFilter::StatusChanged,
_ => self.error("expected peer event type filter", &next)?
});
},
None if self.node_filter.is_some() => {
self.ev_type = match next.as_str() {
"daemon" =>
self.error("daemon event with node filter", &next)?,
"peer" => Some(EventType::Peer),
tag_chunk => {
self.push_tag_chunk(tag_chunk);
Some(EventType::Dynamic)
}
}
},
None => self.ev_type = Some(match next.as_str() {
"daemon" => EventType::Daemon,
"peer" => EventType::Peer,
tag_chunk => {
self.push_tag_chunk(tag_chunk);
EventType::Dynamic
}
})
}
Ok(())
}
fn push_tag_chunk(&mut self, tag_chunk: &str) {
if let Some(tag) = self.tag.as_mut() {
tag.push('.');
tag.push_str(tag_chunk);
}
else {
self.tag = Some(tag_chunk.to_string());
}
}
fn get_node_filter(next: &str) -> Result<Option<ParsedNodeFilter>, String> {
if next.starts_with("@[") {
if next.ends_with(']') && next.len() == 11 {
Ok(Some(ParsedNodeFilter::Id(
next.get(2..10).unwrap().parse()?
)))
}
else {
Err(format!("invalid node filter spec: {next}"))
}
}
else if next.starts_with('@') {
if next.len() < 2 {
Err(format!("invalid node filter spec: {next}"))
}
else {
Ok(next.get(1..)
.map(ToString::to_string)
.map(ParsedNodeFilter::Name)
)
}
}
else {
Ok(None)
}
}
fn error(&self, err_msg: &str, comp: &str) -> Result<Never, String> {
let msg = format!(
"failed to parse handler at {}!\nreceived \"{}\" after \"{}\": {}",
&self.path.display(),
comp,
&self.name,
err_msg
);
Err(msg)
}
}
impl EntryToParse {
pub fn new(
dir_stack: &[String],
file_name: String,
path: PathBuf)
-> Self
{
let mut stack = Self::flatten_dir_stack(dir_stack);
Self::split_file_name_into(&mut stack, file_name);
Self {
components: stack,
file_path: path
}
}
pub fn print(&self) {
let mut comp_iter = self.components.iter();
print!("{}", comp_iter.next().unwrap());
for c in comp_iter { print!(".{c}") }
println!("\t→\t{}", self.file_path.display());
}
fn flatten_dir_stack(dir_stack: &[String]) -> Vec<String> {
let mut res = Vec::new();
for dir_name in dir_stack {
for comp in dir_name.split('.') {
res.push(comp.to_owned())
}
}
res
}
fn split_file_name_into(stack: &mut Vec<String>, file_name: String) {
let split: Vec<&str> = file_name.split('.').collect();
if split.len() == 1 { stack.push(file_name); }
else {
for comp in split { stack.push(comp.to_owned()); }
}
}
}
impl ResolvedEntry {
pub async fn resolve(dir_entry: fs::DirEntry) -> IoResult<Self> {
let name = dir_entry
.file_name()
.into_string()
.map_err(invalid_file_name)?;
let mut file_type = dir_entry.file_type().await?;
let mut symlink_path: Option<PathBuf> = None;
if file_type.is_symlink() {
file_type = fs::metadata(&dir_entry.path())
.await?
.file_type();
symlink_path = Some(fs::canonicalize(&dir_entry.path()).await?);
}
if file_type.is_dir() {
Ok(Self::Directory {
name,
path: dir_entry.path()
})
}
else if file_type.is_file() {
if let Some(path) = symlink_path {
Ok(Self::SymlinkedFile{name, path})
}
else {
Ok(Self::File {
name,
path: fs::canonicalize(&dir_entry.path()).await?
})
}
}
else if file_type.is_symlink() {
unreachable!("fs::metadata should traverse symlinks");
}
else {
let msg = format!(
"unsupported file type, please remove {} from handler dir",
dir_entry.path().to_string_lossy()
);
Err(std::io::Error::other(msg))
}
}
}
fn invalid_file_name(invalid: OsString) -> std::io::Error {
let msg = format!("filename {invalid:?} contains invalid unicode");
use std::io;
io::Error::new(io::ErrorKind::InvalidData, msg)
}