1extern crate alloc;
2
3use alloc::{
4 collections::{BTreeMap, BTreeSet},
5 string::{String, ToString},
6};
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10use crate::identity::PluginIdentity;
11
12#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(deny_unknown_fields)]
18pub struct AugmentedMetadata {
19 pub identity: PluginIdentity,
21
22 pub negotiated_functions: BTreeMap<String, u32>,
25
26 pub negotiated_capabilities: BTreeSet<String>,
29
30 pub handshake_completed_at: i64,
32
33 pub self_check_passed: bool,
35
36 pub self_check_completed_at: i64,
38
39 pub expires_at: i64,
41}
42
43#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(deny_unknown_fields)]
46pub struct PluginRegistryRecord {
47 pub plugin_id: String,
49
50 pub augmented_metadata: AugmentedMetadata,
52
53 pub registered_at: i64,
55}
56
57#[derive(Debug, Clone, Error, PartialEq, Eq)]
59pub enum AugmentedMetadataError {
60 #[error("identity validation failed: {0}")]
61 IdentityValidationFailed(String),
62
63 #[error(
64 "augmented metadata serialization exceeds max size of {} bytes",
65 crate::limits::AUGMENTED_METADATA_MAX_BYTES
66 )]
67 SizeLimitExceeded,
68
69 #[error("invalid timestamp: {0}")]
70 InvalidTimestamp(String),
71
72 #[error("expires_at must be after handshake_completed_at")]
73 ExpirationBeforeHandshake,
74
75 #[error("no negotiated functions")]
76 NoNegotiatedFunctions,
77
78 #[error("serialization failed: {0}")]
79 SerializationFailed(String),
80}
81
82impl AugmentedMetadata {
83 pub fn validate(&self) -> Result<(), AugmentedMetadataError> {
85 self.identity
87 .validate()
88 .map_err(|e| AugmentedMetadataError::IdentityValidationFailed(e.to_string()))?;
89
90 if self.handshake_completed_at <= 0 {
92 return Err(AugmentedMetadataError::InvalidTimestamp(
93 "handshake_completed_at must be positive".to_string(),
94 ));
95 }
96
97 if self.self_check_completed_at <= 0 {
98 return Err(AugmentedMetadataError::InvalidTimestamp(
99 "self_check_completed_at must be positive".to_string(),
100 ));
101 }
102
103 if self.expires_at <= 0 {
104 return Err(AugmentedMetadataError::InvalidTimestamp(
105 "expires_at must be positive".to_string(),
106 ));
107 }
108
109 if self.expires_at <= self.handshake_completed_at {
110 return Err(AugmentedMetadataError::ExpirationBeforeHandshake);
111 }
112
113 if self.negotiated_functions.is_empty() {
115 return Err(AugmentedMetadataError::NoNegotiatedFunctions);
116 }
117
118 self.check_size_limit()?;
120
121 Ok(())
122 }
123
124 pub fn check_size_limit(&self) -> Result<(), AugmentedMetadataError> {
126 let serialized = serde_json::to_vec(self)
127 .map_err(|e| AugmentedMetadataError::SerializationFailed(e.to_string()))?;
128
129 if serialized.len() > crate::limits::AUGMENTED_METADATA_MAX_BYTES {
130 return Err(AugmentedMetadataError::SizeLimitExceeded);
131 }
132
133 Ok(())
134 }
135
136 pub fn from_handshake_and_self_check(
152 identity: PluginIdentity,
153 negotiated_functions: BTreeMap<String, u32>,
154 negotiated_capabilities: BTreeSet<String>,
155 handshake_completed_at: i64,
156 self_check_passed: bool,
157 self_check_completed_at: i64,
158 ttl_seconds: u64,
159 ) -> Result<Self, AugmentedMetadataError> {
160 let expires_at = handshake_completed_at + ttl_seconds as i64;
161
162 let metadata = AugmentedMetadata {
163 identity,
164 negotiated_functions,
165 negotiated_capabilities,
166 handshake_completed_at,
167 self_check_passed,
168 self_check_completed_at,
169 expires_at,
170 };
171
172 metadata.validate()?;
173 Ok(metadata)
174 }
175
176 pub fn is_expired(&self, now_timestamp: i64) -> bool {
178 now_timestamp >= self.expires_at
179 }
180
181 pub fn ttl_seconds(&self, now_timestamp: i64) -> i64 {
183 (self.expires_at - now_timestamp).max(0)
184 }
185}
186
187impl PluginRegistryRecord {
188 pub fn validate(&self) -> Result<(), AugmentedMetadataError> {
190 if self.plugin_id.is_empty() {
191 return Err(AugmentedMetadataError::InvalidTimestamp(
192 "plugin_id must not be empty".to_string(),
193 ));
194 }
195
196 if self.registered_at <= 0 {
197 return Err(AugmentedMetadataError::InvalidTimestamp(
198 "registered_at must be positive".to_string(),
199 ));
200 }
201
202 self.augmented_metadata.validate()?;
203
204 Ok(())
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 fn make_test_identity() -> PluginIdentity {
213 PluginIdentity {
214 magic: crate::identity::CC_LB_PLUGIN_MAGIC,
215 abi_envelope: 1,
216 plugin_name: "test-plugin".to_string(),
217 plugin_version: "1.0.0".to_string(),
218 }
219 }
220
221 #[test]
222 fn test_augmented_metadata_valid() {
223 let mut functions = BTreeMap::new();
224 functions.insert("route".to_string(), 1);
225
226 let mut capabilities = BTreeSet::new();
227 capabilities.insert("async".to_string());
228
229 let metadata = AugmentedMetadata {
230 identity: make_test_identity(),
231 negotiated_functions: functions,
232 negotiated_capabilities: capabilities,
233 handshake_completed_at: 1000,
234 self_check_passed: true,
235 self_check_completed_at: 1100,
236 expires_at: 2000,
237 };
238
239 assert!(metadata.validate().is_ok());
240 }
241
242 #[test]
243 fn test_augmented_metadata_no_functions() {
244 let metadata = AugmentedMetadata {
245 identity: make_test_identity(),
246 negotiated_functions: BTreeMap::new(),
247 negotiated_capabilities: BTreeSet::new(),
248 handshake_completed_at: 1000,
249 self_check_passed: true,
250 self_check_completed_at: 1100,
251 expires_at: 2000,
252 };
253
254 assert_eq!(
255 metadata.validate(),
256 Err(AugmentedMetadataError::NoNegotiatedFunctions)
257 );
258 }
259
260 #[test]
261 fn test_augmented_metadata_invalid_handshake_timestamp() {
262 let mut functions = BTreeMap::new();
263 functions.insert("route".to_string(), 1);
264
265 let metadata = AugmentedMetadata {
266 identity: make_test_identity(),
267 negotiated_functions: functions,
268 negotiated_capabilities: BTreeSet::new(),
269 handshake_completed_at: 0,
270 self_check_passed: true,
271 self_check_completed_at: 1100,
272 expires_at: 2000,
273 };
274
275 assert!(metadata.validate().is_err());
276 }
277
278 #[test]
279 fn test_augmented_metadata_expiration_before_handshake() {
280 let mut functions = BTreeMap::new();
281 functions.insert("route".to_string(), 1);
282
283 let metadata = AugmentedMetadata {
284 identity: make_test_identity(),
285 negotiated_functions: functions,
286 negotiated_capabilities: BTreeSet::new(),
287 handshake_completed_at: 2000,
288 self_check_passed: true,
289 self_check_completed_at: 2100,
290 expires_at: 1000,
291 };
292
293 assert_eq!(
294 metadata.validate(),
295 Err(AugmentedMetadataError::ExpirationBeforeHandshake)
296 );
297 }
298
299 #[test]
300 fn test_augmented_metadata_serde() {
301 let mut functions = BTreeMap::new();
302 functions.insert("route".to_string(), 1);
303 functions.insert("observe".to_string(), 2);
304
305 let mut capabilities = BTreeSet::new();
306 capabilities.insert("async".to_string());
307 capabilities.insert("streaming".to_string());
308
309 let metadata = AugmentedMetadata {
310 identity: make_test_identity(),
311 negotiated_functions: functions,
312 negotiated_capabilities: capabilities,
313 handshake_completed_at: 1000,
314 self_check_passed: true,
315 self_check_completed_at: 1100,
316 expires_at: 2000,
317 };
318
319 let json = serde_json::to_string(&metadata).unwrap();
320 let deserialized: AugmentedMetadata = serde_json::from_str(&json).unwrap();
321 assert_eq!(metadata, deserialized);
322 }
323
324 #[test]
325 fn test_augmented_metadata_deny_unknown_fields() {
326 let json = r#"{"identity":{"magic":[204,27,112,16,0,1,0,0],"abi_envelope":1,"plugin_name":"test","plugin_version":"1.0"},"negotiated_functions":{"route":1},"negotiated_capabilities":[],"handshake_completed_at":1000,"self_check_passed":true,"self_check_completed_at":1100,"expires_at":2000,"unknown":"field"}"#;
327 let result: Result<AugmentedMetadata, _> = serde_json::from_str(json);
328 assert!(result.is_err());
329 }
330
331 #[test]
332 fn test_augmented_metadata_size_limit() {
333 let mut functions = BTreeMap::new();
334 functions.insert("route".to_string(), 1);
335
336 let mut capabilities = BTreeSet::new();
337 capabilities.insert("x".repeat(crate::limits::AUGMENTED_METADATA_MAX_BYTES + 1));
339
340 let metadata = AugmentedMetadata {
341 identity: make_test_identity(),
342 negotiated_functions: functions,
343 negotiated_capabilities: capabilities,
344 handshake_completed_at: 1000,
345 self_check_passed: true,
346 self_check_completed_at: 1100,
347 expires_at: 2000,
348 };
349
350 assert_eq!(
351 metadata.validate(),
352 Err(AugmentedMetadataError::SizeLimitExceeded)
353 );
354 }
355
356 #[test]
357 fn test_from_handshake_and_self_check() {
358 let mut functions = BTreeMap::new();
359 functions.insert("route".to_string(), 1);
360
361 let mut capabilities = BTreeSet::new();
362 capabilities.insert("async".to_string());
363
364 let metadata = AugmentedMetadata::from_handshake_and_self_check(
365 make_test_identity(),
366 functions,
367 capabilities,
368 1000,
369 true,
370 1100,
371 3600,
372 );
373
374 assert!(metadata.is_ok());
375 let m = metadata.unwrap();
376 assert_eq!(m.handshake_completed_at, 1000);
377 assert!(m.self_check_passed);
378 assert_eq!(m.expires_at, 1000 + 3600);
379 }
380
381 #[test]
382 fn test_from_handshake_and_self_check_validates() {
383 let mut functions = BTreeMap::new();
384 functions.insert("route".to_string(), 1);
385
386 let result = AugmentedMetadata::from_handshake_and_self_check(
388 make_test_identity(),
389 functions,
390 BTreeSet::new(),
391 0,
392 true,
393 1100,
394 3600,
395 );
396
397 assert!(result.is_err());
398 }
399
400 #[test]
401 fn test_is_expired() {
402 let mut functions = BTreeMap::new();
403 functions.insert("route".to_string(), 1);
404
405 let metadata = AugmentedMetadata {
406 identity: make_test_identity(),
407 negotiated_functions: functions,
408 negotiated_capabilities: BTreeSet::new(),
409 handshake_completed_at: 1000,
410 self_check_passed: true,
411 self_check_completed_at: 1100,
412 expires_at: 2000,
413 };
414
415 assert!(!metadata.is_expired(1999));
416 assert!(metadata.is_expired(2000));
417 assert!(metadata.is_expired(2001));
418 }
419
420 #[test]
421 fn test_ttl_seconds() {
422 let mut functions = BTreeMap::new();
423 functions.insert("route".to_string(), 1);
424
425 let metadata = AugmentedMetadata {
426 identity: make_test_identity(),
427 negotiated_functions: functions,
428 negotiated_capabilities: BTreeSet::new(),
429 handshake_completed_at: 1000,
430 self_check_passed: true,
431 self_check_completed_at: 1100,
432 expires_at: 2000,
433 };
434
435 assert_eq!(metadata.ttl_seconds(1500), 500);
436 assert_eq!(metadata.ttl_seconds(2000), 0);
437 assert_eq!(metadata.ttl_seconds(2100), 0);
438 }
439
440 #[test]
441 fn test_plugin_registry_record_valid() {
442 let mut functions = BTreeMap::new();
443 functions.insert("route".to_string(), 1);
444
445 let metadata = AugmentedMetadata {
446 identity: make_test_identity(),
447 negotiated_functions: functions,
448 negotiated_capabilities: BTreeSet::new(),
449 handshake_completed_at: 1000,
450 self_check_passed: true,
451 self_check_completed_at: 1100,
452 expires_at: 2000,
453 };
454
455 let record = PluginRegistryRecord {
456 plugin_id: "plugin-uuid-123".to_string(),
457 augmented_metadata: metadata,
458 registered_at: 900,
459 };
460
461 assert!(record.validate().is_ok());
462 }
463
464 #[test]
465 fn test_plugin_registry_record_empty_id() {
466 let mut functions = BTreeMap::new();
467 functions.insert("route".to_string(), 1);
468
469 let metadata = AugmentedMetadata {
470 identity: make_test_identity(),
471 negotiated_functions: functions,
472 negotiated_capabilities: BTreeSet::new(),
473 handshake_completed_at: 1000,
474 self_check_passed: true,
475 self_check_completed_at: 1100,
476 expires_at: 2000,
477 };
478
479 let record = PluginRegistryRecord {
480 plugin_id: String::new(),
481 augmented_metadata: metadata,
482 registered_at: 900,
483 };
484
485 assert!(record.validate().is_err());
486 }
487
488 #[test]
489 fn test_plugin_registry_record_serde() {
490 let mut functions = BTreeMap::new();
491 functions.insert("route".to_string(), 1);
492
493 let metadata = AugmentedMetadata {
494 identity: make_test_identity(),
495 negotiated_functions: functions,
496 negotiated_capabilities: BTreeSet::new(),
497 handshake_completed_at: 1000,
498 self_check_passed: true,
499 self_check_completed_at: 1100,
500 expires_at: 2000,
501 };
502
503 let record = PluginRegistryRecord {
504 plugin_id: "plugin-123".to_string(),
505 augmented_metadata: metadata,
506 registered_at: 900,
507 };
508
509 let json = serde_json::to_string(&record).unwrap();
510 let deserialized: PluginRegistryRecord = serde_json::from_str(&json).unwrap();
511 assert_eq!(record, deserialized);
512 }
513
514 #[test]
515 fn test_plugin_registry_record_deny_unknown_fields() {
516 let json = r#"{"plugin_id":"test","augmented_metadata":{"identity":{"magic":[204,27,112,16,0,1,0,0],"abi_envelope":1,"plugin_name":"test","plugin_version":"1.0"},"negotiated_functions":{"route":1},"negotiated_capabilities":[],"handshake_completed_at":1000,"self_check_passed":true,"self_check_completed_at":1100,"expires_at":2000},"registered_at":900,"unknown":"field"}"#;
517 let result: Result<PluginRegistryRecord, _> = serde_json::from_str(json);
518 assert!(result.is_err());
519 }
520}