mod cache;
mod context;
mod filesystem;
mod web;
pub use cache::{Cache, CacheEntry};
pub use context::{BasicContext, Context};
pub use filesystem::{check_filesystem, resolve_link, Options};
#[allow(deprecated)]
pub use web::get;
pub use web::{check_web, head};
use crate::{Category, Link};
use futures::{Future, StreamExt};
use std::path::Path;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Reason {
#[error("Linking outside of the \"root\" directory is forbidden")]
TraversesParentDirectories,
#[error("An OS-level error occurred")]
Io(#[from] std::io::Error),
#[error("The web client encountered an error")]
Web(#[from] reqwest::Error),
}
impl Reason {
pub fn file_not_found(&self) -> bool {
match self {
Reason::Io(e) => e.kind() == std::io::ErrorKind::NotFound,
_ => false,
}
}
pub fn timed_out(&self) -> bool {
match self {
Reason::Web(e) => e.is_timeout(),
_ => false,
}
}
}
pub fn validate<'a, L, C>(
current_directory: &'a Path,
links: L,
ctx: &'a C,
) -> impl Future<Output = Outcomes> + 'a
where
L: IntoIterator<Item = Link>,
L::IntoIter: 'a,
C: Context + ?Sized,
{
futures::stream::iter(links)
.map(move |link| validate_one(link, current_directory, ctx))
.buffer_unordered(ctx.concurrency())
.collect()
}
async fn validate_one<C>(
link: Link,
current_directory: &Path,
ctx: &C,
) -> Outcome
where
C: Context + ?Sized,
{
if ctx.should_ignore(&link) {
log::debug!("Ignoring \"{}\"", link.href);
return Outcome::Ignored(link);
}
match link.category() {
Some(Category::FileSystem { path, fragment }) => Outcome::from_result(
link,
check_filesystem(
current_directory,
&path,
fragment.as_deref(),
ctx,
),
),
Some(Category::CurrentFile { fragment }) => {
log::warn!("Not checking \"{}\" in the current file because fragment resolution isn't implemented", fragment);
Outcome::Ignored(link)
},
Some(Category::Url(url)) => {
Outcome::from_result(link, check_web(&url, ctx).await)
},
Some(Category::MailTo(_)) => Outcome::Ignored(link),
None => Outcome::UnknownCategory(link),
}
}
#[derive(Debug, Default)]
pub struct Outcomes {
pub valid: Vec<Link>,
pub invalid: Vec<InvalidLink>,
pub ignored: Vec<Link>,
pub unknown_category: Vec<Link>,
}
impl Outcomes {
pub fn empty() -> Self { Outcomes::default() }
pub fn merge(&mut self, other: Outcomes) {
self.valid.extend(other.valid);
self.invalid.extend(other.invalid);
self.ignored.extend(other.ignored);
self.unknown_category.extend(other.unknown_category);
}
}
impl Extend<Outcome> for Outcomes {
fn extend<T: IntoIterator<Item = Outcome>>(&mut self, items: T) {
for outcome in items {
match outcome {
Outcome::Valid(v) => self.valid.push(v),
Outcome::Invalid(i) => self.invalid.push(i),
Outcome::Ignored(i) => self.ignored.push(i),
Outcome::UnknownCategory(u) => self.unknown_category.push(u),
}
}
}
}
impl Extend<Outcomes> for Outcomes {
fn extend<T: IntoIterator<Item = Outcomes>>(&mut self, items: T) {
for item in items {
self.merge(item);
}
}
}
#[derive(Debug)]
pub struct InvalidLink {
pub link: Link,
pub reason: Reason,
}
#[derive(Debug)]
enum Outcome {
Valid(Link),
Invalid(InvalidLink),
Ignored(Link),
UnknownCategory(Link),
}
impl Outcome {
fn from_result<T, E>(link: Link, result: Result<T, E>) -> Self
where
E: Into<Reason>,
{
match result {
Ok(_) => Outcome::Valid(link),
Err(e) => Outcome::Invalid(InvalidLink {
link,
reason: e.into(),
}),
}
}
}