boxferry_model/
metadata.rs1use crate::{Identifier, ProtectedString, Sourced};
3use std::{error::Error, fmt};
4
5pub const METADATA_MAX_DEPTH: usize = 64;
7pub const METADATA_MAX_BYTES: usize = 1024 * 1024;
9
10#[derive(Clone, Debug, Eq, PartialEq)]
12#[non_exhaustive]
13pub enum MetadataScope {
14 Application,
16 Service(Identifier),
18}
19
20#[derive(Clone, Debug, Eq, PartialEq)]
22pub struct MetadataEntry {
23 key: Sourced<ProtectedString>,
24 value: Sourced<MetadataNode>,
25}
26impl MetadataEntry {
27 #[must_use]
29 pub const fn new(key: Sourced<ProtectedString>, value: Sourced<MetadataNode>) -> Self {
30 Self { key, value }
31 }
32 #[must_use]
34 pub const fn key(&self) -> &Sourced<ProtectedString> {
35 &self.key
36 }
37 #[must_use]
39 pub const fn value(&self) -> &Sourced<MetadataNode> {
40 &self.value
41 }
42}
43
44#[derive(Clone, Eq, PartialEq)]
46#[non_exhaustive]
47pub enum MetadataValue {
48 Null,
50 Boolean(bool),
52 Number(ProtectedString),
54 String(ProtectedString),
56 Sequence(Vec<Sourced<MetadataNode>>),
58 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 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#[derive(Clone, Eq, PartialEq)]
78pub struct MetadataNode {
79 value: MetadataValue,
80 sensitive: bool,
81 folded: bool,
82}
83impl MetadataNode {
84 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 #[must_use]
103 pub const fn value(&self) -> &MetadataValue {
104 &self.value
105 }
106 #[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 #[must_use]
114 pub const fn is_folded(&self) -> bool {
115 self.folded
116 }
117 #[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#[derive(Clone, Debug, Eq, PartialEq)]
209pub struct VendorMetadata {
210 scope: MetadataScope,
211 namespace: Sourced<ProtectedString>,
212 payload: Sourced<MetadataNode>,
213}
214impl VendorMetadata {
215 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 #[must_use]
236 pub const fn scope(&self) -> &MetadataScope {
237 &self.scope
238 }
239 #[must_use]
241 pub const fn namespace(&self) -> &Sourced<ProtectedString> {
242 &self.namespace
243 }
244 #[must_use]
246 pub const fn payload(&self) -> &Sourced<MetadataNode> {
247 &self.payload
248 }
249 #[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
265#[non_exhaustive]
266pub enum MetadataError {
267 InvalidValue,
269 Duplicate,
271 UnknownOwner,
273 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}