Skip to main content

cpex_core/extensions/
monotonic.rs

1// Location: ./crates/cpex-core/src/extensions/monotonic.rs
2// Copyright 2025
3// SPDX-License-Identifier: Apache-2.0
4// Authors: Teryl Taylor
5//
6// MonotonicSet — add-only set enforced at the type level.
7//
8// Security labels can only grow. The type exposes insert() but not
9// remove(). Declassification requires a DeclassifierToken that only
10// the security subsystem can construct.
11//
12// Mirrors the spec in rust-implementation-spec.md §2.2.
13
14use std::collections::HashSet;
15use std::hash::Hash;
16
17use serde::{Deserialize, Serialize};
18
19/// A set that only allows additions. No remove() in the public API.
20///
21/// Plugins can call `insert()` but not `remove()`. Declassification
22/// (removal) requires a `DeclassifierToken` that only the security
23/// subsystem can construct.
24///
25/// This enforces the monotonic tier at compile time — a plugin that
26/// tries to call `.remove()` gets a compile error.
27#[derive(Clone, Debug, Serialize, Deserialize)]
28#[serde(transparent)]
29pub struct MonotonicSet<T: Eq + Hash> {
30    inner: HashSet<T>,
31}
32
33impl<T: Eq + Hash> MonotonicSet<T> {
34    /// Create an empty monotonic set.
35    pub fn new() -> Self {
36        Self {
37            inner: HashSet::new(),
38        }
39    }
40
41    /// Create from an existing HashSet.
42    pub fn from_set(set: HashSet<T>) -> Self {
43        Self { inner: set }
44    }
45
46    /// Add a value. Returns true if the value was newly inserted.
47    pub fn insert(&mut self, value: T) -> bool {
48        self.inner.insert(value)
49    }
50
51    /// Check if the set contains a value.
52    pub fn contains(&self, value: &T) -> bool {
53        self.inner.contains(value)
54    }
55
56    /// Iterate over the values.
57    pub fn iter(&self) -> impl Iterator<Item = &T> {
58        self.inner.iter()
59    }
60
61    /// Number of elements.
62    pub fn len(&self) -> usize {
63        self.inner.len()
64    }
65
66    /// Whether the set is empty.
67    pub fn is_empty(&self) -> bool {
68        self.inner.is_empty()
69    }
70
71    /// Whether this set is a superset of another.
72    pub fn is_superset(&self, other: &MonotonicSet<T>) -> bool {
73        self.inner.is_superset(&other.inner)
74    }
75
76    /// Get a reference to the inner HashSet (read-only).
77    pub fn as_set(&self) -> &HashSet<T> {
78        &self.inner
79    }
80
81    /// Removal requires a DeclassifierToken — privileged, audited operation.
82    /// Only the security subsystem can construct the token.
83    pub fn remove_with_declassifier(&mut self, value: &T, _token: &DeclassifierToken) -> bool {
84        self.inner.remove(value)
85    }
86}
87
88impl<T: Eq + Hash> Default for MonotonicSet<T> {
89    fn default() -> Self {
90        Self::new()
91    }
92}
93
94/// Opaque token for declassification — only the security subsystem
95/// can create one. Constructing this token is a privileged operation.
96pub struct DeclassifierToken {
97    _private: (),
98}
99
100impl DeclassifierToken {
101    /// Only callable by the framework/security subsystem.
102    #[allow(dead_code)]
103    pub(crate) fn new() -> Self {
104        Self { _private: () }
105    }
106}
107
108/// Case-insensitive label lookup on MonotonicSet<String>.
109impl MonotonicSet<String> {
110    /// Check if a label exists (case-insensitive).
111    pub fn has_label(&self, label: &str) -> bool {
112        let lower = label.to_lowercase();
113        self.inner.iter().any(|l| l.to_lowercase() == lower)
114    }
115
116    /// Add a label (case-preserving on insert).
117    pub fn add_label(&mut self, label: impl Into<String>) {
118        self.inner.insert(label.into());
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn test_monotonic_insert_only() {
128        let mut set = MonotonicSet::new();
129        set.insert("PII".to_string());
130        set.insert("CONFIDENTIAL".to_string());
131        assert!(set.contains(&"PII".to_string()));
132        assert_eq!(set.len(), 2);
133        // No remove() method available — this is the key guarantee
134    }
135
136    #[test]
137    fn test_monotonic_superset() {
138        let mut before = MonotonicSet::new();
139        before.insert("PII".to_string());
140
141        let mut after = before.clone();
142        after.insert("HIPAA".to_string());
143
144        assert!(after.is_superset(&before));
145        assert!(!before.is_superset(&after));
146    }
147
148    #[test]
149    fn test_monotonic_declassifier() {
150        let mut set = MonotonicSet::new();
151        set.insert("PII".to_string());
152
153        // Only works with the token
154        let token = DeclassifierToken::new();
155        assert!(set.remove_with_declassifier(&"PII".to_string(), &token));
156        assert!(!set.contains(&"PII".to_string()));
157    }
158
159    #[test]
160    fn test_monotonic_has_label_case_insensitive() {
161        let mut set = MonotonicSet::new();
162        set.add_label("PII");
163        assert!(set.has_label("pii"));
164        assert!(set.has_label("PII"));
165        assert!(set.has_label("Pii"));
166    }
167
168    #[test]
169    fn test_monotonic_serde_roundtrip() {
170        let mut set = MonotonicSet::new();
171        set.insert("PII".to_string());
172        set.insert("HIPAA".to_string());
173
174        let json = serde_json::to_string(&set).unwrap();
175        let deserialized: MonotonicSet<String> = serde_json::from_str(&json).unwrap();
176        assert!(deserialized.contains(&"PII".to_string()));
177        assert!(deserialized.contains(&"HIPAA".to_string()));
178    }
179}