use std::{
borrow::Cow, ffi::OsString, fmt::Display, future::Future, iter::once, num::NonZeroUsize,
ops::ControlFlow, path::PathBuf, sync::Arc,
};
use log::{debug, error, warn};
use smallvec::SmallVec;
use tokio::{
fs::ReadDir,
sync::{
mpsc::{self, error::SendError, unbounded_channel},
Semaphore,
},
task::JoinHandle,
};
pub const fn ascii_char_translator(c: u8) -> Option<u8> {
Some(match c {
b'0' => 0,
b'1' => 1,
b'2' => 2,
b'3' => 3,
b'4' => 4,
b'5' => 5,
b'6' => 6,
b'7' => 7,
b'8' => 8,
b'9' => 9,
b'a' | b'A' => 10,
b'b' | b'B' => 11,
b'c' | b'C' => 12,
b'd' | b'D' => 13,
b'e' | b'E' => 14,
b'f' | b'F' => 15,
_ => return None,
})
}
#[derive(thiserror::Error, Debug)]
pub enum HashFormatErr {
#[error("\"{0}\" is a non-ascii string, and hence not a valid SHA1 hash")]
NonAscii(String),
#[error("\"{0}\" is the wrong length for a SHA1 hash (expected 40 characters)")]
WrongLength(String),
#[error("Non-hex character in SHA1 string at {position}")]
InvalidSHA1HexCharacter { position: usize },
}
pub fn hash_from_string(v: &str) -> Result<[u8; 20], HashFormatErr> {
if !v.is_ascii() {
return Err(HashFormatErr::NonAscii(v.to_owned()));
};
let (raw_str_bytes, raw_str) = (v.as_bytes(), v);
if raw_str_bytes.len() != 2 * 20 {
return Err(HashFormatErr::WrongLength(raw_str.to_owned()));
};
let mut v: [u8; 20] = Default::default();
for (idx, item) in v.iter_mut().enumerate() {
*item = (|idx| -> Result<u8, HashFormatErr> {
let lhs = ascii_char_translator(*raw_str_bytes.get(2 * idx).unwrap())
.ok_or(HashFormatErr::InvalidSHA1HexCharacter { position: 2 * idx })?
<< 4;
let rhs = ascii_char_translator(*raw_str_bytes.get(2 * idx + 1).unwrap()).ok_or(
HashFormatErr::InvalidSHA1HexCharacter {
position: 2 * idx + 1,
},
)?;
Ok(rhs | lhs)
})(idx)?;
}
Ok(v)
}
#[cfg(test)]
pub mod test_hash_from_str {
use crate::util::hash_from_string;
#[test]
pub fn basic() {
const VALUE: u8 = 60;
const FIRST_ASCII: u8 = b'3';
const SECOND_ASCII: u8 = b'C';
const ASCII_ENCODED_BYTE: [u8; 2] = [FIRST_ASCII, SECOND_ASCII];
let bytestring: [u8; 40] = core::array::from_fn(|e| ASCII_ENCODED_BYTE[e % 2]);
let rawstring = core::str::from_utf8(&bytestring).unwrap();
let actual_hash = [VALUE; 20];
assert_eq!(
actual_hash,
hash_from_string(rawstring).expect("Should be valid parseable string")
);
}
}
pub fn normalise_remote_neocities_path(s: Cow<'_, str>) -> Cow<'_, str> {
match s {
Cow::Borrowed(v) => Cow::Borrowed(v.trim_matches('/')),
Cow::Owned(v) => {
let r = v.trim_matches('/');
if r == v.as_str() {
Cow::Owned(v)
} else {
Cow::Owned(r.to_owned())
}
}
}
}
pub type SplitPath = SmallVec<[OsString; 8]>;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum SymlinkResolvedType {
File,
Directory,
}
pub async fn recursive_directory_traverser(
initial_path: PathBuf,
ignore_io_errors: bool,
) -> (
JoinHandle<Result<(), anyhow::Error>>,
mpsc::UnboundedReceiver<
Result<(SplitPath, tokio::fs::DirEntry, SymlinkResolvedType), tokio::io::Error>,
>,
) {
let (sender, receiver) = mpsc::unbounded_channel();
const MAX_SYMLINK_DEPTH_INCLUSIVE: usize = 256;
(
tokio::spawn(async move {
let initial_directory = tokio::fs::read_dir(&initial_path).await.map_err(|err| {
error!("Can't open directory to even upload");
error!("{err}");
err
})?;
let mut directory_stack = Vec::<(Option<OsString>, ReadDir, bool)>::with_capacity(8);
directory_stack.push((None, initial_directory, false));
let mut symlink_cnt: usize = 0;
let sender_by_ref = &sender;
'lp: while !directory_stack.is_empty() {
let mut current_directory =
directory_stack.pop().expect("just checked for emptiness");
let next_entry = current_directory.1.next_entry().await;
match next_entry {
Err(e) => {
error!("Error reading directory entry");
error!("{e}");
sender_by_ref.send(Err(e))?;
directory_stack.push(current_directory);
}
Ok(Some(directory_entry)) => {
let metadata = match directory_entry.metadata().await {
Ok(v) => v,
Err(err) => {
warn!("Error reading information about directory entry");
warn!("{err}");
if ignore_io_errors {
directory_stack.push(current_directory);
continue 'lp;
} else {
error!("Ceasing all directory traversal with failure");
return Err(err.into());
}
}
};
let entry_filename = directory_entry.file_name();
let full_entry_path = directory_entry.path();
let directory_path_stack: SplitPath = directory_stack
.iter()
.map(|(name, _, _)| name)
.cloned()
.chain(once(current_directory.0.clone()))
.flatten()
.collect();
let send_current_as = move |resolved_type: SymlinkResolvedType| {
sender_by_ref.send(Ok((
directory_path_stack,
directory_entry,
resolved_type,
)))
};
if metadata.is_dir() {
send_current_as(SymlinkResolvedType::Directory)?;
let read_dir = match tokio::fs::read_dir(&full_entry_path).await {
Ok(read_dir) => read_dir,
Err(err) => {
warn!(
"Could not open {} for reading as directory",
full_entry_path.display()
);
warn!("{err}");
if ignore_io_errors {
warn!("Skipping");
directory_stack.push(current_directory);
continue 'lp;
} else {
error!("Aborting traversal");
return Err(err.into());
}
}
};
directory_stack.extend(
[current_directory, (Some(entry_filename), read_dir, false)]
.into_iter(),
);
} else if metadata.is_symlink()
&& symlink_cnt <= MAX_SYMLINK_DEPTH_INCLUSIVE
{
let refined_metadata = match tokio::fs::metadata(&full_entry_path).await
{
Ok(v) => v,
Err(e) => {
warn!("Could not obtain symlink metadata about {} to determine traversal properties", &full_entry_path.display());
warn!("{e}");
if ignore_io_errors {
warn!("Skipping");
directory_stack.push(current_directory);
continue 'lp;
} else {
error!(
"Could not obtain information about symlink, dieing"
);
return Err(e.into());
}
}
};
if refined_metadata.is_dir() {
send_current_as(SymlinkResolvedType::Directory)?;
let read_dir = match tokio::fs::read_dir(&full_entry_path).await {
Ok(v) => v,
Err(e) => {
warn!(
"Could not open symlinked-directory {}",
full_entry_path.display()
);
warn!("{e}");
if ignore_io_errors {
warn!("Skipping");
directory_stack.push(current_directory);
continue 'lp;
} else {
error!("Could not open symlinked directory, dieing!");
return Err(e.into());
}
}
};
directory_stack.extend(
[current_directory, (Some(entry_filename), read_dir, true)]
.into_iter(),
);
symlink_cnt += 1;
} else {
send_current_as(SymlinkResolvedType::File)?;
debug!(
"Symlink {} is not a directory, not attempting to traverse",
full_entry_path.display()
);
directory_stack.push(current_directory);
}
} else if metadata.is_symlink() && symlink_cnt > MAX_SYMLINK_DEPTH_INCLUSIVE
{
warn!("Not traversing symlink at {} because it would overflow the limit of {MAX_SYMLINK_DEPTH_INCLUSIVE} to prevent infinite loops", full_entry_path.display());
directory_stack.push(current_directory);
} else {
send_current_as(SymlinkResolvedType::File)?;
directory_stack.push(current_directory);
}
}
Ok(None) => {
if current_directory.2 {
symlink_cnt -= 1;
}
}
}
}
Ok(())
}),
receiver,
)
}
#[derive(thiserror::Error, Debug)]
pub enum SendOrBreakError<ChannelItem, BreakError> {
#[error(transparent)]
SendErr(SendError<ChannelItem>),
#[error(transparent)]
BreakErr(BreakError),
}
pub async fn restricted_parallel_async<
O: Send + 'static,
State: Send + Clone + 'static,
BreakError: Send + 'static,
IterGenerator: IntoIterator<Item = Item> + Send + 'static,
Item: Send + 'static,
ResolvedItem: Send + 'static,
GeneratedTask: Future<Output = O> + Send + 'static,
>(
state: State,
task_generator: IterGenerator,
mut resolver: impl 'static + Send + FnMut(Item) -> Result<ResolvedItem, ControlFlow<BreakError, ()>>,
mut task_ctor: impl 'static + Send + FnMut(State, ResolvedItem) -> GeneratedTask,
maximum_concurrency: NonZeroUsize,
) -> (
mpsc::UnboundedReceiver<JoinHandle<O>>,
JoinHandle<Result<(), SendOrBreakError<JoinHandle<O>, BreakError>>>,
)
where
IterGenerator::Item: Send,
IterGenerator::IntoIter: Send,
{
let semaphore = Arc::new(Semaphore::new(maximum_concurrency.get()));
let mut iter = task_generator.into_iter();
let (task_send, task_recv) = unbounded_channel();
let spawner_task = tokio::spawn(async move {
while let Ok(permit) = semaphore.clone().acquire_owned().await {
if let Some(next_gen_item) = iter.next() {
match resolver(next_gen_item) {
Ok(next_resolved_item) => {
let state = state.clone();
let task = task_ctor(state, next_resolved_item);
let running_handle = tokio::spawn(async move {
let task_result = task.await;
drop(permit);
task_result
});
task_send
.send(running_handle)
.map_err(SendOrBreakError::SendErr)?;
}
Err(ControlFlow::Break(b)) => return Err(SendOrBreakError::BreakErr(b)),
Err(ControlFlow::Continue(_)) => continue,
}
} else {
break;
}
}
Ok(())
});
(task_recv, spawner_task)
}
pub async fn restricted_parallel_streaming_async<
O: Send + 'static,
BreakError: Send + 'static,
State: Send + Clone + 'static,
Item: Send + 'static,
ResolvedItem: Send + 'static,
GeneratedTask: Future<Output = O> + Send + 'static,
>(
state: State,
mut istream: mpsc::UnboundedReceiver<Item>,
mut resolver: impl 'static + Send + FnMut(Item) -> Result<ResolvedItem, ControlFlow<BreakError, ()>>,
mut task_ctor: impl 'static + Send + FnMut(State, ResolvedItem) -> GeneratedTask,
maximum_concurrency: NonZeroUsize,
) -> (
mpsc::UnboundedReceiver<JoinHandle<O>>,
JoinHandle<Result<(), SendOrBreakError<JoinHandle<O>, BreakError>>>,
) {
let semaphore = Arc::new(Semaphore::new(maximum_concurrency.get()));
let (task_send, task_recv) = unbounded_channel();
let spawner_task = tokio::spawn(async move {
while let Ok(permit) = semaphore.clone().acquire_owned().await {
if let Some(next_gen_item) = istream.recv().await {
match resolver(next_gen_item) {
Ok(next_gen_item) => {
let state = state.clone();
let task = task_ctor(state, next_gen_item);
let running_handle = tokio::spawn(async move {
let task_result = task.await;
drop(permit);
task_result
});
task_send
.send(running_handle)
.map_err(SendOrBreakError::SendErr)?;
}
Err(ControlFlow::Break(b)) => return Err(SendOrBreakError::BreakErr(b)),
Err(ControlFlow::Continue(_)) => continue,
}
} else {
break;
}
}
Ok(())
});
(task_recv, spawner_task)
}
pub fn wrap_err_as_control_flow<T, E>(v: Result<T, E>, skip: bool) -> Result<T, ControlFlow<E>> {
v.map_err(|e| {
if skip {
ControlFlow::Continue(())
} else {
ControlFlow::Break(e)
}
})
}
#[derive(Debug)]
pub enum ImpossibleErr {}
impl Display for ImpossibleErr {
fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
unreachable!("Uninhabited type");
}
}
impl std::error::Error for ImpossibleErr {}