Skip to main content

boxferry_model/
metadata.rs

1//! Bounded inert vendor metadata, independent of native configuration languages.
2use crate::{Identifier, ProtectedString, Sourced};
3use std::{error::Error, fmt};
4
5/// Maximum supported recursive metadata depth.
6pub const METADATA_MAX_DEPTH: usize = 64;
7/// Maximum total metadata text and node accounting bytes per application.
8pub const METADATA_MAX_BYTES: usize = 1024 * 1024;
9
10/// Owner of an inert vendor extension.
11#[derive(Clone, Debug, Eq, PartialEq)]
12#[non_exhaustive]
13pub enum MetadataScope {
14    /// Application-root extension.
15    Application,
16    /// Direct service extension, identified by its neutral service name.
17    Service(Identifier),
18}
19
20/// One ordered string-keyed mapping entry, including key provenance and sensitivity.
21#[derive(Clone, Debug, Eq, PartialEq)]
22pub struct MetadataEntry {
23    key: Sourced<ProtectedString>,
24    value: Sourced<MetadataNode>,
25}
26impl MetadataEntry {
27    /// Creates an ordered mapping entry.
28    #[must_use]
29    pub const fn new(key: Sourced<ProtectedString>, value: Sourced<MetadataNode>) -> Self {
30        Self { key, value }
31    }
32    /// Returns the original key and its source evidence.
33    #[must_use]
34    pub const fn key(&self) -> &Sourced<ProtectedString> {
35        &self.key
36    }
37    /// Returns the nested value and its source evidence.
38    #[must_use]
39    pub const fn value(&self) -> &Sourced<MetadataNode> {
40        &self.value
41    }
42}
43
44/// Semantic value of vendor metadata. Map and sequence order is retained.
45#[derive(Clone, Eq, PartialEq)]
46#[non_exhaustive]
47pub enum MetadataValue {
48    /// Null value.
49    Null,
50    /// Boolean value.
51    Boolean(bool),
52    /// JSON-compatible decimal number spelling, without floating-point conversion.
53    Number(ProtectedString),
54    /// Semantic string value.
55    String(ProtectedString),
56    /// Ordered nested sequence.
57    Sequence(Vec<Sourced<MetadataNode>>),
58    /// Ordered string-keyed mapping.
59    Mapping(Vec<MetadataEntry>),
60}
61
62impl fmt::Debug for MetadataValue {
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        match self {
65            Self::Null => f.write_str("Null"),
66            // A directly extracted enum does not carry its enclosing node's sensitivity bit.
67            Self::Boolean(_) => f.write_str("Boolean([REDACTED])"),
68            Self::Number(value) => f.debug_tuple("Number").field(value).finish(),
69            Self::String(value) => f.debug_tuple("String").field(value).finish(),
70            Self::Sequence(values) => f.debug_tuple("Sequence").field(values).finish(),
71            Self::Mapping(entries) => f.debug_tuple("Mapping").field(entries).finish(),
72        }
73    }
74}
75
76/// One value with its own sensitivity and source folded-text presentation hint.
77#[derive(Clone, Eq, PartialEq)]
78pub struct MetadataNode {
79    value: MetadataValue,
80    sensitive: bool,
81    folded: bool,
82}
83impl MetadataNode {
84    /// Creates a bounded node. The folded hint is valid only for strings.
85    /// Protecting a collection also protects every nested value and mapping key.
86    ///
87    /// # Errors
88    /// Rejects invalid decimal spellings, duplicate mapping keys, invalid folded hints and limits.
89    pub fn new(value: MetadataValue, sensitive: bool, folded: bool) -> Result<Self, MetadataError> {
90        let mut node = Self {
91            value,
92            sensitive,
93            folded,
94        };
95        node.measure(1)?;
96        if node.is_sensitive() {
97            node.protect_contents();
98        }
99        Ok(node)
100    }
101    /// Returns the typed semantic value.
102    #[must_use]
103    pub const fn value(&self) -> &MetadataValue {
104        &self.value
105    }
106    /// Returns the sensitivity of this node, independently of descendants.
107    #[must_use]
108    pub fn is_sensitive(&self) -> bool {
109        self.sensitive
110            || matches!(&self.value, MetadataValue::String(value) | MetadataValue::Number(value) if value.is_sensitive())
111    }
112    /// Returns whether the source requested folded text presentation.
113    #[must_use]
114    pub const fn is_folded(&self) -> bool {
115        self.folded
116    }
117    /// Returns whether this node, any descendant or mapping key is protected.
118    #[must_use]
119    pub fn contains_sensitive(&self) -> bool {
120        self.is_sensitive()
121            || match &self.value {
122                MetadataValue::Sequence(values) => values.iter().any(|value| value.value().contains_sensitive()),
123                MetadataValue::Mapping(entries) => entries
124                    .iter()
125                    .any(|entry| entry.key.value().is_sensitive() || entry.value.value().contains_sensitive()),
126                _ => false,
127            }
128    }
129    fn protect_contents(&mut self) {
130        self.sensitive = true;
131        match &mut self.value {
132            MetadataValue::String(value) | MetadataValue::Number(value) => {
133                *value = ProtectedString::sensitive(value.expose());
134            }
135            MetadataValue::Sequence(values) => {
136                for value in values {
137                    value.value_mut().protect_contents();
138                }
139            }
140            MetadataValue::Mapping(entries) => {
141                for entry in entries {
142                    *entry.key.value_mut() = ProtectedString::sensitive(entry.key.value().expose());
143                    entry.value.value_mut().protect_contents();
144                }
145            }
146            MetadataValue::Null | MetadataValue::Boolean(_) => {}
147        }
148    }
149
150    pub(crate) fn measure(&self, depth: usize) -> Result<usize, MetadataError> {
151        if depth > METADATA_MAX_DEPTH || (self.folded && !matches!(self.value, MetadataValue::String(_))) {
152            return Err(MetadataError::InvalidValue);
153        }
154        let mut bytes = 32_usize;
155        match &self.value {
156            MetadataValue::Number(value) => {
157                if !valid_number(value.expose()) {
158                    return Err(MetadataError::InvalidValue);
159                }
160                bytes += value.expose().len();
161            }
162            MetadataValue::String(value) => bytes += value.expose().len(),
163            MetadataValue::Sequence(values) => {
164                for value in values {
165                    bytes = bytes
166                        .checked_add(value.value().measure(depth + 1)?)
167                        .ok_or(MetadataError::Limit)?;
168                }
169            }
170            MetadataValue::Mapping(entries) => {
171                let mut keys = std::collections::BTreeSet::new();
172                for entry in entries {
173                    if !keys.insert(entry.key.value().expose()) {
174                        return Err(MetadataError::Duplicate);
175                    }
176                    bytes = bytes
177                        .checked_add(entry.key.value().expose().len())
178                        .and_then(|bytes| bytes.checked_add(32))
179                        .ok_or(MetadataError::Limit)?;
180                    bytes = bytes
181                        .checked_add(entry.value.value().measure(depth + 1)?)
182                        .ok_or(MetadataError::Limit)?;
183                }
184            }
185            _ => {}
186        }
187        if bytes > METADATA_MAX_BYTES {
188            return Err(MetadataError::Limit);
189        }
190        Ok(bytes)
191    }
192}
193impl fmt::Debug for MetadataNode {
194    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195        if self.is_sensitive() {
196            f.write_str("MetadataNode([REDACTED])")
197        } else {
198            f.debug_struct("MetadataNode")
199                .field("value", &self.value)
200                .field("sensitive", &self.sensitive)
201                .field("folded", &self.folded)
202                .finish()
203        }
204    }
205}
206
207/// One exact, case-sensitive vendor namespace at a supported neutral owner.
208#[derive(Clone, Debug, Eq, PartialEq)]
209pub struct VendorMetadata {
210    scope: MetadataScope,
211    namespace: Sourced<ProtectedString>,
212    payload: Sourced<MetadataNode>,
213}
214impl VendorMetadata {
215    /// Creates an inert extension. Namespace validation does not derive a printable target section.
216    ///
217    /// # Errors
218    /// Rejects empty/non-extension namespace spellings and bounded payload violations.
219    pub fn new(
220        scope: MetadataScope,
221        namespace: Sourced<ProtectedString>,
222        payload: Sourced<MetadataNode>,
223    ) -> Result<Self, MetadataError> {
224        if !namespace.value().expose().starts_with("x-") || namespace.value().expose().len() <= 2 {
225            return Err(MetadataError::InvalidValue);
226        }
227        payload.value().measure(1)?;
228        Ok(Self {
229            scope,
230            namespace,
231            payload,
232        })
233    }
234    /// Returns the owner.
235    #[must_use]
236    pub const fn scope(&self) -> &MetadataScope {
237        &self.scope
238    }
239    /// Returns the exact original namespace and source evidence.
240    #[must_use]
241    pub const fn namespace(&self) -> &Sourced<ProtectedString> {
242        &self.namespace
243    }
244    /// Returns the inert semantic tree.
245    #[must_use]
246    pub const fn payload(&self) -> &Sourced<MetadataNode> {
247        &self.payload
248    }
249    /// Returns whether any namespace, key or value is protected.
250    #[must_use]
251    pub fn contains_sensitive(&self) -> bool {
252        self.namespace.value().is_sensitive() || self.payload.value().contains_sensitive()
253    }
254    pub(crate) fn measure(&self) -> Result<usize, MetadataError> {
255        self.payload
256            .value()
257            .measure(1)?
258            .checked_add(self.namespace.value().expose().len())
259            .ok_or(MetadataError::Limit)
260    }
261}
262
263/// Value-free validation error for inert metadata.
264#[derive(Clone, Copy, Debug, Eq, PartialEq)]
265#[non_exhaustive]
266pub enum MetadataError {
267    /// A shape, number, namespace or presentation hint is invalid.
268    InvalidValue,
269    /// A case-sensitive identity or map key occurs twice.
270    Duplicate,
271    /// Service owner does not exist.
272    UnknownOwner,
273    /// A finite size or recursion budget was exceeded.
274    Limit,
275}
276impl fmt::Display for MetadataError {
277    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
278        f.write_str(match self {
279            Self::InvalidValue => "invalid vendor metadata value",
280            Self::Duplicate => "duplicate vendor metadata identity or key",
281            Self::UnknownOwner => "vendor metadata references an absent service",
282            Self::Limit => "vendor metadata exceeds supported bounds",
283        })
284    }
285}
286impl Error for MetadataError {}
287
288fn valid_number(text: &str) -> bool {
289    let bytes = text.as_bytes();
290    let mut i = usize::from(bytes.first() == Some(&b'-'));
291    match bytes.get(i) {
292        Some(b'0') => i += 1,
293        Some(b'1'..=b'9') => {
294            i += 1;
295            while bytes.get(i).is_some_and(u8::is_ascii_digit) {
296                i += 1;
297            }
298        }
299        _ => return false,
300    }
301    if bytes.get(i) == Some(&b'.') {
302        i += 1;
303        let start = i;
304        while bytes.get(i).is_some_and(u8::is_ascii_digit) {
305            i += 1;
306        }
307        if start == i {
308            return false;
309        }
310    }
311    if matches!(bytes.get(i), Some(b'e' | b'E')) {
312        i += 1;
313        if matches!(bytes.get(i), Some(b'+' | b'-')) {
314            i += 1;
315        }
316        let start = i;
317        while bytes.get(i).is_some_and(u8::is_ascii_digit) {
318            i += 1;
319        }
320        if start == i {
321            return false;
322        }
323    }
324    i == bytes.len()
325}