1use std::collections::HashMap;
23
24use crate::error::{Error, Result};
25use crate::resolve::Resolver;
26use crate::schema::Schema;
27
28pub const BUNDLE_MAGIC: &[u8; 4] = b"VRSB";
30pub const BUNDLE_VERSION: u8 = 1;
32const BUNDLE_HEADER_LEN: usize = 12;
33
34#[derive(Clone, Debug, Default)]
36pub struct SchemaRegistry {
37 by_id: HashMap<u128, Schema>,
38}
39
40impl SchemaRegistry {
41 pub fn new() -> SchemaRegistry {
42 SchemaRegistry {
43 by_id: HashMap::new(),
44 }
45 }
46
47 pub fn register(&mut self, schema: Schema) -> u128 {
50 let id = schema.id();
51 self.by_id.entry(id).or_insert(schema);
52 id
53 }
54
55 pub fn register_canonical(&mut self, bytes: &[u8]) -> Result<u128> {
59 Ok(self.register(Schema::from_canonical(bytes)?))
60 }
61
62 pub fn get(&self, id: u128) -> Option<&Schema> {
64 self.by_id.get(&id)
65 }
66
67 pub fn contains(&self, id: u128) -> bool {
68 self.by_id.contains_key(&id)
69 }
70
71 pub fn len(&self) -> usize {
72 self.by_id.len()
73 }
74
75 pub fn is_empty(&self) -> bool {
76 self.by_id.is_empty()
77 }
78
79 pub fn ids(&self) -> impl Iterator<Item = u128> + '_ {
81 self.by_id.keys().copied()
82 }
83
84 pub fn resolver_for(&self, writer_id: u128, reader: &Schema) -> Result<Resolver> {
93 let writer = self.get(writer_id).ok_or(Error::SchemaIdMismatch {
94 message: writer_id,
95 expected: reader.id(),
96 })?;
97 Resolver::new(writer, reader)
98 }
99
100 pub fn to_bundle(&self) -> Vec<u8> {
104 let mut ids: Vec<u128> = self.by_id.keys().copied().collect();
105 ids.sort_unstable();
106
107 let mut buf = Vec::new();
108 buf.extend_from_slice(BUNDLE_MAGIC);
109 buf.push(BUNDLE_VERSION);
110 buf.extend_from_slice(&[0u8; 3]); buf.extend_from_slice(&(ids.len() as u32).to_le_bytes());
112 for id in ids {
113 let canonical = self.by_id[&id].canonical_bytes();
114 buf.extend_from_slice(&(canonical.len() as u32).to_le_bytes());
115 buf.extend_from_slice(canonical);
116 }
117 buf
118 }
119
120 pub fn from_bundle(bytes: &[u8]) -> Result<SchemaRegistry> {
124 let mut reg = SchemaRegistry::new();
125 reg.merge_bundle(bytes)?;
126 Ok(reg)
127 }
128
129 pub fn merge_bundle(&mut self, bytes: &[u8]) -> Result<usize> {
132 if bytes.len() < BUNDLE_HEADER_LEN {
133 return Err(Error::BadSchema("schema bundle truncated".into()));
134 }
135 if &bytes[0..4] != BUNDLE_MAGIC {
136 return Err(Error::BadSchema("bad schema-bundle magic".into()));
137 }
138 if bytes[4] != BUNDLE_VERSION {
139 return Err(Error::BadSchema("unsupported schema-bundle version".into()));
140 }
141 let count = u32::from_le_bytes(bytes[8..12].try_into().unwrap()) as usize;
142 let mut pos = BUNDLE_HEADER_LEN;
143 let mut added = 0;
144 for _ in 0..count {
145 let end = pos
146 .checked_add(4)
147 .filter(|&e| e <= bytes.len())
148 .ok_or_else(|| Error::BadSchema("schema bundle truncated".into()))?;
149 let len = u32::from_le_bytes(bytes[pos..end].try_into().unwrap()) as usize;
150 pos = end;
151 let entry_end = pos
152 .checked_add(len)
153 .filter(|&e| e <= bytes.len())
154 .ok_or_else(|| Error::BadSchema("schema bundle entry out of bounds".into()))?;
155 let before = self.len();
156 self.register_canonical(&bytes[pos..entry_end])?;
157 if self.len() > before {
158 added += 1;
159 }
160 pos = entry_end;
161 }
162 if pos != bytes.len() {
163 return Err(Error::BadSchema(
164 "trailing bytes after schema bundle".into(),
165 ));
166 }
167 Ok(added)
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174 use crate::schema::{Dt, SchemaBuilder};
175 use crate::{encode, Message, SchemaMode, Value};
176
177 fn v0() -> Schema {
178 SchemaBuilder::new()
179 .add_struct(
180 "LogEvent",
181 vec![(1, "service", Dt::Str), (4, "latency", Dt::U16)],
182 )
183 .build("LogEvent")
184 .unwrap()
185 }
186
187 fn v1() -> Schema {
188 SchemaBuilder::new()
189 .add_struct(
190 "LogEvent",
191 vec![
192 (1, "service", Dt::Str),
193 (3, "message", Dt::Str),
194 (4, "latency", Dt::U32), ],
196 )
197 .build("LogEvent")
198 .unwrap()
199 }
200
201 #[test]
202 fn register_is_content_addressed_and_idempotent() {
203 let mut reg = SchemaRegistry::new();
204 let id_a = reg.register(v0());
205 let id_b = reg.register(v0());
207 assert_eq!(id_a, id_b);
208 assert_eq!(reg.len(), 1);
209 assert!(reg.contains(id_a));
210 assert_eq!(reg.get(id_a).unwrap().id(), id_a);
211 }
212
213 #[test]
214 fn registry_resolves_a_hash_only_peer_message() {
215 let writer = v0();
217 let bytes = encode(
218 &writer,
219 &Value::Struct(vec![(1, Value::str("checkout")), (4, Value::U16(900))]),
220 SchemaMode::HashOnly,
221 )
222 .unwrap();
223 let msg = Message::parse(&bytes).unwrap();
224 assert!(
225 !msg.has_inline_schema(),
226 "hash-only: nothing to read without the registry"
227 );
228
229 let mut reg = SchemaRegistry::new();
231 reg.register(writer);
232 let reader = v1();
233
234 let resolver = reg.resolver_for(msg.schema_id(), &reader).unwrap();
235 let root = msg.root(&resolver).unwrap();
236 assert_eq!(root.get_str(1).unwrap(), Some("checkout"));
237 assert_eq!(root.get_u32(4).unwrap(), Some(900), "u16 widened to u32");
238 assert_eq!(
239 root.get_str(3).unwrap(),
240 None,
241 "field added in v1, absent in v0 data"
242 );
243
244 assert!(reg.resolver_for(0xdead_beef, &reader).is_err());
246 }
247
248 #[test]
249 fn bundle_round_trips_and_is_deterministic() {
250 let mut reg = SchemaRegistry::new();
251 reg.register(v0());
252 reg.register(v1());
253 let bundle = reg.to_bundle();
254 let mut reg2 = SchemaRegistry::new();
256 reg2.register(v1());
257 reg2.register(v0());
258 assert_eq!(bundle, reg2.to_bundle());
259
260 let loaded = SchemaRegistry::from_bundle(&bundle).unwrap();
261 assert_eq!(loaded.len(), 2);
262 for id in reg.ids() {
263 assert_eq!(loaded.get(id).unwrap().id(), id);
264 }
265 }
266
267 #[test]
268 fn merge_reports_new_additions_and_skips_duplicates() {
269 let mut a = SchemaRegistry::new();
270 a.register(v0());
271 let mut b = SchemaRegistry::new();
272 b.register(v0()); b.register(v1()); let added = a.merge_bundle(&b.to_bundle()).unwrap();
275 assert_eq!(added, 1, "only v1 is new");
276 assert_eq!(a.len(), 2);
277 }
278
279 #[test]
280 fn rejects_corrupt_bundle() {
281 let mut reg = SchemaRegistry::new();
282 reg.register(v0());
283 let good = reg.to_bundle();
284
285 let mut bad_magic = good.clone();
286 bad_magic[0] = b'X';
287 assert!(SchemaRegistry::from_bundle(&bad_magic).is_err());
288
289 let mut bad_len = good.clone();
290 bad_len[8..12].copy_from_slice(&1u32.to_le_bytes()); bad_len[12..16].copy_from_slice(&u32::MAX.to_le_bytes());
293 assert!(SchemaRegistry::from_bundle(&bad_len).is_err());
294
295 assert!(
296 SchemaRegistry::from_bundle(&good[..6]).is_err(),
297 "truncated header"
298 );
299 }
300}