1use crate::ids::{numeric_id, string_id, validation_error};
4use serde::{Deserialize, Serialize};
5use std::collections::BTreeMap;
6use std::fmt;
7use thiserror::Error;
8
9pub const MAX_ATTRIBUTE_KEY_BYTES: usize = 128;
11pub const MAX_ATTRIBUTE_VALUE_BYTES: usize = 4096;
13pub const MAX_ATTRIBUTE_ENTRIES: usize = 100;
15pub const MAX_ATTRIBUTES_TOTAL_BYTES: usize = 65_536;
20
21pub const RESERVED_ATTRIBUTE_KEY_PREFIX: &str = "loonfs.";
23
24validation_error!(
25 AttributeKeyValidationError,
26 "invalid attribute key {value:?}: {reason}"
27);
28
29string_id! {
30 AttributeKey,
35 error = AttributeKeyValidationError,
36 validate = validate_attribute_key,
37 schema(example = "owner")
38}
39
40impl AttributeKey {
41 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 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 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 pub fn logical_bytes(&self) -> usize {
103 self.as_str().len()
104 }
105}
106
107#[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 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 pub fn get(&self, key: &AttributeKey) -> Option<&AttributeValue> {
138 self.0.get(key)
139 }
140
141 pub fn iter(&self) -> impl Iterator<Item = (&AttributeKey, &AttributeValue)> {
143 self.0.iter()
144 }
145
146 pub fn len(&self) -> usize {
148 self.0.len()
149 }
150
151 pub fn is_empty(&self) -> bool {
153 self.0.is_empty()
154 }
155
156 pub fn as_map(&self) -> &BTreeMap<AttributeKey, AttributeValue> {
158 &self.0
159 }
160
161 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#[derive(Debug, Clone, PartialEq, Eq, Error)]
200pub enum AttributesError {
201 #[error("attribute map holds {entries} entries, which exceeds the maximum of {MAX_ATTRIBUTE_ENTRIES}")]
203 TooManyEntries {
204 entries: usize,
206 },
207 #[error("attribute map holds {total_bytes} logical bytes, which exceeds the maximum of {MAX_ATTRIBUTES_TOTAL_BYTES} bytes")]
209 TooLarge {
210 total_bytes: usize,
212 },
213}
214
215numeric_id! {
216 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 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 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 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 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 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 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}