148 lines
4.9 KiB
Rust
148 lines
4.9 KiB
Rust
use std::error::Error;
|
|
use std::fmt;
|
|
use std::fs::File;
|
|
use std::io::{self, Read};
|
|
use std::path::Path;
|
|
use std::string::FromUtf8Error;
|
|
|
|
use crate::{SourceOrigin, SourceUnit};
|
|
|
|
/// The host resolves names, authorizes access and selects a revision before
|
|
/// returning its source bytes. No URI path layout or hash algorithm is assumed.
|
|
pub trait EndorSourceProvider {
|
|
fn load(&self, uri: &str) -> io::Result<ResolvedEndorSource>;
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct ResolvedEndorSource {
|
|
pub revision: String,
|
|
pub bytes: Vec<u8>,
|
|
}
|
|
|
|
/// Stateless source adapters, before lexing. Streams are read to EOF; session
|
|
/// prompting and incomplete REPL input detection belong to the interactive host.
|
|
#[derive(Debug, Clone, Copy, Default)]
|
|
pub struct EndorReader;
|
|
|
|
impl EndorReader {
|
|
pub fn from_text(name: impl Into<String>, text: impl Into<String>) -> SourceUnit {
|
|
SourceUnit::new(SourceOrigin::Text { name: name.into() }, text.into())
|
|
}
|
|
|
|
pub fn from_stream(
|
|
name: impl Into<String>,
|
|
reader: impl Read,
|
|
) -> Result<SourceUnit, SourceReadError> {
|
|
Self::read(SourceOrigin::Stream { name: name.into() }, reader)
|
|
}
|
|
|
|
pub fn from_file(path: impl AsRef<Path>) -> Result<SourceUnit, SourceReadError> {
|
|
let path = path.as_ref();
|
|
let origin = SourceOrigin::File {
|
|
path: path.to_owned(),
|
|
};
|
|
let file = File::open(path).map_err(|error| SourceReadError {
|
|
origin: origin.to_string(),
|
|
kind: SourceReadErrorKind::Io(error),
|
|
})?;
|
|
Self::read(origin, file)
|
|
}
|
|
|
|
pub fn from_endor<P: EndorSourceProvider + ?Sized>(
|
|
uri: &str,
|
|
provider: &P,
|
|
) -> Result<SourceUnit, SourceReadError> {
|
|
// Only recognize the scheme and reject empty/control-filled locators.
|
|
// Namespace syntax, revision selectors and percent decoding are host policy.
|
|
let valid = uri.split_once(':').is_some_and(|(scheme, address)| {
|
|
scheme.eq_ignore_ascii_case("endor") && !address.is_empty()
|
|
}) && !uri
|
|
.chars()
|
|
.any(|character| character.is_whitespace() || character.is_control());
|
|
if !valid {
|
|
return Err(SourceReadError {
|
|
origin: uri.into(),
|
|
kind: SourceReadErrorKind::InvalidEndorUri,
|
|
});
|
|
}
|
|
let resolved = provider.load(uri).map_err(|error| SourceReadError {
|
|
origin: uri.into(),
|
|
kind: SourceReadErrorKind::Io(error),
|
|
})?;
|
|
if resolved.revision.trim().is_empty() {
|
|
return Err(SourceReadError {
|
|
origin: uri.into(),
|
|
kind: SourceReadErrorKind::MissingRevision,
|
|
});
|
|
}
|
|
let origin = SourceOrigin::Endor {
|
|
uri: uri.into(),
|
|
revision: resolved.revision,
|
|
};
|
|
Self::decode(origin, resolved.bytes)
|
|
}
|
|
|
|
fn read(origin: SourceOrigin, mut reader: impl Read) -> Result<SourceUnit, SourceReadError> {
|
|
let mut bytes = Vec::new();
|
|
reader
|
|
.read_to_end(&mut bytes)
|
|
.map_err(|error| SourceReadError {
|
|
origin: origin.to_string(),
|
|
kind: SourceReadErrorKind::Io(error),
|
|
})?;
|
|
Self::decode(origin, bytes)
|
|
}
|
|
|
|
fn decode(origin: SourceOrigin, bytes: Vec<u8>) -> Result<SourceUnit, SourceReadError> {
|
|
let text = String::from_utf8(bytes).map_err(|error| SourceReadError {
|
|
origin: origin.to_string(),
|
|
kind: SourceReadErrorKind::InvalidUtf8(error),
|
|
})?;
|
|
Ok(SourceUnit::new(origin, text))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum SourceReadErrorKind {
|
|
Io(io::Error),
|
|
InvalidUtf8(FromUtf8Error),
|
|
InvalidEndorUri,
|
|
MissingRevision,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct SourceReadError {
|
|
pub origin: String,
|
|
pub kind: SourceReadErrorKind,
|
|
}
|
|
|
|
impl fmt::Display for SourceReadError {
|
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(formatter, "{}: ", self.origin)?;
|
|
match &self.kind {
|
|
SourceReadErrorKind::Io(error) => write!(formatter, "{error}"),
|
|
SourceReadErrorKind::InvalidUtf8(error) => write!(
|
|
formatter,
|
|
"invalid UTF-8 at byte {}",
|
|
error.utf8_error().valid_up_to()
|
|
),
|
|
SourceReadErrorKind::InvalidEndorUri => formatter.write_str(
|
|
"expected a nonempty endor URI without whitespace or control characters",
|
|
),
|
|
SourceReadErrorKind::MissingRevision => {
|
|
formatter.write_str("source provider returned no resolved revision")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Error for SourceReadError {
|
|
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
|
match &self.kind {
|
|
SourceReadErrorKind::Io(error) => Some(error),
|
|
SourceReadErrorKind::InvalidUtf8(error) => Some(error),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|