Skip to main content

loonfs_api/
attributes.rs

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