Add Endor source reader before lexing with provenance and URI provider
Verify databases and deploy Aster VM / verify-and-deploy (push) Successful in 6m53s
Verify databases and deploy Aster VM / verify-and-deploy (push) Successful in 6m53s
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user