#![allow(clippy::result_large_err)]
use crate::lex::assembling::AttachAnnotations;
use crate::lex::ast::elements::container::GeneralContainer;
use crate::lex::ast::elements::content_item::ContentItem;
use crate::lex::ast::elements::session::Session;
use crate::lex::ast::range::Range;
use crate::lex::ast::Document;
use crate::lex::transforms::Runnable;
use lex_extension::handler::HandlerError;
use lex_extension_host::registry::Registry;
use std::path::{Path, PathBuf};
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct ResolveConfig {
pub root: PathBuf,
pub max_depth: usize,
pub max_total_includes: usize,
}
impl ResolveConfig {
pub const DEFAULT_MAX_DEPTH: usize = 8;
pub const DEFAULT_MAX_TOTAL_INCLUDES: usize = 1000;
pub fn with_root(root: PathBuf) -> Self {
Self {
root,
max_depth: Self::DEFAULT_MAX_DEPTH,
max_total_includes: Self::DEFAULT_MAX_TOTAL_INCLUDES,
}
}
}
pub trait Loader {
fn load(&self, path: &Path) -> Result<LoadedFile, LoadError>;
}
#[derive(Debug, Clone)]
pub struct LoadedFile {
pub source: String,
pub canonical_path: PathBuf,
}
#[derive(Debug, Clone)]
pub enum LoadError {
NotFound { path: PathBuf },
OutsideRoot { path: PathBuf, root: PathBuf },
TooLarge {
path: PathBuf,
size: u64,
limit: u64,
},
Io { path: PathBuf, message: String },
}
impl std::fmt::Display for LoadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LoadError::NotFound { path } => write!(f, "include not found: {}", path.display()),
LoadError::OutsideRoot { path, root } => write!(
f,
"include path {} resolves outside loader root {}",
path.display(),
root.display()
),
LoadError::TooLarge { path, size, limit } => write!(
f,
"include file {} is {size} bytes, exceeds limit of {limit} bytes",
path.display()
),
LoadError::Io { path, message } => {
write!(f, "io error reading {}: {message}", path.display())
}
}
}
}
impl std::error::Error for LoadError {}
#[derive(Debug, Clone)]
pub enum IncludeError {
Cycle {
include_site: Range,
path: PathBuf,
chain: Vec<PathBuf>,
},
DepthExceeded {
include_site: Range,
limit: usize,
chain: Vec<PathBuf>,
},
TotalIncludesExceeded { include_site: Range, limit: usize },
FileTooLarge {
include_site: Range,
path: PathBuf,
size: u64,
limit: u64,
},
RootEscape { path: PathBuf, root: PathBuf },
AbsolutePath { path: PathBuf },
NotFound { include_site: Range, path: PathBuf },
ParseFailed { path: PathBuf, message: String },
ContainerPolicy {
include_site: Range,
container: &'static str,
file: PathBuf,
violation: &'static str,
},
LoaderIo { path: PathBuf, message: String },
MissingSrc { include_site: Range },
HandlerFailed {
include_site: Range,
label: String,
code: String,
message: String,
},
}
impl std::fmt::Display for IncludeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IncludeError::Cycle { path, chain, .. } => {
let chain_display: Vec<String> =
chain.iter().map(|p| p.display().to_string()).collect();
write!(
f,
"include cycle: {} (chain: {})",
path.display(),
chain_display.join(" -> ")
)
}
IncludeError::DepthExceeded { limit, chain, .. } => {
let chain_display: Vec<String> =
chain.iter().map(|p| p.display().to_string()).collect();
write!(
f,
"include depth exceeded limit of {limit} (chain: {})",
chain_display.join(" -> ")
)
}
IncludeError::TotalIncludesExceeded { limit, .. } => {
write!(f, "total include count exceeded limit of {limit}")
}
IncludeError::FileTooLarge {
path, size, limit, ..
} => {
write!(
f,
"included file {} is {size} bytes, exceeds limit of {limit} bytes",
path.display()
)
}
IncludeError::RootEscape { path, root } => write!(
f,
"include path {} escapes resolution root {}",
path.display(),
root.display()
),
IncludeError::AbsolutePath { path } => write!(
f,
"include src {} is a platform-absolute path; \
the spec forbids absolute filesystem paths — use a relative path \
(chapters/01.lex) or a root-absolute path (/shared/01.lex)",
path.display()
),
IncludeError::NotFound { path, .. } => {
write!(f, "include not found: {}", path.display())
}
IncludeError::ParseFailed { path, message } => {
write!(f, "failed to parse {}: {message}", path.display())
}
IncludeError::ContainerPolicy {
container,
file,
violation,
..
} => write!(
f,
"included file {} contains {} but include site is inside {} \
(which does not allow {})",
file.display(),
violation,
container,
violation
),
IncludeError::LoaderIo { path, message } => {
write!(f, "loader error reading {}: {message}", path.display())
}
IncludeError::MissingSrc { .. } => {
write!(f, "lex.include annotation missing required src= parameter")
}
IncludeError::HandlerFailed {
label,
code,
message,
..
} => write!(f, "extension handler `{label}` failed ({code}): {message}"),
}
}
}
impl std::error::Error for IncludeError {}
#[derive(Debug, Clone, Copy)]
enum ContainerKind {
Session,
Definition,
AnnotationBody,
ListItem,
}
impl ContainerKind {
fn name(self) -> &'static str {
match self {
ContainerKind::Session => "Session",
ContainerKind::Definition => "Definition",
ContainerKind::AnnotationBody => "Annotation body",
ContainerKind::ListItem => "ListItem",
}
}
fn allows_sessions(self) -> bool {
matches!(self, ContainerKind::Session)
}
}
pub const KERNEL_DEPTH_BACKSTOP: usize = 32;
pub fn resolve_from_source(
source: &str,
source_path: Option<PathBuf>,
config: &ResolveConfig,
registry: &Registry,
) -> Result<Document, IncludeError> {
let entry_origin = source_path.as_ref().map(|p| Arc::new(p.clone()));
let mut doc = parse_no_attach(source).map_err(|message| IncludeError::ParseFailed {
path: source_path.clone().unwrap_or_default(),
message,
})?;
if let Some(origin) = entry_origin.as_ref() {
stamp_doc(&mut doc, origin);
}
let mut chain: Vec<ResolveKey> = Vec::new();
let mut state = ResolverState {
config,
registry,
chain: &mut chain,
depth: 0,
total_resolved: 0,
};
splice_in_session_container(doc.root.children.as_mut_vec(), &mut state)?;
let doc = AttachAnnotations::new()
.run(doc)
.map_err(|e| IncludeError::ParseFailed {
path: source_path.unwrap_or_default(),
message: format!("annotation attachment failed: {e}"),
})?;
Ok(doc)
}
#[derive(Debug, Clone, PartialEq)]
struct ResolveKey {
label: String,
origin: Option<PathBuf>,
start: crate::lex::ast::range::Position,
}
impl ResolveKey {
fn from_annotation(a: &crate::lex::ast::elements::annotation::Annotation) -> Self {
Self {
label: a.data.label.value.clone(),
origin: a.location.origin_path.as_ref().map(|p| (**p).clone()),
start: a.location.start,
}
}
}
struct ResolverState<'a> {
config: &'a ResolveConfig,
registry: &'a Registry,
chain: &'a mut Vec<ResolveKey>,
depth: usize,
total_resolved: usize,
}
fn splice_in_session_container(
children: &mut Vec<ContentItem>,
state: &mut ResolverState<'_>,
) -> Result<(), IncludeError> {
recurse_into_children(children, state)?;
process_resolves(children, state, ContainerKind::Session)
}
fn splice_in_general_container(
container: &mut GeneralContainer,
state: &mut ResolverState<'_>,
kind: ContainerKind,
) -> Result<(), IncludeError> {
recurse_into_children(container.as_mut_vec(), state)?;
process_resolves(container.as_mut_vec(), state, kind)
}
#[allow(clippy::ptr_arg)]
fn process_resolves(
children: &mut Vec<ContentItem>,
state: &mut ResolverState<'_>,
kind: ContainerKind,
) -> Result<(), IncludeError> {
let resolve_indices: Vec<usize> = children
.iter()
.enumerate()
.filter_map(|(i, item)| match item {
ContentItem::Annotation(a) => {
let label = &a.data.label.value;
if state
.registry
.schema_for(label)
.map(|s| s.hooks.resolve)
.unwrap_or(false)
{
Some(i)
} else {
None
}
}
_ => None,
})
.collect();
for i in resolve_indices.into_iter().rev() {
let annotation = match &children[i] {
ContentItem::Annotation(a) => a.clone(),
_ => unreachable!("index came from resolve filter"),
};
match resolve_one_invocation(&annotation, state, kind)? {
ResolveOutcome::Spliced(splice_items) => {
let mut replacement = Vec::with_capacity(splice_items.len() + 1);
replacement.push(ContentItem::Annotation(annotation));
replacement.extend(splice_items);
children.splice(i..=i, replacement);
}
ResolveOutcome::Unexpanded => {
let mut owned = annotation;
splice_in_general_container(
&mut owned.children,
state,
ContainerKind::AnnotationBody,
)?;
children[i] = ContentItem::Annotation(owned);
}
}
}
Ok(())
}
enum ResolveOutcome {
Spliced(Vec<ContentItem>),
Unexpanded,
}
fn resolve_one_invocation(
annotation: &crate::lex::ast::elements::annotation::Annotation,
state: &mut ResolverState<'_>,
parent_kind: ContainerKind,
) -> Result<ResolveOutcome, IncludeError> {
let label = &annotation.data.label.value;
let key = ResolveKey::from_annotation(annotation);
if state.chain.contains(&key) {
return Err(IncludeError::Cycle {
include_site: annotation.location.clone(),
path: key.origin.clone().unwrap_or_default(),
chain: state
.chain
.iter()
.map(|k| k.origin.clone().unwrap_or_default())
.collect(),
});
}
let effective_depth_limit = state.config.max_depth.min(KERNEL_DEPTH_BACKSTOP);
if state.depth >= effective_depth_limit {
return Err(IncludeError::DepthExceeded {
include_site: annotation.location.clone(),
limit: effective_depth_limit,
chain: state
.chain
.iter()
.map(|k| k.origin.clone().unwrap_or_default())
.collect(),
});
}
if state.total_resolved >= state.config.max_total_includes {
return Err(IncludeError::TotalIncludesExceeded {
include_site: annotation.location.clone(),
limit: state.config.max_total_includes,
});
}
let ctx = build_label_ctx(annotation);
let wire_node = match state.registry.dispatch_resolve_raw(&ctx) {
Ok(Some(node)) => node,
Ok(None) => {
return Ok(ResolveOutcome::Unexpanded);
}
Err(handler_err) => {
return Err(handler_error_to_include_error(
&handler_err,
label,
&annotation.location,
));
}
};
state.total_resolved += 1;
let mut splice_items = decode_wire_to_items(&wire_node, label, &annotation.location)?;
let included_path = wire_node_origin_pathbuf(&wire_node)
.or_else(|| splice_items_first_origin(&splice_items))
.unwrap_or_default();
state.chain.push(key);
let saved_depth = state.depth;
state.depth = saved_depth + 1;
let recurse_result = splice_in_session_container(&mut splice_items, state);
state.depth = saved_depth;
state.chain.pop();
recurse_result?;
validate_against_kind(
&splice_items,
parent_kind,
&annotation.location,
&included_path,
)?;
Ok(ResolveOutcome::Spliced(splice_items))
}
fn build_label_ctx(
a: &crate::lex::ast::elements::annotation::Annotation,
) -> lex_extension::wire::LabelCtx {
use crate::lex::wire::to_wire_node;
use lex_extension::wire::{AnnotationBody, LabelCtx, NodeRef};
let label = a.data.label.value.clone();
let params = {
let mut obj = serde_json::Map::with_capacity(a.data.parameters.len());
for p in &a.data.parameters {
obj.insert(p.key.clone(), serde_json::Value::String(p.unquoted_value()));
}
serde_json::Value::Object(obj)
};
let body = if a.children.is_empty() {
AnnotationBody::None
} else {
let wire_children: Vec<lex_extension::wire::WireNode> =
a.children.iter().map(to_wire_node).collect();
AnnotationBody::Lex {
children: wire_children,
}
};
let range = lex_extension::wire::Range::new(
lex_extension::wire::Position::new(
u32::try_from(a.location.start.line).unwrap_or(u32::MAX),
u32::try_from(a.location.start.column).unwrap_or(u32::MAX),
),
lex_extension::wire::Position::new(
u32::try_from(a.location.end.line).unwrap_or(u32::MAX),
u32::try_from(a.location.end.column).unwrap_or(u32::MAX),
),
);
let origin = a
.location
.origin_path
.as_ref()
.map(|p| p.to_string_lossy().into_owned());
LabelCtx {
label,
params,
body,
node: NodeRef {
kind: "annotation".into(),
range,
origin,
},
}
}
fn wire_node_origin_pathbuf(node: &lex_extension::wire::WireNode) -> Option<PathBuf> {
use lex_extension::wire::WireNode as W;
let s = match node {
W::Document { origin, .. } => origin.as_deref(),
W::Session { origin, .. } => origin.as_deref(),
W::Definition { origin, .. } => origin.as_deref(),
W::Paragraph { origin, .. } => origin.as_deref(),
W::List { origin, .. } => origin.as_deref(),
W::Verbatim { origin, .. } => origin.as_deref(),
W::Table { origin, .. } => origin.as_deref(),
W::Annotation { origin, .. } => origin.as_deref(),
W::Blank { origin, .. } => origin.as_deref(),
_ => None,
};
s.map(PathBuf::from)
}
fn splice_items_first_origin(items: &[ContentItem]) -> Option<PathBuf> {
for item in items {
let r = match item {
ContentItem::Paragraph(p) => &p.location,
ContentItem::Session(s) => &s.location,
ContentItem::Definition(d) => &d.location,
ContentItem::List(l) => &l.location,
ContentItem::ListItem(li) => &li.location,
ContentItem::Annotation(a) => &a.location,
ContentItem::VerbatimBlock(v) => &v.location,
ContentItem::VerbatimLine(vl) => &vl.location,
ContentItem::Table(t) => &t.location,
ContentItem::TextLine(tl) => &tl.location,
ContentItem::BlankLineGroup(blg) => &blg.location,
};
if let Some(arc) = r.origin_path.as_ref() {
return Some((**arc).clone());
}
}
None
}
fn decode_wire_to_items(
wire: &lex_extension::wire::WireNode,
invocation_label: &str,
include_site: &Range,
) -> Result<Vec<ContentItem>, IncludeError> {
use crate::lex::wire::from_wire_node;
from_wire_node(wire).map_err(|e| IncludeError::HandlerFailed {
include_site: include_site.clone(),
label: invocation_label.to_string(),
code: "wire.decode".into(),
message: format!("decoding handler-returned wire payload failed: {e}"),
})
}
fn handler_error_to_include_error(
err: &HandlerError,
label: &str,
include_site: &Range,
) -> IncludeError {
use crate::lex::builtins::include::{
CODE_ABSOLUTE_PATH, CODE_IO, CODE_MISSING_SRC, CODE_NOT_FOUND, CODE_OUTSIDE_ROOT,
CODE_PARSE_FAILED, CODE_TOO_LARGE,
};
match err {
HandlerError::Custom {
code,
message,
data,
} => match *code {
CODE_NOT_FOUND => IncludeError::NotFound {
include_site: include_site.clone(),
path: data_str(data, "path")
.map(PathBuf::from)
.unwrap_or_default(),
},
CODE_OUTSIDE_ROOT => IncludeError::RootEscape {
path: data_str(data, "path")
.map(PathBuf::from)
.unwrap_or_default(),
root: data_str(data, "root")
.map(PathBuf::from)
.unwrap_or_default(),
},
CODE_TOO_LARGE => IncludeError::FileTooLarge {
include_site: include_site.clone(),
path: data_str(data, "path")
.map(PathBuf::from)
.unwrap_or_default(),
size: data_u64(data, "size").unwrap_or(0),
limit: data_u64(data, "limit").unwrap_or(0),
},
CODE_ABSOLUTE_PATH => IncludeError::AbsolutePath {
path: data_str(data, "path")
.map(PathBuf::from)
.unwrap_or_default(),
},
CODE_IO => IncludeError::LoaderIo {
path: data_str(data, "path")
.map(PathBuf::from)
.unwrap_or_default(),
message: message.clone(),
},
CODE_MISSING_SRC => IncludeError::MissingSrc {
include_site: include_site.clone(),
},
CODE_PARSE_FAILED => IncludeError::ParseFailed {
path: data_str(data, "path")
.map(PathBuf::from)
.unwrap_or_default(),
message: data_str(data, "message").unwrap_or_else(|| message.clone()),
},
other => IncludeError::HandlerFailed {
include_site: include_site.clone(),
label: label.to_string(),
code: format!("handler.custom({other})"),
message: message.clone(),
},
},
HandlerError::Internal { message } => IncludeError::HandlerFailed {
include_site: include_site.clone(),
label: label.to_string(),
code: "handler.internal".into(),
message: message.clone(),
},
HandlerError::Unsupported { detail } => IncludeError::HandlerFailed {
include_site: include_site.clone(),
label: label.to_string(),
code: "handler.unsupported".into(),
message: detail.clone(),
},
}
}
fn data_str(data: &Option<serde_json::Value>, key: &str) -> Option<String> {
data.as_ref()?.get(key)?.as_str().map(str::to_string)
}
fn data_u64(data: &Option<serde_json::Value>, key: &str) -> Option<u64> {
data.as_ref()?.get(key)?.as_u64()
}
#[allow(clippy::ptr_arg)]
fn recurse_into_children(
children: &mut Vec<ContentItem>,
state: &mut ResolverState<'_>,
) -> Result<(), IncludeError> {
for item in children.iter_mut() {
match item {
ContentItem::Session(s) => {
splice_in_session_container(s.children.as_mut_vec(), state)?;
}
ContentItem::Definition(d) => {
splice_in_general_container(&mut d.children, state, ContainerKind::Definition)?;
}
ContentItem::Annotation(a) => {
let is_resolve_hooked = state
.registry
.schema_for(&a.data.label.value)
.map(|s| s.hooks.resolve)
.unwrap_or(false);
if !is_resolve_hooked {
splice_in_general_container(
&mut a.children,
state,
ContainerKind::AnnotationBody,
)?;
}
}
ContentItem::List(l) => {
for li in l.items.as_mut_vec().iter_mut() {
if let ContentItem::ListItem(item) = li {
splice_in_general_container(
&mut item.children,
state,
ContainerKind::ListItem,
)?;
}
}
}
_ => {}
}
}
Ok(())
}
fn validate_against_kind(
items: &[ContentItem],
kind: ContainerKind,
site: &Range,
file: &Path,
) -> Result<(), IncludeError> {
if kind.allows_sessions() {
return Ok(());
}
if items.iter().any(|i| matches!(i, ContentItem::Session(_))) {
return Err(IncludeError::ContainerPolicy {
include_site: site.clone(),
container: kind.name(),
file: file.to_path_buf(),
violation: "Sessions",
});
}
Ok(())
}
pub fn resolve_file_reference(
target: &str,
ref_origin: Option<&Path>,
root: &Path,
) -> Result<PathBuf, IncludeError> {
let host_dir: PathBuf = ref_origin
.and_then(|p| p.parent())
.map(Path::to_path_buf)
.unwrap_or_else(|| root.to_path_buf());
resolve_path(target, &host_dir, root)
}
fn resolve_path(src: &str, host_dir: &Path, root: &Path) -> Result<PathBuf, IncludeError> {
let candidate = if let Some(rel) = src.strip_prefix('/') {
root.join(rel)
} else {
if Path::new(src).is_absolute() {
return Err(IncludeError::AbsolutePath {
path: PathBuf::from(src),
});
}
host_dir.join(src)
};
let normalized = lexical_normalize(&candidate);
let canonical_root = lexical_normalize(root);
if !normalized.starts_with(&canonical_root) {
return Err(IncludeError::RootEscape {
path: normalized,
root: canonical_root,
});
}
Ok(normalized)
}
fn lexical_normalize(p: &Path) -> PathBuf {
let mut out = PathBuf::new();
for c in p.components() {
match c {
std::path::Component::ParentDir => {
let can_pop = matches!(
out.components().next_back(),
Some(std::path::Component::Normal(_))
);
if can_pop {
out.pop();
} else {
out.push("..");
}
}
std::path::Component::CurDir => {}
other => out.push(other.as_os_str()),
}
}
out
}
pub(crate) fn stamp_doc(doc: &mut Document, origin: &Arc<PathBuf>) {
if let Some(title) = doc.title.as_mut() {
title.location.origin_path = Some(Arc::clone(origin));
}
for ann in doc.annotations.iter_mut() {
stamp_annotation(ann, origin);
}
stamp_session(&mut doc.root, origin);
}
fn stamp_session(s: &mut Session, origin: &Arc<PathBuf>) {
s.location.origin_path = Some(Arc::clone(origin));
if let Some(loc) = s.title.location.as_mut() {
loc.origin_path = Some(Arc::clone(origin));
}
for ann in s.annotations.iter_mut() {
stamp_annotation(ann, origin);
}
for item in s.children.as_mut_vec().iter_mut() {
stamp_item(item, origin);
}
}
fn stamp_annotation(
a: &mut crate::lex::ast::elements::annotation::Annotation,
origin: &Arc<PathBuf>,
) {
a.location.origin_path = Some(Arc::clone(origin));
a.data.location.origin_path = Some(Arc::clone(origin));
for item in a.children.as_mut_vec().iter_mut() {
stamp_item(item, origin);
}
}
fn stamp_item(item: &mut ContentItem, origin: &Arc<PathBuf>) {
match item {
ContentItem::Session(s) => stamp_session(s, origin),
ContentItem::Annotation(a) => stamp_annotation(a, origin),
ContentItem::Paragraph(p) => {
p.location.origin_path = Some(Arc::clone(origin));
for ann in p.annotations.iter_mut() {
stamp_annotation(ann, origin);
}
for line in p.lines.iter_mut() {
stamp_item(line, origin);
}
}
ContentItem::List(l) => {
l.location.origin_path = Some(Arc::clone(origin));
for li in l.items.as_mut_vec().iter_mut() {
stamp_item(li, origin);
}
}
ContentItem::ListItem(li) => {
li.location.origin_path = Some(Arc::clone(origin));
for ann in li.annotations.iter_mut() {
stamp_annotation(ann, origin);
}
for child in li.children.as_mut_vec().iter_mut() {
stamp_item(child, origin);
}
}
ContentItem::Definition(d) => {
d.location.origin_path = Some(Arc::clone(origin));
for ann in d.annotations.iter_mut() {
stamp_annotation(ann, origin);
}
for child in d.children.as_mut_vec().iter_mut() {
stamp_item(child, origin);
}
}
ContentItem::VerbatimBlock(v) => {
v.location.origin_path = Some(Arc::clone(origin));
}
ContentItem::VerbatimLine(vl) => {
vl.location.origin_path = Some(Arc::clone(origin));
}
ContentItem::Table(t) => {
t.location.origin_path = Some(Arc::clone(origin));
}
ContentItem::TextLine(tl) => {
tl.location.origin_path = Some(Arc::clone(origin));
}
ContentItem::BlankLineGroup(b) => {
b.location.origin_path = Some(Arc::clone(origin));
}
}
}
pub(crate) fn parse_no_attach(source: &str) -> Result<Document, String> {
crate::lex::testing::parse_without_annotation_attachment(source)
}
pub struct FsLoader {
canonical_root: PathBuf,
max_file_size: u64,
}
impl FsLoader {
pub const DEFAULT_MAX_FILE_SIZE: u64 = 10 * 1024 * 1024;
pub fn new(root: PathBuf) -> Self {
let canonical_root = std::fs::canonicalize(&root).unwrap_or(root);
Self {
canonical_root,
max_file_size: Self::DEFAULT_MAX_FILE_SIZE,
}
}
pub fn with_max_file_size(mut self, max_file_size: u64) -> Self {
self.max_file_size = max_file_size;
self
}
}
impl Loader for FsLoader {
fn load(&self, path: &Path) -> Result<LoadedFile, LoadError> {
let canonical_path = std::fs::canonicalize(path).map_err(|e| match e.kind() {
std::io::ErrorKind::NotFound => LoadError::NotFound {
path: path.to_path_buf(),
},
_ => LoadError::Io {
path: path.to_path_buf(),
message: e.to_string(),
},
})?;
if !canonical_path.starts_with(&self.canonical_root) {
return Err(LoadError::OutsideRoot {
path: canonical_path,
root: self.canonical_root.clone(),
});
}
let meta = std::fs::metadata(&canonical_path).map_err(|e| LoadError::Io {
path: canonical_path.clone(),
message: e.to_string(),
})?;
if !meta.is_file() {
return Err(LoadError::Io {
path: canonical_path,
message: "include target is not a regular file".to_string(),
});
}
let size = meta.len();
if size > self.max_file_size {
return Err(LoadError::TooLarge {
path: canonical_path,
size,
limit: self.max_file_size,
});
}
let source = std::fs::read_to_string(&canonical_path).map_err(|e| LoadError::Io {
path: canonical_path.clone(),
message: e.to_string(),
})?;
Ok(LoadedFile {
source,
canonical_path,
})
}
}
#[cfg(any(test, feature = "test-support"))]
pub struct MemoryLoader {
files: std::collections::HashMap<PathBuf, String>,
}
#[cfg(any(test, feature = "test-support"))]
impl MemoryLoader {
pub fn new() -> Self {
Self {
files: std::collections::HashMap::new(),
}
}
pub fn insert<P: Into<PathBuf>, S: Into<String>>(&mut self, path: P, contents: S) -> &mut Self {
self.files.insert(path.into(), contents.into());
self
}
pub fn from_pairs<I, P, S>(pairs: I) -> Self
where
I: IntoIterator<Item = (P, S)>,
P: Into<PathBuf>,
S: Into<String>,
{
let mut loader = Self::new();
for (path, contents) in pairs {
loader.insert(path, contents);
}
loader
}
}
#[cfg(any(test, feature = "test-support"))]
impl Default for MemoryLoader {
fn default() -> Self {
Self::new()
}
}
#[cfg(any(test, feature = "test-support"))]
impl Loader for MemoryLoader {
fn load(&self, path: &Path) -> Result<LoadedFile, LoadError> {
let source = self
.files
.get(path)
.cloned()
.ok_or_else(|| LoadError::NotFound {
path: path.to_path_buf(),
})?;
Ok(LoadedFile {
source,
canonical_path: path.to_path_buf(),
})
}
}
#[cfg(test)]
mod tests;