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