use std::collections::HashMap;
use std::path::{Path, PathBuf};
pub trait SchemaResolver {
fn resolve(
&self,
location: &str,
target_namespace: Option<&str>,
) -> Result<Option<Vec<u8>>, std::io::Error>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NoResolver;
impl SchemaResolver for NoResolver {
fn resolve(&self, _location: &str, _target_ns: Option<&str>)
-> Result<Option<Vec<u8>>, std::io::Error>
{
Ok(None)
}
}
#[derive(Debug, Clone)]
pub struct FsResolver {
base_dir: PathBuf,
}
impl FsResolver {
pub fn new(base_dir: impl Into<PathBuf>) -> Self {
Self { base_dir: base_dir.into() }
}
}
impl SchemaResolver for FsResolver {
fn resolve(&self, location: &str, _target_ns: Option<&str>)
-> Result<Option<Vec<u8>>, std::io::Error>
{
let p = Path::new(location);
if p.is_absolute() {
return Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
format!("absolute schemaLocation rejected: {location:?}"),
));
}
let candidate = self.base_dir.join(p);
let base_components: Vec<_> = self.base_dir.components().collect();
let cand_components: Vec<_> = candidate.components().collect();
if cand_components.len() < base_components.len()
|| cand_components[..base_components.len()] != base_components[..]
{
return Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
format!("schemaLocation escapes base directory: {location:?}"),
));
}
let tail = &cand_components[base_components.len()..];
if tail.iter().any(|c| matches!(c, std::path::Component::ParentDir)) {
return Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
format!("schemaLocation contains '..': {location:?}"),
));
}
match std::fs::read(&candidate) {
Ok(b) => Ok(Some(b)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e),
}
}
}
#[derive(Debug, Default, Clone)]
pub struct InMemoryResolver {
map: HashMap<String, Vec<u8>>,
}
impl InMemoryResolver {
pub fn new() -> Self { Self::default() }
pub fn insert(&mut self, location: impl Into<String>, bytes: impl Into<Vec<u8>>) {
self.map.insert(location.into(), bytes.into());
}
pub fn with(mut self, location: impl Into<String>, bytes: impl Into<Vec<u8>>) -> Self {
self.insert(location, bytes);
self
}
}
impl SchemaResolver for InMemoryResolver {
fn resolve(&self, location: &str, _target_ns: Option<&str>)
-> Result<Option<Vec<u8>>, std::io::Error>
{
Ok(self.map.get(location).cloned())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_resolver_always_declines() {
let r = NoResolver;
assert!(r.resolve("anything.xsd", None).unwrap().is_none());
}
#[test]
fn in_memory_resolves_known_locations() {
let r = InMemoryResolver::new()
.with("a.xsd", b"<a/>".to_vec())
.with("b.xsd", b"<b/>".to_vec());
assert_eq!(r.resolve("a.xsd", None).unwrap().as_deref(), Some(b"<a/>".as_slice()));
assert!(r.resolve("c.xsd", None).unwrap().is_none());
}
#[test]
fn fs_resolver_rejects_absolute() {
let r = FsResolver::new("/tmp");
let err = r.resolve("/etc/passwd", None).unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::PermissionDenied);
}
#[test]
fn fs_resolver_rejects_parent_traversal() {
let r = FsResolver::new("/tmp/schemas");
let err = r.resolve("../etc/passwd", None).unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::PermissionDenied);
}
}