Skip to main content

zerodds_security/
properties.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! Property list — name/value pairs for plugin configuration.
5//!
6//! Spec OMG DDS-Security 1.1 §8.2.1 `Property_t` + `PropertyQosPolicy`.
7//! Properties are created on the participant and passed through to the
8//! plugins (e.g. certificate paths, HSM URIs, cipher suites).
9
10extern crate alloc;
11
12use alloc::borrow::Cow;
13use alloc::vec::Vec;
14
15/// A single property: name + value.
16///
17/// `propagate=true` is sent to remote participants via SEDP (e.g.
18/// `dds.sec.permissions_hash`); `false` stays local (e.g.
19/// `dds.sec.auth.private_key_path` — never over the wire!).
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct Property {
22    /// Key (reverse-DNS convention, `dds.sec.xyz`).
23    pub name: Cow<'static, str>,
24    /// Value (opaque UTF-8 string; interpreted by the plugin).
25    pub value: Cow<'static, str>,
26    /// `true` → propagated via SEDP. Default `false` — never
27    /// leak secret strings to remote.
28    pub propagate: bool,
29}
30
31impl Property {
32    /// Constructor: local, not propagated (default).
33    #[must_use]
34    pub fn local(name: impl Into<Cow<'static, str>>, value: impl Into<Cow<'static, str>>) -> Self {
35        Self {
36            name: name.into(),
37            value: value.into(),
38            propagate: false,
39        }
40    }
41
42    /// Constructor: propagated via SEDP.
43    #[must_use]
44    pub fn propagated(
45        name: impl Into<Cow<'static, str>>,
46        value: impl Into<Cow<'static, str>>,
47    ) -> Self {
48        Self {
49            name: name.into(),
50            value: value.into(),
51            propagate: true,
52        }
53    }
54}
55
56/// List of properties. Order does not matter, names must be unique
57/// — on a duplicate the last entry wins (as per OMG spec).
58#[derive(Debug, Clone, Default, PartialEq, Eq)]
59pub struct PropertyList {
60    entries: Vec<Property>,
61}
62
63impl PropertyList {
64    /// Empty list.
65    #[must_use]
66    pub fn new() -> Self {
67        Self::default()
68    }
69
70    /// Adds a property (or replaces it, if the name already exists).
71    pub fn set(&mut self, p: Property) {
72        if let Some(existing) = self.entries.iter_mut().find(|e| e.name == p.name) {
73            *existing = p;
74        } else {
75            self.entries.push(p);
76        }
77    }
78
79    /// Builder variant of `set`.
80    #[must_use]
81    pub fn with(mut self, p: Property) -> Self {
82        self.set(p);
83        self
84    }
85
86    /// Returns the value for a name.
87    #[must_use]
88    pub fn get(&self, name: &str) -> Option<&str> {
89        self.entries
90            .iter()
91            .find(|p| p.name == name)
92            .map(|p| p.value.as_ref())
93    }
94
95    /// All properties (read-only).
96    #[must_use]
97    pub fn entries(&self) -> &[Property] {
98        &self.entries
99    }
100
101    /// Only propagatable properties (for the SEDP wire).
102    pub fn propagatable(&self) -> impl Iterator<Item = &Property> {
103        self.entries.iter().filter(|p| p.propagate)
104    }
105}
106
107#[cfg(test)]
108#[allow(clippy::unwrap_used)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn set_replaces_duplicate() {
114        let mut list = PropertyList::new();
115        list.set(Property::local("k", "v1"));
116        list.set(Property::local("k", "v2"));
117        assert_eq!(list.entries().len(), 1);
118        assert_eq!(list.get("k"), Some("v2"));
119    }
120
121    #[test]
122    fn propagatable_filter() {
123        let list = PropertyList::new()
124            .with(Property::local("secret", "sensitive"))
125            .with(Property::propagated("public", "hashed"));
126        let propagated: Vec<_> = list.propagatable().collect();
127        assert_eq!(propagated.len(), 1);
128        assert_eq!(propagated[0].name, "public");
129    }
130}