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; } #[derive(Debug, Clone, PartialEq, Eq)] pub struct ResolvedEndorSource { pub revision: String, pub bytes: Vec, } /// 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, text: impl Into) -> SourceUnit { SourceUnit::new(SourceOrigin::Text { name: name.into() }, text.into()) } pub fn from_stream( name: impl Into, reader: impl Read, ) -> Result { Self::read(SourceOrigin::Stream { name: name.into() }, reader) } pub fn from_file(path: impl AsRef) -> Result { 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( uri: &str, provider: &P, ) -> Result { // 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 { 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) -> Result { 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, } } }