zerodds_security_runtime/
data_tagging.rs1extern crate alloc;
54
55use alloc::collections::BTreeMap;
56use alloc::string::String;
57use alloc::vec::Vec;
58
59use zerodds_rtps::property_list::{WireProperty, WirePropertyList};
60use zerodds_security::data_tagging::{DataTag, DataTaggingPlugin};
61
62pub const TAG_PROPERTY_PREFIX: &str = "dds.sec.data_tags.";
67
68#[derive(Debug, Default)]
70pub struct BuiltinDataTaggingPlugin {
71 tags: BTreeMap<[u8; 16], Vec<DataTag>>,
72}
73
74impl BuiltinDataTaggingPlugin {
75 #[must_use]
77 pub fn new() -> Self {
78 Self::default()
79 }
80
81 #[must_use]
83 pub fn tags_match(publisher: &[DataTag], subscriber: &[DataTag]) -> bool {
84 if subscriber.is_empty() {
85 return true;
86 }
87 subscriber.iter().all(|s| publisher.iter().any(|p| p == s))
88 }
89
90 #[must_use]
95 pub fn encode_tags(tags: &[DataTag]) -> Vec<WireProperty> {
96 tags.iter()
97 .map(|t| {
98 let mut name = String::with_capacity(TAG_PROPERTY_PREFIX.len() + t.name.len());
99 name.push_str(TAG_PROPERTY_PREFIX);
100 name.push_str(&t.name);
101 WireProperty::new(name, t.value.clone())
102 })
103 .collect()
104 }
105
106 #[must_use]
110 pub fn decode_tags(list: &WirePropertyList) -> Vec<DataTag> {
111 list.entries
112 .iter()
113 .filter_map(|p| {
114 p.name.strip_prefix(TAG_PROPERTY_PREFIX).map(|n| DataTag {
115 name: n.to_string(),
116 value: p.value.clone(),
117 })
118 })
119 .collect()
120 }
121}
122
123impl DataTaggingPlugin for BuiltinDataTaggingPlugin {
124 fn set_tags(&mut self, endpoint_guid: [u8; 16], tags: Vec<DataTag>) {
125 if tags.is_empty() {
126 self.tags.remove(&endpoint_guid);
127 } else {
128 self.tags.insert(endpoint_guid, tags);
129 }
130 }
131
132 fn get_tags(&self, endpoint_guid: [u8; 16]) -> Vec<DataTag> {
133 self.tags.get(&endpoint_guid).cloned().unwrap_or_default()
134 }
135
136 fn plugin_class_id(&self) -> &str {
137 "DDS:Tagging:Builtin"
138 }
139}
140
141#[cfg(test)]
146#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
147mod tests {
148 use super::*;
149
150 fn tag(name: &str, value: &str) -> DataTag {
151 DataTag {
152 name: name.into(),
153 value: value.into(),
154 }
155 }
156
157 #[test]
158 fn set_get_roundtrip() {
159 let mut p = BuiltinDataTaggingPlugin::new();
160 let g = [0xAA; 16];
161 p.set_tags(g, vec![tag("classification", "secret")]);
162 assert_eq!(p.get_tags(g), vec![tag("classification", "secret")]);
163 }
164
165 #[test]
166 fn unknown_endpoint_returns_empty() {
167 let p = BuiltinDataTaggingPlugin::new();
168 assert!(p.get_tags([0xCC; 16]).is_empty());
169 }
170
171 #[test]
172 fn set_empty_clears_existing() {
173 let mut p = BuiltinDataTaggingPlugin::new();
174 let g = [0xBB; 16];
175 p.set_tags(g, vec![tag("a", "1")]);
176 p.set_tags(g, Vec::new());
177 assert!(p.get_tags(g).is_empty());
178 }
179
180 #[test]
181 fn match_empty_subscriber_is_wildcard() {
182 assert!(BuiltinDataTaggingPlugin::tags_match(&[], &[]));
184 assert!(BuiltinDataTaggingPlugin::tags_match(
185 &[tag("classification", "secret")],
186 &[],
187 ));
188 }
189
190 #[test]
191 fn match_subset_passes() {
192 let publisher = vec![
193 tag("classification", "secret"),
194 tag("releasability", "nato"),
195 ];
196 let subscriber = vec![tag("classification", "secret")];
197 assert!(BuiltinDataTaggingPlugin::tags_match(
198 &publisher,
199 &subscriber
200 ));
201 }
202
203 #[test]
204 fn match_full_set_passes() {
205 let publisher = vec![
206 tag("classification", "secret"),
207 tag("releasability", "nato"),
208 ];
209 let subscriber = publisher.clone();
210 assert!(BuiltinDataTaggingPlugin::tags_match(
211 &publisher,
212 &subscriber
213 ));
214 }
215
216 #[test]
217 fn match_missing_required_tag_rejects() {
218 let publisher = vec![tag("classification", "secret")];
219 let subscriber = vec![tag("releasability", "nato")];
220 assert!(!BuiltinDataTaggingPlugin::tags_match(
221 &publisher,
222 &subscriber
223 ));
224 }
225
226 #[test]
227 fn match_value_mismatch_rejects() {
228 let publisher = vec![tag("classification", "topsecret")];
230 let subscriber = vec![tag("classification", "secret")];
231 assert!(!BuiltinDataTaggingPlugin::tags_match(
232 &publisher,
233 &subscriber
234 ));
235 }
236
237 #[test]
238 fn match_unknown_subscriber_tag_rejects() {
239 let publisher = vec![tag("classification", "secret")];
241 let subscriber = vec![tag("project", "alpha")];
242 assert!(!BuiltinDataTaggingPlugin::tags_match(
243 &publisher,
244 &subscriber
245 ));
246 }
247
248 #[test]
249 fn empty_publisher_with_required_subscriber_rejects() {
250 let subscriber = vec![tag("classification", "secret")];
252 assert!(!BuiltinDataTaggingPlugin::tags_match(&[], &subscriber));
253 }
254
255 #[test]
256 fn encode_tags_uses_namespace_prefix() {
257 let tags = vec![tag("classification", "secret")];
258 let wire = BuiltinDataTaggingPlugin::encode_tags(&tags);
259 assert_eq!(wire.len(), 1);
260 assert_eq!(wire[0].name, "dds.sec.data_tags.classification");
261 assert_eq!(wire[0].value, "secret");
262 }
263
264 #[test]
265 fn decode_tags_skips_non_tag_properties() {
266 let mut list = WirePropertyList::new();
267 list.push(WireProperty::new(
268 "dds.sec.auth.plugin_class",
269 "DDS:Auth:PKI-DH:1.2",
270 ));
271 list.push(WireProperty::new(
272 "dds.sec.data_tags.classification",
273 "secret",
274 ));
275 list.push(WireProperty::new(
276 "zerodds.sec.supported_suites",
277 "AES_128_GCM",
278 ));
279 list.push(WireProperty::new("dds.sec.data_tags.releasability", "nato"));
280 let decoded = BuiltinDataTaggingPlugin::decode_tags(&list);
281 assert_eq!(
282 decoded,
283 vec![
284 tag("classification", "secret"),
285 tag("releasability", "nato"),
286 ]
287 );
288 }
289
290 #[test]
291 fn wire_roundtrip_via_property_list() {
292 let tags = vec![
295 tag("classification", "secret"),
296 tag("releasability", "nato"),
297 ];
298 let mut list = WirePropertyList::new();
299 for w in BuiltinDataTaggingPlugin::encode_tags(&tags) {
300 list.push(w);
301 }
302 let bytes = list.encode(true).expect("encode");
303 let decoded_list = WirePropertyList::decode(&bytes, true).expect("decode");
304 let decoded_tags = BuiltinDataTaggingPlugin::decode_tags(&decoded_list);
305 assert_eq!(decoded_tags, tags);
306 }
307
308 #[test]
309 fn plugin_is_object_safe_via_dyn_trait() {
310 let mut boxed: alloc::boxed::Box<dyn DataTaggingPlugin> =
312 alloc::boxed::Box::new(BuiltinDataTaggingPlugin::new());
313 boxed.set_tags([1; 16], vec![tag("a", "b")]);
314 assert_eq!(boxed.get_tags([1; 16]), vec![tag("a", "b")]);
315 assert_eq!(boxed.plugin_class_id(), "DDS:Tagging:Builtin");
316 }
317
318 #[test]
319 fn plugin_class_id_matches_spec_format() {
320 let p = BuiltinDataTaggingPlugin::new();
321 assert_eq!(p.plugin_class_id(), "DDS:Tagging:Builtin");
323 }
324}