Skip to main content

camel_component_validator/
resolver.rs

1//! Resource resolution for schema files.
2//!
3//! Currently only filesystem paths are supported.
4
5use camel_component_api::CamelError;
6
7// TODO(VAL-013): Resource resolution currently filesystem-only.
8// Future: support classpath:, http:, and data: URIs.
9
10/// Resolves schema resources. Currently only filesystem paths are supported.
11/// TODO(VAL-013): Implement URL and classpath resolvers.
12pub trait ResourceResolver: Send + Sync {
13    /// Read the resource at `path` into bytes.
14    fn resolve(&self, path: &str) -> Result<Vec<u8>, CamelError>;
15}
16
17/// Default filesystem-based resolver.
18pub struct FilesystemResolver;
19
20/// Maximum schema size read from disk (audit 2026-08-31, F4-6). Schemas are
21/// operator config; the cap is defense-in-depth against a URI pointing at a
22/// huge/unbounded file (e.g. a device file or a multi-GB log).
23const MAX_SCHEMA_BYTES: u64 = 16 * 1024 * 1024;
24
25impl ResourceResolver for FilesystemResolver {
26    fn resolve(&self, path: &str) -> Result<Vec<u8>, CamelError> {
27        use std::io::Read;
28        // Re-review of F4-6: enforce the cap on the READ, not on metadata —
29        // a file can grow between a stat check and fs::read (TOCTOU), and
30        // device files report len 0. take(MAX+1) detects an over-cap file
31        // instead of silently truncating it.
32        let file = std::fs::File::open(path).map_err(|e| {
33            CamelError::EndpointCreationFailed(format!("failed to open schema file '{path}': {e}"))
34        })?;
35        let mut buf = Vec::new();
36        file.take(MAX_SCHEMA_BYTES + 1)
37            .read_to_end(&mut buf)
38            .map_err(|e| {
39                CamelError::EndpointCreationFailed(format!(
40                    "failed to read schema file '{path}': {e}"
41                ))
42            })?;
43        if buf.len() as u64 > MAX_SCHEMA_BYTES {
44            return Err(CamelError::EndpointCreationFailed(format!(
45                "schema file '{path}' exceeds {} bytes",
46                MAX_SCHEMA_BYTES
47            )));
48        }
49        Ok(buf)
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    #[test]
58    fn filesystem_resolver_reads_existing_file() {
59        let mut f = tempfile::Builder::new().suffix(".json").tempfile().unwrap();
60        use std::io::Write;
61        f.write_all(b"hello").unwrap();
62        let resolver = FilesystemResolver;
63        let data = resolver.resolve(f.path().to_str().unwrap()).unwrap();
64        assert_eq!(data, b"hello");
65    }
66
67    #[test]
68    fn filesystem_resolver_errors_on_missing_file() {
69        let resolver = FilesystemResolver;
70        let result = resolver.resolve("/nonexistent/file.json");
71        assert!(result.is_err());
72    }
73
74    /// Re-review of F4-6: the size cap must be enforced on the read itself.
75    /// A file grown past the cap after any metadata pre-check (or a device
76    /// file reporting len 0) must be rejected, not read unbounded.
77    #[test]
78    fn filesystem_resolver_rejects_file_past_cap_on_read() {
79        let mut f = tempfile::Builder::new().suffix(".json").tempfile().unwrap();
80        // Extend past the cap without materializing content in the test body.
81        f.as_file().set_len(super::MAX_SCHEMA_BYTES + 1).unwrap();
82        let resolver = FilesystemResolver;
83        let err = resolver
84            .resolve(f.path().to_str().unwrap())
85            .expect_err("over-cap file must be rejected");
86        let msg = err.to_string();
87        assert!(msg.contains("exceeds"), "cap rejection: {msg}");
88    }
89}