Add Endor source reader before lexing with provenance and URI provider
Verify databases and deploy Aster VM / verify-and-deploy (push) Successful in 6m53s

This commit is contained in:
2026-10-19 03:23:38 +08:00
parent 4bf889e5b7
commit 483d9d1717
27 changed files with 667 additions and 37 deletions
+147
View File
@@ -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,
}
}
}