// SPDX-FileCopyrightText: 2024 Ɓukasz Niemier <#@hauleth.dev> // // SPDX-License-Identifier: EUPL-1.2 use std::path::Path; use async_trait::async_trait; use serde::Deserialize; use tokio::fs; use tokio::io::{self, AsyncReadExt, AsyncSeekExt}; use futures::prelude::*; use crate::pattern::Pattern; #[typetag::deserialize(tag = "type")] #[async_trait] pub trait Filter: std::fmt::Debug + Sync { async fn matches(&self, path: &Path) -> bool; } macro_rules! filter { { $name:literal; $(#[$sattr:meta])* struct $sname:ident {$($(#[$attr:meta])* $fname:ident : $ftype:ty,)*}; ($self_:ident, $path:ident) -> $body:block } => { #[derive(Debug, serde::Deserialize)] $(#[$sattr])* pub struct $sname { $($(#[$attr])* $fname: $ftype),* } #[typetag::deserialize(name = $name)] #[async_trait] impl Filter for $sname { async fn matches(&$self_, $path: &Path) -> bool { $body } } }; { $name:literal; $(#[$sattr:meta])* struct $sname:ident ($($field:ty),*); ($self_:ident, $path:ident) -> $body:block } => { #[derive(Debug, serde::Deserialize)] $(#[$sattr])* pub struct $sname ($($field),*); #[typetag::deserialize(name = $name)] #[async_trait] impl Filter for $sname { async fn matches(&$self_, $path: &Path) -> bool { $body } } } } filter! { "name"; struct Name(Pattern); (self, path) -> { self.0.matches(path) } } #[derive(Deserialize)] #[serde(remote = "std::cmp::Ordering")] #[serde(rename_all = "snake_case")] enum Ordering { Less, Equal, Greater, } impl Ordering { fn default() -> std::cmp::Ordering { std::cmp::Ordering::Equal } } filter! { "size"; struct Size { size: u64, #[serde(with = "Ordering", default = "Ordering::default")] ordering: std::cmp::Ordering, }; (self, path) -> { with_metadata(path, |metadata| { let len = metadata.len(); len.cmp(&self.size) == self.ordering }) .await } } #[derive(Debug, Deserialize)] #[serde(tag = "is", rename_all = "snake_case")] pub enum FileType { Dir, File, Symlink, } #[typetag::deserialize(name = "file_type")] #[async_trait] impl Filter for FileType { async fn matches(&self, path: &Path) -> bool { use FileType::*; with_metadata(path, |metadata| { let ft = metadata.file_type(); match *self { Dir => ft.is_dir(), File => ft.is_file(), Symlink => ft.is_symlink(), } }) .await } } async fn with_metadata(path: &Path, fun: F) -> bool where F: FnOnce(std::fs::Metadata) -> bool, { fs::metadata(path).await.map(fun).unwrap_or(false) } filter! { "not"; struct Not { filter: Box, }; (self, path) -> { !self.filter.matches(path).await } } filter! { "any"; struct Any { filters: Box<[Box]>, }; (self, path) -> { stream::iter(&*self.filters) .any(|f| f.matches(path)) .await } } filter! { "all"; struct All { filters: Box<[Box]>, }; (self, path) -> { stream::iter(&*self.filters) .all(|f| f.matches(path)) .await } } #[derive(Debug, Deserialize)] #[serde(rename_all = "snake_case")] pub enum Magic { Mime(String), Magic { bytes: Box<[u8]>, #[serde(default)] offset: u64, }, } async fn read_first_bytes(n: usize, path: &Path, offset: u64) -> io::Result> { use std::io::SeekFrom; let mut file = fs::File::open(path).await?; let mut buf = vec![0; n]; file.seek(SeekFrom::Start(offset)).await?; file.read_exact(&mut buf).await?; Ok(buf.into()) } async fn guess_mime(path: &Path) -> Option { let mut file = fs::File::open(path).await.ok()?; let mut buf = vec![0; 8192]; let len = file.read(&mut buf).await.ok()?; infer::get(&buf[0..len]) } #[typetag::deserialize(name = "content_type")] #[async_trait] impl Filter for Magic { async fn matches(&self, file: &Path) -> bool { match *self { Self::Magic { ref bytes, offset } => read_first_bytes(bytes.len(), file, offset) .await .map(|read| read == *bytes) .unwrap_or(false), Self::Mime(ref mime_type) => guess_mime(file) .await .map(|typ| typ.mime_type() == mime_type) .unwrap_or(false), } } }