use std::{
fmt,
fs::File,
io::{Cursor, Read},
path::PathBuf,
sync::Arc,
};
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum IncludeSourceTarget {
File(PathBuf),
Uri(String),
}
pub struct IncludeSource {
reader: Box<dyn Read>,
#[cfg(feature = "network")]
pub(crate) read_limit: Option<usize>,
}
impl IncludeSource {
#[must_use]
pub fn from_reader(reader: impl Read + 'static) -> Self {
Self {
reader: Box::new(reader),
#[cfg(feature = "network")]
read_limit: None,
}
}
#[must_use]
pub fn from_bytes(bytes: impl Into<Vec<u8>>) -> Self {
Self::from_reader(Cursor::new(bytes.into()))
}
#[must_use]
pub fn from_string(source: impl Into<String>) -> Self {
Self::from_bytes(source.into().into_bytes())
}
pub(crate) fn into_reader(self) -> Box<dyn Read> {
self.reader
}
}
impl fmt::Debug for IncludeSource {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("IncludeSource")
.finish_non_exhaustive()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum IncludeSourceErrorKind {
NotFound,
Unavailable,
Unsupported,
Fatal,
}
#[derive(Debug, thiserror::Error)]
#[error("{message}")]
pub struct IncludeSourceError {
kind: IncludeSourceErrorKind,
message: String,
}
impl IncludeSourceError {
#[must_use]
pub fn new(kind: IncludeSourceErrorKind, message: impl Into<String>) -> Self {
Self {
kind,
message: message.into(),
}
}
#[must_use]
pub const fn kind(&self) -> IncludeSourceErrorKind {
self.kind
}
}
pub trait IncludeSourceProvider {
fn open(&self, target: &IncludeSourceTarget) -> Result<IncludeSource, IncludeSourceError>;
}
impl<F> IncludeSourceProvider for F
where
F: Fn(&IncludeSourceTarget) -> Result<IncludeSource, IncludeSourceError>,
{
fn open(&self, target: &IncludeSourceTarget) -> Result<IncludeSource, IncludeSourceError> {
self(target)
}
}
#[derive(Clone, Default)]
#[non_exhaustive]
pub enum IncludeLoader {
Disabled,
#[default]
System,
Custom(Arc<dyn IncludeSourceProvider + Send + Sync>),
}
impl IncludeLoader {
#[must_use]
pub fn custom(provider: impl IncludeSourceProvider + Send + Sync + 'static) -> Self {
Self::Custom(Arc::new(provider))
}
pub(crate) fn provider(&self) -> Option<&(dyn IncludeSourceProvider + '_)> {
static SYSTEM: SystemIncludeSourceProvider = SystemIncludeSourceProvider;
match self {
Self::Disabled => None,
Self::System => Some(&SYSTEM),
Self::Custom(provider) => Some(&**provider),
}
}
}
impl fmt::Debug for IncludeLoader {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Disabled => formatter.write_str("Disabled"),
Self::System => formatter.write_str("System"),
Self::Custom(_) => formatter.debug_tuple("Custom").finish_non_exhaustive(),
}
}
}
#[derive(Clone, Copy, Debug, Default)]
struct SystemIncludeSourceProvider;
impl IncludeSourceProvider for SystemIncludeSourceProvider {
fn open(&self, target: &IncludeSourceTarget) -> Result<IncludeSource, IncludeSourceError> {
match target {
IncludeSourceTarget::File(path) => {
if !path.is_file() {
return Err(IncludeSourceError::new(
IncludeSourceErrorKind::NotFound,
format!("include file not found: {}", path.display()),
));
}
File::open(path)
.map(IncludeSource::from_reader)
.map_err(|error| {
let kind = if error.kind() == std::io::ErrorKind::NotFound {
IncludeSourceErrorKind::NotFound
} else {
IncludeSourceErrorKind::Unavailable
};
IncludeSourceError::new(kind, error.to_string())
})
}
IncludeSourceTarget::Uri(uri) => {
#[cfg(all(
feature = "network",
not(all(target_family = "wasm", target_os = "unknown"))
))]
{
let response = ureq::get(uri).call().map_err(|error| {
IncludeSourceError::new(
IncludeSourceErrorKind::Unavailable,
error.to_string(),
)
})?;
let (_, body) = response.into_parts();
let mut source = IncludeSource::from_reader(body.into_reader());
source.read_limit = Some(10 * 1024 * 1024);
Ok(source)
}
#[cfg(any(
not(feature = "network"),
all(target_family = "wasm", target_os = "unknown")
))]
{
let _ = uri;
let message = if cfg!(all(target_family = "wasm", target_os = "unknown")) {
"built-in HTTP(S) includes are unavailable on this WebAssembly target. Fetch content first and supply it through a custom include provider"
} else {
"network support is disabled"
};
Err(IncludeSourceError::new(
IncludeSourceErrorKind::Unsupported,
message,
))
}
}
}
}
}