camel_component_api/embedded_assets.rs
1//! Process-global registry of embedded xslt/xsd/sql assets (mission 351).
2//!
3//! Sealed artifacts carry their xslt/xsd/sql resources as verbatim store
4//! assets. At boot, `camel-cli` registers every xslt-, xsd-, and
5//! sql-class entry here under its store path (`assets/xslt/t.xslt`)
6//! from digest-verified store entries, and rewrites the corresponding
7//! endpoint/site references to `camel-embedded:<asset path>`. The
8//! in-process resolvers read such references from this registry — no
9//! file is written for those classes, so a sealed artifact boots on a
10//! host with no writable temp directory. This module mirrors the
11//! `camel-proto-compiler::embedded` registry (mission 350) that serves
12//! the proto class; the two live in separate leaf crates because the
13//! proto compiler and this API crate cannot depend on each other.
14//!
15//! # Reserved reference prefix
16//!
17//! [`crate::embedded_assets::EMBEDDED_REF_PREFIX`] (`camel-embedded:`) and its percent-encoded
18//! spelling (`camel-embedded%3A`) are reserved: a value that starts with
19//! either spelling is resolved against this registry and never against
20//! the filesystem. A real file whose name starts with either spelling is
21//! therefore shadowed — an accepted reservation, documented here and in
22//! the consuming components' acceptance rules.
23//!
24//! Resolution fails closed: a reference whose key was never registered
25//! resolves to `None`, and the caller reports a boot error instead of
26//! falling back to the host filesystem.
27
28use std::collections::HashMap;
29use std::sync::{Arc, Mutex, OnceLock};
30
31/// Prefix marking an in-memory asset reference: `camel-embedded:<asset
32/// path>`. Reserved across the workspace; see the module documentation.
33pub const EMBEDDED_REF_PREFIX: &str = "camel-embedded:";
34
35/// Percent-encoded spelling of [`EMBEDDED_REF_PREFIX`], used when the
36/// reference travels inside a URI component.
37const ENCODED_REF_PREFIX: &str = "camel-embedded%3A";
38
39type Registry = HashMap<String, Arc<[u8]>>;
40
41fn registry() -> &'static Mutex<Registry> {
42 static REGISTRY: OnceLock<Mutex<Registry>> = OnceLock::new();
43 REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
44}
45
46/// Registers `bytes` under `name` (the store asset path, e.g.
47/// `assets/xslt/t.xslt`). A later registration for the same name
48/// replaces the earlier bytes. Idempotent and infallible: the registry
49/// is populated once per boot from verified store entries.
50pub fn register(name: &str, bytes: Vec<u8>) {
51 let mut guard = registry().lock().unwrap_or_else(|p| p.into_inner());
52 guard.insert(name.to_owned(), Arc::from(bytes.into_boxed_slice()));
53}
54
55/// Returns the bytes registered under `name`, or `None` when the name is
56/// not registered (the reference then fails closed at resolution).
57pub fn get(name: &str) -> Option<Arc<[u8]>> {
58 let guard = registry().lock().unwrap_or_else(|p| p.into_inner());
59 guard.get(name).cloned()
60}
61
62/// Resolves an embedded asset reference to its registered bytes.
63///
64/// Two postures, distinguished by spelling:
65///
66/// - a value starting with the canonical [`EMBEDDED_REF_PREFIX`] is a
67/// literal registry key after prefix removal; the suffix is NEVER
68/// decoded (store paths may contain spaces, Unicode, colons, and
69/// literal `%` sequences, and decoding would corrupt the key);
70/// - a value starting with the encoded prefix (`camel-embedded%3A`) is
71/// percent-decoded in full, exactly once, and the canonical prefix is
72/// then stripped from the result. Invalid UTF-8 after decoding yields
73/// `None`.
74///
75/// Returns `None` when the value carries neither spelling or when the
76/// key is not registered (fail closed; no filesystem fallback).
77pub fn resolve_ref(value: &str) -> Option<Arc<[u8]>> {
78 if let Some(name) = value.strip_prefix(EMBEDDED_REF_PREFIX) {
79 return get(name);
80 }
81 if value.starts_with(ENCODED_REF_PREFIX) {
82 let decoded = percent_encoding::percent_decode_str(value)
83 .decode_utf8()
84 .ok()?;
85 let name = decoded.strip_prefix(EMBEDDED_REF_PREFIX)?;
86 return get(name);
87 }
88 None
89}
90
91#[cfg(test)]
92mod tests {
93 use super::{register, resolve_ref};
94 use std::sync::Arc;
95
96 fn bytes(value: Option<Arc<[u8]>>) -> Vec<u8> {
97 value.expect("registered reference resolves").to_vec()
98 }
99
100 #[test]
101 fn encoded_prefix_is_the_canonical_prefix_with_encoded_colon() {
102 assert_eq!(
103 super::ENCODED_REF_PREFIX,
104 super::EMBEDDED_REF_PREFIX.replace(':', "%3A")
105 );
106 }
107
108 #[test]
109 fn resolve_ref_resolves_canonical_and_percent_encoded_spellings() {
110 register("assets/xslt/t.xslt", b"stylesheet-bytes".to_vec());
111 assert_eq!(
112 &bytes(resolve_ref("camel-embedded:assets/xslt/t.xslt"))[..],
113 b"stylesheet-bytes"
114 );
115 assert_eq!(
116 &bytes(resolve_ref("camel-embedded%3Aassets%2Fxslt%2Ft.xslt"))[..],
117 b"stylesheet-bytes"
118 );
119 }
120
121 #[test]
122 fn resolve_ref_canonical_suffix_is_literal() {
123 register("assets/x/a%20b.xslt", b"pct".to_vec());
124 register("assets/x/a b.xslt", b"space".to_vec());
125 assert_eq!(
126 &bytes(resolve_ref("camel-embedded:assets/x/a%20b.xslt"))[..],
127 b"pct",
128 "the canonical suffix must never be percent-decoded"
129 );
130 assert_eq!(
131 &bytes(resolve_ref("camel-embedded:assets/x/a b.xslt"))[..],
132 b"space"
133 );
134 }
135
136 #[test]
137 fn resolve_ref_encoded_handles_spaces_unicode_colon_and_double_encode() {
138 register("assets/styles/a b.xslt", b"s".to_vec());
139 register("assets/styles/ünïcode.xslt", b"u".to_vec());
140 register("assets/styles/a:b.xslt", b"c".to_vec());
141 register("assets/x/lit%20.xslt", b"litpct".to_vec());
142 register("assets/x/lit.xslt", b"lit".to_vec());
143 // The two `lit` fixtures back the literal canonical spellings: the
144 // percent sequence in the first key stays undecoded.
145 assert_eq!(
146 &bytes(resolve_ref("camel-embedded:assets/x/lit%20.xslt"))[..],
147 b"litpct"
148 );
149 assert_eq!(
150 &bytes(resolve_ref("camel-embedded:assets/x/lit.xslt"))[..],
151 b"lit"
152 );
153 // Re-registered with the same bytes so this test does not depend on
154 // the sibling test's registration order (the registry is global).
155 register("assets/x/a%20b.xslt", b"pct".to_vec());
156 register("assets/x/a b.xslt", b"space".to_vec());
157
158 assert_eq!(
159 &bytes(resolve_ref("camel-embedded%3Aassets%2Fstyles%2Fa%20b.xslt"))[..],
160 b"s"
161 );
162 assert_eq!(
163 &bytes(resolve_ref(
164 "camel-embedded%3Aassets%2Fstyles%2F%C3%BCn%C3%AFcode.xslt"
165 ))[..],
166 b"u"
167 );
168 assert_eq!(
169 &bytes(resolve_ref("camel-embedded%3Aassets%2Fstyles%2Fa%3Ab.xslt"))[..],
170 b"c"
171 );
172
173 // Decode-once: `%2520` → `%20`, so the literal key (with the percent
174 // sequence) is looked up and resolves to the `pct` registration.
175 assert_eq!(
176 &bytes(resolve_ref("camel-embedded%3Aassets%2Fx%2Fa%2520b.xslt"))[..],
177 b"pct"
178 );
179 assert_eq!(
180 &bytes(resolve_ref("camel-embedded%3Aassets%2Fx%2Fa%20b.xslt"))[..],
181 b"space"
182 );
183
184 // `%253A` decodes exactly once to the literal key
185 // `assets/x/lit%3A.xslt` (the `lit` fixtures above are unrelated —
186 // they only back the canonical assertions). That key is never
187 // registered, so resolution fails closed.
188 assert!(
189 resolve_ref("camel-embedded%3Aassets%2Fx%2Flit%253A.xslt").is_none(),
190 "a double-encoded reference must decode exactly once"
191 );
192 }
193
194 #[test]
195 fn resolve_ref_fails_closed_on_unknown_or_plain_values() {
196 assert!(resolve_ref("plain/path").is_none());
197 assert!(resolve_ref("camel-embedded:assets/never-registered").is_none());
198 assert!(
199 resolve_ref("camel-embedded%3Aassets%2Fx%2F%FFbad").is_none(),
200 "invalid UTF-8 after percent-decoding must fail closed"
201 );
202 }
203}