sup_xml_core/xsd/
resolver.rs1use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11
12pub trait SchemaResolver {
30 fn resolve(
31 &self,
32 location: &str,
33 target_namespace: Option<&str>,
34 ) -> Result<Option<Vec<u8>>, std::io::Error>;
35}
36
37#[derive(Debug, Clone, Copy, Default)]
41pub struct NoResolver;
42
43impl SchemaResolver for NoResolver {
44 fn resolve(&self, _location: &str, _target_ns: Option<&str>)
45 -> Result<Option<Vec<u8>>, std::io::Error>
46 {
47 Ok(None)
48 }
49}
50
51#[derive(Debug, Clone)]
58pub struct FsResolver {
59 base_dir: PathBuf,
60}
61
62impl FsResolver {
63 pub fn new(base_dir: impl Into<PathBuf>) -> Self {
64 Self { base_dir: base_dir.into() }
65 }
66}
67
68impl SchemaResolver for FsResolver {
69 fn resolve(&self, location: &str, _target_ns: Option<&str>)
70 -> Result<Option<Vec<u8>>, std::io::Error>
71 {
72 let p = Path::new(location);
73 if p.is_absolute() {
74 return Err(std::io::Error::new(
75 std::io::ErrorKind::PermissionDenied,
76 format!("absolute schemaLocation rejected: {location:?}"),
77 ));
78 }
79 let candidate = self.base_dir.join(p);
81 let base_components: Vec<_> = self.base_dir.components().collect();
86 let cand_components: Vec<_> = candidate.components().collect();
87 if cand_components.len() < base_components.len()
88 || cand_components[..base_components.len()] != base_components[..]
89 {
90 return Err(std::io::Error::new(
91 std::io::ErrorKind::PermissionDenied,
92 format!("schemaLocation escapes base directory: {location:?}"),
93 ));
94 }
95 let tail = &cand_components[base_components.len()..];
97 if tail.iter().any(|c| matches!(c, std::path::Component::ParentDir)) {
98 return Err(std::io::Error::new(
99 std::io::ErrorKind::PermissionDenied,
100 format!("schemaLocation contains '..': {location:?}"),
101 ));
102 }
103 match std::fs::read(&candidate) {
104 Ok(b) => Ok(Some(b)),
105 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
106 Err(e) => Err(e),
107 }
108 }
109}
110
111#[derive(Debug, Default, Clone)]
115pub struct InMemoryResolver {
116 map: HashMap<String, Vec<u8>>,
117}
118
119impl InMemoryResolver {
120 pub fn new() -> Self { Self::default() }
121
122 pub fn insert(&mut self, location: impl Into<String>, bytes: impl Into<Vec<u8>>) {
123 self.map.insert(location.into(), bytes.into());
124 }
125
126 pub fn with(mut self, location: impl Into<String>, bytes: impl Into<Vec<u8>>) -> Self {
129 self.insert(location, bytes);
130 self
131 }
132}
133
134impl SchemaResolver for InMemoryResolver {
135 fn resolve(&self, location: &str, _target_ns: Option<&str>)
136 -> Result<Option<Vec<u8>>, std::io::Error>
137 {
138 Ok(self.map.get(location).cloned())
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn no_resolver_always_declines() {
148 let r = NoResolver;
149 assert!(r.resolve("anything.xsd", None).unwrap().is_none());
150 }
151
152 #[test]
153 fn in_memory_resolves_known_locations() {
154 let r = InMemoryResolver::new()
155 .with("a.xsd", b"<a/>".to_vec())
156 .with("b.xsd", b"<b/>".to_vec());
157 assert_eq!(r.resolve("a.xsd", None).unwrap().as_deref(), Some(b"<a/>".as_slice()));
158 assert!(r.resolve("c.xsd", None).unwrap().is_none());
159 }
160
161 #[test]
162 fn fs_resolver_rejects_absolute() {
163 let r = FsResolver::new("/tmp");
164 let err = r.resolve("/etc/passwd", None).unwrap_err();
165 assert_eq!(err.kind(), std::io::ErrorKind::PermissionDenied);
166 }
167
168 #[test]
169 fn fs_resolver_rejects_parent_traversal() {
170 let r = FsResolver::new("/tmp/schemas");
171 let err = r.resolve("../etc/passwd", None).unwrap_err();
172 assert_eq!(err.kind(), std::io::ErrorKind::PermissionDenied);
173 }
174}