1use crate::ids::{numeric_id, string_id, validation_error};
10use serde::{Deserialize, Serialize};
11use std::collections::BTreeMap;
12use std::fmt;
13use thiserror::Error;
14
15pub const MAX_ATTRIBUTE_KEY_BYTES: usize = 128;
17pub const MAX_ATTRIBUTE_VALUE_BYTES: usize = 4096;
19pub const MAX_ATTRIBUTE_ENTRIES: usize = 100;
21pub const MAX_ATTRIBUTES_TOTAL_BYTES: usize = 65_536;
26
27pub const RESERVED_ATTRIBUTE_KEY_PREFIX: &str = "loonfs.";
29
30validation_error!(
31 AttributeKeyValidationError,
32 "invalid attribute key {value:?}: {reason}"
33);
34
35string_id! {
36 AttributeKey,
48 error = AttributeKeyValidationError,
49 validate = validate_attribute_key,
50 schema(example = "owner")
51}
52
53impl AttributeKey {
54 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 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 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 pub fn logical_bytes(&self) -> usize {
125 self.as_str().len()
126 }
127}
128
129#[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 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 pub fn get(&self, key: &AttributeKey) -> Option<&AttributeValue> {
167 self.0.get(key)
168 }
169
170 pub fn iter(&self) -> impl Iterator<Item = (&AttributeKey, &AttributeValue)> {
172 self.0.iter()
173 }
174
175 pub fn len(&self) -> usize {
177 self.0.len()
178 }
179
180 pub fn is_empty(&self) -> bool {
182 self.0.is_empty()
183 }
184
185 pub fn as_map(&self) -> &BTreeMap<AttributeKey, AttributeValue> {
187 &self.0
188 }
189
190 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#[derive(Debug, Clone, PartialEq, Eq, Error)]
229pub enum AttributesError {
230 #[error("attribute map holds {entries} entries, which exceeds the maximum of {MAX_ATTRIBUTE_ENTRIES}")]
232 TooManyEntries {
233 entries: usize,
235 },
236 #[error("attribute map holds {total_bytes} logical bytes, which exceeds the maximum of {MAX_ATTRIBUTES_TOTAL_BYTES} bytes")]
238 TooLarge {
239 total_bytes: usize,
241 },
242}
243
244numeric_id! {
245 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 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 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 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 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 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 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}