Skip to main content

loonfs_api/
attributes.rs

1//! Validated key-value attributes stored on inodes.
2
3use crate::ids::{numeric_id, string_id, validation_error};
4use serde::{Deserialize, Serialize};
5use std::collections::BTreeMap;
6use std::fmt;
7use thiserror::Error;
8
9/// Maximum attribute key length in UTF-8 bytes.
10pub const MAX_ATTRIBUTE_KEY_BYTES: usize = 128;
11/// Maximum length of one attribute value in UTF-8 bytes.
12pub const MAX_ATTRIBUTE_VALUE_BYTES: usize = 4096;
13/// Maximum number of entries in one attribute map.
14pub const MAX_ATTRIBUTE_ENTRIES: usize = 100;
15/// Maximum total size of one attribute map in logical UTF-8 bytes. The total
16/// counts every key's bytes plus every value's bytes. It excludes encoder
17/// framing, so the limit does not move when the map is written as JSON instead
18/// of CBOR.
19pub const MAX_ATTRIBUTES_TOTAL_BYTES: usize = 65_536;
20
21/// Key prefix reserved for system-owned attributes.
22pub const RESERVED_ATTRIBUTE_KEY_PREFIX: &str = "loonfs.";
23
24validation_error!(
25    AttributeKeyValidationError,
26    "invalid attribute key {value:?}: {reason}"
27);
28
29string_id! {
30    /// A validated inode attribute name from 1 to 128 UTF-8 bytes without control characters.
31    ///
32    /// Names are byte-exact; writes reject the reserved `loonfs.` prefix even though
33    /// the type accepts it.
34    AttributeKey,
35    error = AttributeKeyValidationError,
36    validate = validate_attribute_key,
37    schema(example = "owner")
38}
39
40impl AttributeKey {
41    /// Reports whether this key names a system-owned attribute.
42    pub fn is_reserved(&self) -> bool {
43        self.as_str().starts_with(RESERVED_ATTRIBUTE_KEY_PREFIX)
44    }
45}
46
47fn validate_attribute_key(value: &str) -> Result<(), AttributeKeyValidationError> {
48    if value.is_empty() {
49        return Err(AttributeKeyValidationError::new(value, "must not be empty"));
50    }
51    if value.len() > MAX_ATTRIBUTE_KEY_BYTES {
52        // An oversized or hostile key must not ride along in error payloads
53        // that serialize onto the wire. The length check runs before the
54        // character check so that no oversized key reaches an error that does
55        // echo its input.
56        return Err(AttributeKeyValidationError::new(
57            "",
58            format!("exceeds the maximum attribute key length of {MAX_ATTRIBUTE_KEY_BYTES} bytes"),
59        ));
60    }
61    if value.chars().any(char::is_control) {
62        return Err(AttributeKeyValidationError::new(
63            value,
64            "must not contain control characters",
65        ));
66    }
67    Ok(())
68}
69
70validation_error!(
71    AttributeValueValidationError,
72    "invalid attribute value: {reason}"
73);
74
75string_id! {
76    /// A validated inode attribute value of at most [`MAX_ATTRIBUTE_VALUE_BYTES`] UTF-8 bytes.
77    ///
78    /// Empty strings and control characters are valid, and only an explicit remove
79    /// operation deletes an attribute.
80    AttributeValue,
81    error = AttributeValueValidationError,
82    validate = validate_attribute_value,
83    schema(example = "platform")
84}
85
86fn validate_attribute_value(value: &str) -> Result<(), AttributeValueValidationError> {
87    if value.len() > MAX_ATTRIBUTE_VALUE_BYTES {
88        return Err(AttributeValueValidationError::new(
89            "",
90            format!(
91                "exceeds the maximum attribute value length of {MAX_ATTRIBUTE_VALUE_BYTES} bytes"
92            ),
93        ));
94    }
95    Ok(())
96}
97
98impl AttributeValue {
99    /// Returns this value's logical size in UTF-8 bytes.
100    ///
101    /// The value counts its own bytes and no encoder framing.
102    pub fn logical_bytes(&self) -> usize {
103        self.as_str().len()
104    }
105}
106
107/// A validated attribute map limited to [`MAX_ATTRIBUTE_ENTRIES`] entries and
108/// [`MAX_ATTRIBUTES_TOTAL_BYTES`] total key and value UTF-8 bytes.
109///
110/// Construction and decoding reject values over these limits; an empty map
111/// represents cleared attributes.
112#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
113#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
114#[cfg_attr(
115    feature = "openapi",
116    schema(value_type = std::collections::BTreeMap<String, AttributeValue>)
117)]
118#[serde(transparent)]
119pub struct Attributes(BTreeMap<AttributeKey, AttributeValue>);
120
121impl Attributes {
122    /// Creates a map after checking every limit.
123    pub fn new(entries: BTreeMap<AttributeKey, AttributeValue>) -> Result<Self, AttributesError> {
124        if entries.len() > MAX_ATTRIBUTE_ENTRIES {
125            return Err(AttributesError::TooManyEntries {
126                entries: entries.len(),
127            });
128        }
129        let total_bytes = logical_bytes_of(&entries);
130        if total_bytes > MAX_ATTRIBUTES_TOTAL_BYTES {
131            return Err(AttributesError::TooLarge { total_bytes });
132        }
133        Ok(Self(entries))
134    }
135
136    /// Returns the value stored under a key.
137    pub fn get(&self, key: &AttributeKey) -> Option<&AttributeValue> {
138        self.0.get(key)
139    }
140
141    /// Iterates the entries in key order.
142    pub fn iter(&self) -> impl Iterator<Item = (&AttributeKey, &AttributeValue)> {
143        self.0.iter()
144    }
145
146    /// Returns the number of entries.
147    pub fn len(&self) -> usize {
148        self.0.len()
149    }
150
151    /// Reports whether the map holds no entries.
152    pub fn is_empty(&self) -> bool {
153        self.0.is_empty()
154    }
155
156    /// Returns the entries.
157    pub fn as_map(&self) -> &BTreeMap<AttributeKey, AttributeValue> {
158        &self.0
159    }
160
161    /// Returns the map's total logical size in UTF-8 bytes.
162    pub fn logical_bytes(&self) -> usize {
163        logical_bytes_of(&self.0)
164    }
165}
166
167impl TryFrom<BTreeMap<AttributeKey, AttributeValue>> for Attributes {
168    type Error = AttributesError;
169
170    fn try_from(entries: BTreeMap<AttributeKey, AttributeValue>) -> Result<Self, Self::Error> {
171        Self::new(entries)
172    }
173}
174
175impl From<Attributes> for BTreeMap<AttributeKey, AttributeValue> {
176    fn from(attributes: Attributes) -> Self {
177        attributes.0
178    }
179}
180
181impl<'de> Deserialize<'de> for Attributes {
182    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
183    where
184        D: serde::Deserializer<'de>,
185    {
186        let entries = BTreeMap::<AttributeKey, AttributeValue>::deserialize(deserializer)?;
187        Self::new(entries).map_err(serde::de::Error::custom)
188    }
189}
190
191fn logical_bytes_of(entries: &BTreeMap<AttributeKey, AttributeValue>) -> usize {
192    entries
193        .iter()
194        .map(|(key, value)| key.as_str().len() + value.logical_bytes())
195        .sum()
196}
197
198/// Describes which attribute-map limit an input broke.
199#[derive(Debug, Clone, PartialEq, Eq, Error)]
200pub enum AttributesError {
201    /// The map holds more entries than the limit allows.
202    #[error("attribute map holds {entries} entries, which exceeds the maximum of {MAX_ATTRIBUTE_ENTRIES}")]
203    TooManyEntries {
204        /// Number of entries the rejected map held.
205        entries: usize,
206    },
207    /// The whole map is larger than the limit allows.
208    #[error("attribute map holds {total_bytes} logical bytes, which exceeds the maximum of {MAX_ATTRIBUTES_TOTAL_BYTES} bytes")]
209    TooLarge {
210        /// Total logical size in UTF-8 bytes of the rejected map.
211        total_bytes: usize,
212    },
213}
214
215numeric_id! {
216    /// The revision number for an inode's attribute map.
217    ///
218    /// It starts at zero and increases when the map changes; clients can guard
219    /// updates but cannot query earlier maps.
220    AttributeRevisionNo,
221    public_ordinal,
222    schema_description = "Revision number for an inode's attributes. It starts at 0 and increases whenever the attribute map changes."
223}
224
225#[cfg(test)]
226mod tests {
227    use super::{
228        AttributeKey, AttributeRevisionNo, AttributeValue, Attributes, AttributesError,
229        MAX_ATTRIBUTES_TOTAL_BYTES, MAX_ATTRIBUTE_ENTRIES, MAX_ATTRIBUTE_KEY_BYTES,
230        MAX_ATTRIBUTE_VALUE_BYTES,
231    };
232    use crate::RevisionNo;
233    use std::collections::BTreeMap;
234
235    fn key(value: &str) -> AttributeKey {
236        AttributeKey::parse(value).expect("valid attribute key")
237    }
238
239    fn string_value(bytes: usize) -> AttributeValue {
240        AttributeValue::parse("v".repeat(bytes)).expect("valid attribute value")
241    }
242
243    fn value(value: &str) -> AttributeValue {
244        AttributeValue::parse(value).expect("valid attribute value")
245    }
246
247    fn map(entries: impl IntoIterator<Item = (AttributeKey, AttributeValue)>) -> Attributes {
248        Attributes::new(entries.into_iter().collect()).expect("valid attribute map")
249    }
250
251    fn map_error(
252        entries: impl IntoIterator<Item = (AttributeKey, AttributeValue)>,
253    ) -> AttributesError {
254        Attributes::new(entries.into_iter().collect()).expect_err("invalid attribute map")
255    }
256
257    #[test]
258    fn attribute_key_accepts_the_allowed_grammar() {
259        for value in [
260            "a",
261            &"k".repeat(MAX_ATTRIBUTE_KEY_BYTES),
262            "user.tag",
263            "Case.Sensitive",
264        ] {
265            assert_eq!(key(value).as_str(), value);
266        }
267    }
268
269    #[test]
270    fn attribute_key_counts_length_in_utf8_bytes() {
271        // Four bytes per character, so 32 characters is exactly the cap and
272        // 33 is over it.
273        let at_cap = "🐧".repeat(MAX_ATTRIBUTE_KEY_BYTES / 4);
274        assert_eq!(at_cap.len(), MAX_ATTRIBUTE_KEY_BYTES);
275        assert_eq!(key(&at_cap).as_str(), at_cap);
276        assert!(AttributeKey::parse(format!("{at_cap}🐧")).is_err());
277    }
278
279    #[test]
280    fn attribute_key_rejects_invalid_values() {
281        assert_eq!(
282            AttributeKey::parse("").expect_err("empty").reason(),
283            "must not be empty"
284        );
285        assert_eq!(
286            AttributeKey::parse("a\u{0}b").expect_err("nul").reason(),
287            "must not contain control characters"
288        );
289        assert_eq!(
290            AttributeKey::parse("a\u{7}b")
291                .expect_err("control")
292                .reason(),
293            "must not contain control characters"
294        );
295    }
296
297    #[test]
298    fn attribute_key_over_length_error_does_not_echo_the_key() {
299        let oversized = "k".repeat(MAX_ATTRIBUTE_KEY_BYTES + 1);
300        let error = AttributeKey::parse(&oversized).expect_err("over cap");
301
302        assert_eq!(error.value(), "");
303        assert_eq!(
304            error.reason(),
305            "exceeds the maximum attribute key length of 128 bytes"
306        );
307        assert!(!error.to_string().contains(&oversized));
308    }
309
310    #[test]
311    fn attribute_key_accepts_the_reserved_prefix() {
312        // The durable format has to carry system attributes, so the type
313        // admits them. Rejecting a caller's write of one is the write
314        // operation's job.
315        let reserved = key("loonfs.kind");
316
317        assert!(reserved.is_reserved());
318        assert!(!key("loonfs").is_reserved());
319        assert!(!key("user.loonfs.kind").is_reserved());
320    }
321
322    #[test]
323    fn attribute_value_serializes_as_a_bare_string() {
324        let value = value("hello");
325
326        assert_eq!(
327            serde_json::to_string(&value).expect("serialize attribute value"),
328            r#""hello""#
329        );
330        assert_eq!(
331            serde_json::from_str::<AttributeValue>(r#""hello""#)
332                .expect("deserialize attribute value"),
333            value
334        );
335    }
336
337    #[test]
338    fn attribute_value_rejects_the_old_tagged_shape() {
339        assert!(
340            serde_json::from_str::<AttributeValue>(r#"{"kind":"string","value":"hello"}"#).is_err()
341        );
342        assert!(serde_json::from_str::<AttributeValue>(
343            r#"{"kind":"string_list","values":["a","b"]}"#
344        )
345        .is_err());
346    }
347
348    #[test]
349    fn attribute_value_accepts_empty_and_free_text() {
350        for text in ["", "a\n\u{0}b", "draft,review", "café ☃ 日本語 🙂"] {
351            assert_eq!(value(text).as_str(), text);
352        }
353    }
354
355    #[test]
356    fn attribute_value_enforces_the_utf8_byte_cap_with_a_named_error() {
357        let at_cap = "🐧".repeat(MAX_ATTRIBUTE_VALUE_BYTES / 4);
358        assert_eq!(value(&at_cap).logical_bytes(), MAX_ATTRIBUTE_VALUE_BYTES);
359
360        let oversized = format!("{at_cap}🐧");
361        let error = AttributeValue::parse(&oversized).expect_err("over cap");
362        assert_eq!(error.value(), "");
363        assert_eq!(
364            error.reason(),
365            "exceeds the maximum attribute value length of 4096 bytes"
366        );
367        assert!(!error.to_string().contains(&oversized));
368    }
369
370    #[test]
371    fn attribute_map_enforces_the_entry_count() {
372        let at_cap: Vec<_> = (0..MAX_ATTRIBUTE_ENTRIES)
373            .map(|index| (key(&format!("k{index}")), string_value(1)))
374            .collect();
375        let over_cap: Vec<_> = (0..MAX_ATTRIBUTE_ENTRIES + 1)
376            .map(|index| (key(&format!("k{index}")), string_value(1)))
377            .collect();
378
379        assert_eq!(map(at_cap).len(), MAX_ATTRIBUTE_ENTRIES);
380        assert_eq!(
381            map_error(over_cap),
382            AttributesError::TooManyEntries {
383                entries: MAX_ATTRIBUTE_ENTRIES + 1
384            }
385        );
386    }
387
388    #[test]
389    fn attribute_map_enforces_the_total_size() {
390        // Sixteen entries of one 4,096-byte string each hold 65,536 value
391        // bytes, which is over the total before any key is counted. Dropping
392        // one entry brings the same map back under it.
393        let entries: Vec<_> = (0..16)
394            .map(|index| {
395                (
396                    key(&format!("k{index:02}")),
397                    string_value(MAX_ATTRIBUTE_VALUE_BYTES),
398                )
399            })
400            .collect();
401        let smaller: Vec<_> = entries.iter().skip(1).cloned().collect();
402
403        assert_eq!(
404            map_error(entries),
405            AttributesError::TooLarge {
406                total_bytes: 16 * (MAX_ATTRIBUTE_VALUE_BYTES + "k00".len())
407            }
408        );
409        assert_eq!(map(smaller).len(), 15);
410    }
411
412    #[test]
413    fn attribute_map_total_counts_key_bytes() {
414        // These sixteen entries sit exactly 128 bytes under the total, so one
415        // more entry fits only if its 128-byte key is not counted. It is
416        // counted, so the map is rejected.
417        let entries: Vec<_> = (0..16)
418            .map(|index| {
419                (
420                    key(&format!("k{index:02}")),
421                    string_value((MAX_ATTRIBUTES_TOTAL_BYTES - MAX_ATTRIBUTE_KEY_BYTES) / 16 - 3),
422                )
423            })
424            .collect();
425        let fits = map(entries.clone());
426        assert_eq!(
427            fits.logical_bytes(),
428            MAX_ATTRIBUTES_TOTAL_BYTES - MAX_ATTRIBUTE_KEY_BYTES
429        );
430
431        let mut with_long_key: Vec<_> = entries;
432        with_long_key.push((key(&"k".repeat(MAX_ATTRIBUTE_KEY_BYTES)), string_value(1)));
433        assert_eq!(
434            map_error(with_long_key),
435            AttributesError::TooLarge {
436                total_bytes: MAX_ATTRIBUTES_TOTAL_BYTES + 1
437            }
438        );
439    }
440
441    #[test]
442    fn attribute_map_validates_on_deserialize_too() {
443        // A durable row that breaks a limit must fail to decode rather than
444        // decode to something within the limits.
445        let over_entries = serde_json::to_string(
446            &(0..MAX_ATTRIBUTE_ENTRIES + 1)
447                .map(|index| (format!("k{index}"), string_value(1)))
448                .collect::<BTreeMap<_, _>>(),
449        )
450        .expect("serialize oversized map");
451        let over_value = format!(
452            r#"{{"a":{}}}"#,
453            serde_json::to_string(&"v".repeat(MAX_ATTRIBUTE_VALUE_BYTES + 1))
454                .expect("serialize oversized value")
455        );
456
457        assert!(serde_json::from_str::<Attributes>(&over_entries).is_err());
458        assert!(serde_json::from_str::<Attributes>(&over_value).is_err());
459        // The key grammar is enforced on the way in as well.
460        assert!(serde_json::from_str::<Attributes>(r#"{"":"a"}"#).is_err());
461    }
462
463    #[test]
464    fn attribute_map_round_trips_and_reads_back() {
465        let attributes = map([
466            (key("a"), string_value(3)),
467            (key("b"), value("draft,review")),
468            (key("empty"), value("")),
469        ]);
470
471        let json = serde_json::to_string(&attributes).expect("serialize attributes");
472        assert_eq!(json, r#"{"a":"vvv","b":"draft,review","empty":""}"#);
473        assert_eq!(
474            serde_json::from_str::<Attributes>(&json).expect("deserialize attributes"),
475            attributes
476        );
477        assert_eq!(attributes.get(&key("a")), Some(&string_value(3)));
478        assert_eq!(attributes.get(&key("missing")), None);
479        assert_eq!(attributes.get(&key("empty")), Some(&value("")));
480        assert_eq!(attributes.iter().count(), 3);
481        assert_eq!(attributes.as_map().len(), 3);
482        assert_eq!(attributes.logical_bytes(), 1 + 3 + 1 + 12 + 5);
483        assert_eq!(
484            BTreeMap::from(attributes.clone()),
485            attributes.as_map().clone()
486        );
487    }
488
489    #[test]
490    fn empty_attribute_map_is_a_valid_state() {
491        let empty = Attributes::default();
492
493        assert!(empty.is_empty());
494        assert_eq!(empty.len(), 0);
495        assert_eq!(empty.logical_bytes(), 0);
496        assert_eq!(Attributes::new(BTreeMap::new()).expect("empty map"), empty);
497        let json = serde_json::to_string(&empty).expect("serialize empty map");
498        assert_eq!(json, "{}");
499        assert_eq!(
500            serde_json::from_str::<Attributes>(&json).expect("deserialize empty map"),
501            empty
502        );
503    }
504
505    #[test]
506    fn attribute_revision_no_serializes_like_a_revision_no() {
507        let revision = AttributeRevisionNo(7);
508
509        assert_eq!(
510            serde_json::to_string(&revision).expect("serialize attribute revision"),
511            serde_json::to_string(&RevisionNo(7)).expect("serialize revision")
512        );
513        assert_eq!(
514            serde_json::to_string(&revision).expect("serialize attribute revision"),
515            "7"
516        );
517        assert_eq!(
518            serde_json::from_str::<AttributeRevisionNo>("7").expect("deserialize"),
519            revision
520        );
521        assert_eq!(AttributeRevisionNo::from(7), revision);
522        assert_eq!(revision.to_string(), "7");
523    }
524}