Skip to main content

blueprint_core/
metadata.rs

1//! Metadata that can be included in a [`JobCall`] or [`JobResult`] to provide additional context.
2
3use core::convert::Infallible;
4use core::fmt;
5use core::str::FromStr;
6
7use alloc::borrow::Cow;
8use alloc::collections::BTreeMap;
9use alloc::collections::btree_map::{Entry, Iter, IterMut};
10use alloc::string::String;
11use alloc::vec::Vec;
12use bytes::{Bytes, BytesMut};
13
14/// A typed metadata map
15///
16/// # Type Parameters
17///
18/// * `T`: The type of values stored in the metadata map.
19#[derive(Clone, Debug, Default, PartialEq)]
20pub struct MetadataMap<T> {
21    map: BTreeMap<Cow<'static, str>, T>,
22}
23
24impl<T> MetadataMap<T> {
25    /// Creates a new empty `MetadataMap`.
26    pub fn new() -> Self {
27        Self {
28            map: BTreeMap::new(),
29        }
30    }
31
32    /// Returns the number of elements in the map.
33    pub fn len(&self) -> usize {
34        self.map.len()
35    }
36
37    /// Returns `true` if the map is empty.
38    pub fn is_empty(&self) -> bool {
39        self.map.is_empty()
40    }
41
42    /// Inserts a key-value pair into the map.
43    ///
44    /// If the map did not have this key present, [`None`] is returned.
45    ///
46    /// If the map did have this key present, the value is updated, and the old
47    /// value is returned. The key is not updated, though; it is retained as
48    /// is (verbatim).
49    pub fn insert<K, V>(&mut self, key: K, value: V) -> Option<T>
50    where
51        K: Into<Cow<'static, str>>,
52        V: Into<T>,
53    {
54        self.map.insert(key.into(), value.into())
55    }
56
57    /// Gets a reference to the value associated with the given key.
58    pub fn get<K>(&self, key: K) -> Option<&T>
59    where
60        K: AsRef<str>,
61    {
62        self.map.get(key.as_ref())
63    }
64
65    /// Gets a mutable reference to the value associated with the given key.
66    pub fn get_mut<K>(&mut self, key: &K) -> Option<&mut T>
67    where
68        K: AsRef<str>,
69    {
70        self.map.get_mut(key.as_ref())
71    }
72
73    /// Removes a key from the map, returning the value at the key if the key
74    /// was previously in the map.
75    pub fn remove<K>(&mut self, key: &K) -> Option<T>
76    where
77        K: AsRef<str>,
78    {
79        self.map.remove(key.as_ref())
80    }
81
82    /// Clears the map, removing all key-value pairs. Keeps the allocated memory for reuse.
83    pub fn clear(&mut self) {
84        self.map.clear();
85    }
86
87    /// Returns an iterator over the map's entries.
88    #[allow(clippy::iter_without_into_iter)] // IntoIterator impl deferred to avoid API churn
89    pub fn iter(&self) -> Iter<'_, Cow<'static, str>, T> {
90        self.map.iter()
91    }
92
93    /// Returns a mutable iterator over the map's entries.
94    #[allow(clippy::iter_without_into_iter)] // IntoIterator impl deferred to avoid API churn
95    pub fn iter_mut(&mut self) -> IterMut<'_, Cow<'static, str>, T> {
96        self.map.iter_mut()
97    }
98
99    /// Provides a view into a single entry in the map, which may or may not be present.
100    pub fn entry<K>(&mut self, key: K) -> Entry<'_, Cow<'static, str>, T>
101    where
102        K: Into<Cow<'static, str>>,
103    {
104        self.map.entry(key.into())
105    }
106
107    /// Extends the map with the key-value pairs from the given map.
108    pub fn extend(&mut self, other: Self) {
109        self.map.extend(other.map);
110    }
111}
112
113/// Represents a [`JobCall`] metadata field value.
114///
115/// To handle this, the `MetadataValue` is usable as a type and can be compared
116/// with strings and implements `Debug`. A `to_str` method is provided that returns
117/// an `Err` if the metadata value contains non-visible ASCII characters.
118#[derive(Clone, Default)]
119pub struct MetadataValue {
120    inner: Bytes,
121    is_sensitive: bool,
122}
123
124impl FromStr for MetadataValue {
125    type Err = Infallible;
126
127    fn from_str(s: &str) -> Result<Self, Self::Err> {
128        Ok(Self {
129            inner: Bytes::copy_from_slice(s.as_bytes()),
130            is_sensitive: false,
131        })
132    }
133}
134
135impl MetadataValue {
136    /// Create a new `MetadataValue` from a string.
137    pub fn from_bytes(value: Bytes) -> Self {
138        Self {
139            inner: value,
140            is_sensitive: false,
141        }
142    }
143
144    /// Create a new `MetadataValue` from a string.
145    pub fn from_sensitive_str(value: &str) -> Self {
146        Self {
147            inner: Bytes::copy_from_slice(value.as_bytes()),
148            is_sensitive: true,
149        }
150    }
151
152    /// Create a new `MetadataValue` from a string.
153    pub fn from_sensitive_bytes(value: Bytes) -> Self {
154        Self {
155            inner: value,
156            is_sensitive: true,
157        }
158    }
159
160    /// Returns true if the metadata value is sensitive.
161    pub fn is_sensitive(&self) -> bool {
162        self.is_sensitive
163    }
164
165    /// Returns the length of the metadata value.
166    pub fn len(&self) -> usize {
167        self.inner.len()
168    }
169
170    /// Returns true if the metadata value is empty.
171    pub fn is_empty(&self) -> bool {
172        self.inner.is_empty()
173    }
174
175    /// Converts the metadata value into a string if it contains valid UTF-8.
176    pub fn to_str(&self) -> Result<&str, core::str::Utf8Error> {
177        core::str::from_utf8(&self.inner)
178    }
179
180    /// Converts the metadata value into bytes.
181    pub fn into_bytes(self) -> Bytes {
182        self.inner
183    }
184
185    pub fn as_bytes(&self) -> &[u8] {
186        &self.inner[..]
187    }
188}
189
190impl AsRef<[u8]> for MetadataValue {
191    #[inline]
192    fn as_ref(&self) -> &[u8] {
193        self.inner.as_ref()
194    }
195}
196
197impl fmt::Debug for MetadataValue {
198    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199        if self.is_sensitive {
200            f.write_str("Sensitive")
201        } else {
202            f.write_str("\"")?;
203            let mut from = 0;
204            let bytes = self.as_bytes();
205            for (i, &b) in bytes.iter().enumerate() {
206                if !is_visible_ascii(b) || b == b'"' {
207                    if from != i {
208                        f.write_str(
209                            core::str::from_utf8(&bytes[from..i]).map_err(|_| fmt::Error)?,
210                        )?;
211                    }
212                    if b == b'"' {
213                        f.write_str("\\\"")?;
214                    } else {
215                        write!(f, "\\x{b:x}")?;
216                    }
217                    from = i + 1;
218                }
219            }
220
221            if from != bytes.len() {
222                f.write_str(core::str::from_utf8(&bytes[from..]).map_err(|_| fmt::Error)?)?;
223            }
224            f.write_str("\"")
225        }
226    }
227}
228
229impl From<&str> for MetadataValue {
230    fn from(value: &str) -> Self {
231        Self::from_str(value).unwrap()
232    }
233}
234
235impl From<Bytes> for MetadataValue {
236    fn from(value: Bytes) -> Self {
237        Self::from_bytes(value)
238    }
239}
240
241impl From<&Bytes> for MetadataValue {
242    fn from(value: &Bytes) -> Self {
243        Self::from_bytes(value.clone())
244    }
245}
246
247impl From<BytesMut> for MetadataValue {
248    fn from(value: BytesMut) -> Self {
249        Self::from_bytes(value.freeze())
250    }
251}
252
253impl From<&BytesMut> for MetadataValue {
254    fn from(value: &BytesMut) -> Self {
255        Self::from_bytes(value.clone().freeze())
256    }
257}
258
259impl From<String> for MetadataValue {
260    fn from(value: String) -> Self {
261        Self::from_str(&value).unwrap()
262    }
263}
264
265impl From<&String> for MetadataValue {
266    fn from(value: &String) -> Self {
267        Self::from_str(value).unwrap()
268    }
269}
270
271impl From<&[u8]> for MetadataValue {
272    fn from(value: &[u8]) -> Self {
273        Self::from_bytes(Bytes::copy_from_slice(value))
274    }
275}
276
277impl From<Vec<u8>> for MetadataValue {
278    fn from(value: Vec<u8>) -> Self {
279        Self::from_bytes(Bytes::from(value))
280    }
281}
282
283impl From<&Vec<u8>> for MetadataValue {
284    fn from(value: &Vec<u8>) -> Self {
285        Self::from_bytes(Bytes::copy_from_slice(value))
286    }
287}
288
289impl<const N: usize> From<[u8; N]> for MetadataValue {
290    fn from(value: [u8; N]) -> Self {
291        Self::from_bytes(Bytes::copy_from_slice(&value))
292    }
293}
294
295impl<const N: usize> From<&[u8; N]> for MetadataValue {
296    fn from(value: &[u8; N]) -> Self {
297        Self::from_bytes(Bytes::copy_from_slice(value))
298    }
299}
300
301macro_rules! impl_from_numbers {
302    ($($t:ty),*) => {
303        $(
304            /// Converts a number into a metadata value by converting it to a big-endian byte array.
305            impl From<$t> for MetadataValue {
306                fn from(value: $t) -> Self {
307                    Self::from_bytes(Bytes::copy_from_slice(&value.to_be_bytes()))
308                }
309            }
310
311            impl From<&$t> for MetadataValue {
312                fn from(value: &$t) -> Self {
313                    Self::from_bytes(Bytes::copy_from_slice(&value.to_be_bytes()))
314                }
315            }
316        )*
317
318    };
319}
320
321macro_rules! impl_try_from_metadata_for_numbers {
322    ($($t:ty),*) => {
323        $(
324            /// Tries to convert a metadata value into a number by parsing it as a big-endian byte array.
325            impl core::convert::TryFrom<MetadataValue> for $t {
326                type Error = core::array::TryFromSliceError;
327
328                fn try_from(value: MetadataValue) -> Result<Self, Self::Error> {
329                    let bytes = value.as_bytes();
330                    let arr: [u8; core::mem::size_of::<Self>()] = bytes.try_into()?;
331                    Ok(Self::from_be_bytes(arr))
332                }
333            }
334
335            /// Tries to convert a metadata value into a number by parsing it as a big-endian byte array.
336            impl core::convert::TryFrom<&MetadataValue> for $t {
337                type Error = core::array::TryFromSliceError;
338
339                fn try_from(value: &MetadataValue) -> Result<Self, Self::Error> {
340                    let bytes = value.as_bytes();
341                    let arr: [u8; core::mem::size_of::<Self>()] = bytes.try_into()?;
342                    Ok(Self::from_be_bytes(arr))
343                }
344            }
345        )*
346    };
347}
348
349impl_from_numbers! { u16, u32, u64, u128, usize, i16, i32, i64, i128, isize }
350impl_try_from_metadata_for_numbers! { u16, u32, u64, u128, usize, i16, i32, i64, i128, isize }
351
352const fn is_visible_ascii(b: u8) -> bool {
353    b >= 32 && b < 127 || b == b'\t'
354}
355
356#[cfg(test)]
357mod tests {
358    use super::MetadataValue;
359
360    #[test]
361    fn numeric_try_from_rejects_invalid_byte_lengths() {
362        let value = MetadataValue::from([1u8]);
363        assert!(u64::try_from(value.clone()).is_err());
364        assert!(u64::try_from(&value).is_err());
365    }
366}