Skip to main content

aitp_core/
extensions.rs

1//! Forward-compatible extension fields.
2//!
3//! Per RFC-AITP-0001 §7, every signed object reserves an `extensions` slot.
4//! Unknown JSON fields _outside_ `extensions` MUST be rejected (because
5//! signature canonicalization depends on the exact field set). Unknown keys
6//! _inside_ `extensions` MUST be ignored.
7
8use serde::{Deserialize, Serialize};
9use std::collections::BTreeMap;
10
11/// A map of vendor-namespaced extension values.
12///
13/// Keys SHOULD use a reverse-DNS-style prefix (e.g. `vendor.example/feature`)
14/// to avoid collisions across implementations. The map preserves insertion
15/// order for stable canonicalization.
16#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(transparent)]
18pub struct ExtensionsMap(BTreeMap<String, serde_json::Value>);
19
20impl ExtensionsMap {
21    /// Construct an empty extensions map.
22    pub fn new() -> Self {
23        Self::default()
24    }
25
26    /// True if no extensions are set.
27    ///
28    /// Callers embedding this map in a signed struct MUST wrap the field
29    /// as `Option<ExtensionsMap>` with
30    /// `#[serde(default, skip_serializing_if = "Option::is_none")]`, NOT
31    /// `skip_serializing_if = "ExtensionsMap::is_empty"` on a bare
32    /// (non-`Option`) field — the latter conflates an absent `extensions`
33    /// member with a present-but-empty `"extensions":{}`, which are
34    /// different canonical bytes and therefore different signatures
35    /// (RFC-AITP-0001 §5.4.1). See `docs/jcs.md` for the full rationale
36    /// and the list of types that follow this pattern. This method exists
37    /// for callers that only need to check emptiness directly, not as a
38    /// `skip_serializing_if` target.
39    pub fn is_empty(&self) -> bool {
40        self.0.is_empty()
41    }
42
43    /// Insert a key/value pair.
44    pub fn insert(&mut self, key: impl Into<String>, value: serde_json::Value) {
45        self.0.insert(key.into(), value);
46    }
47
48    /// Get a value by key.
49    pub fn get(&self, key: &str) -> Option<&serde_json::Value> {
50        self.0.get(key)
51    }
52
53    /// Iterate over key/value pairs in canonical (sorted) order.
54    pub fn iter(&self) -> impl Iterator<Item = (&String, &serde_json::Value)> {
55        self.0.iter()
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62    use serde::{Deserialize, Serialize};
63    use serde_json::json;
64
65    #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
66    struct Holder {
67        #[serde(default, skip_serializing_if = "ExtensionsMap::is_empty")]
68        extensions: ExtensionsMap,
69    }
70
71    #[test]
72    fn empty_is_empty() {
73        let m = ExtensionsMap::new();
74        assert!(m.is_empty());
75    }
76
77    #[test]
78    fn after_insert_not_empty() {
79        let mut m = ExtensionsMap::new();
80        m.insert("vendor.example/foo", json!({"x": 1}));
81        assert!(!m.is_empty());
82        assert_eq!(m.get("vendor.example/foo"), Some(&json!({"x": 1})));
83    }
84
85    #[test]
86    fn round_trip_with_extension() {
87        let mut m = ExtensionsMap::new();
88        m.insert("vendor.example/foo", json!({"x": 1}));
89        let h = Holder { extensions: m };
90        let s = serde_json::to_string(&h).unwrap();
91        assert!(s.contains("vendor.example/foo"));
92        let back: Holder = serde_json::from_str(&s).unwrap();
93        assert_eq!(h, back);
94    }
95
96    #[test]
97    fn empty_extensions_omitted_from_canonical_json() {
98        let h = Holder {
99            extensions: ExtensionsMap::new(),
100        };
101        let s = serde_json::to_string(&h).unwrap();
102        // skip_serializing_if drops the field entirely when empty.
103        assert_eq!(s, "{}");
104    }
105
106    #[test]
107    fn keys_iterate_in_sorted_order() {
108        let mut m = ExtensionsMap::new();
109        m.insert("z", json!(1));
110        m.insert("a", json!(2));
111        m.insert("m", json!(3));
112        let order: Vec<_> = m.iter().map(|(k, _)| k.as_str()).collect();
113        assert_eq!(order, vec!["a", "m", "z"]);
114    }
115}