Skip to main content

loonfs_api/
public_inode_id.rs

1//! Converts inode IDs between internal numbers and public API strings.
2//!
3//! Public IDs use the form `ino_<number>`. They are scoped to a namespace and
4//! should be treated as identifiers rather than numbers.
5
6use crate::ids::validation_error;
7use crate::InodeId;
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9use thiserror::Error;
10
11/// Prefix used by every public inode ID.
12const PREFIX: &str = "ino_";
13
14/// OpenAPI pattern for public inode IDs.
15pub const PATTERN: &str = r"^ino_[1-9][0-9]*$";
16
17/// Example public inode ID used in OpenAPI.
18pub const EXAMPLE: &str = "ino_123";
19
20/// OpenAPI description for public inode IDs.
21pub const DESCRIPTION: &str = "Stable inode ID within a namespace";
22
23const INVALID_REASON: &str = "must use `ino_` followed by a nonzero u64 without leading zeroes";
24
25validation_error!(PublicInodeIdError, "invalid inode id {value:?}: {reason}");
26
27/// Converts an inode ID to its public API string.
28pub fn encode(id: InodeId) -> String {
29    format!("{PREFIX}{}", id.0)
30}
31
32/// Parses and validates a public inode ID.
33pub fn decode(value: &str) -> Result<InodeId, PublicInodeIdError> {
34    let Some(suffix) = value.strip_prefix(PREFIX) else {
35        return Err(PublicInodeIdError::new(value, INVALID_REASON));
36    };
37    if suffix.is_empty()
38        || suffix.starts_with('0')
39        || !suffix.bytes().all(|byte| byte.is_ascii_digit())
40    {
41        return Err(PublicInodeIdError::new(value, INVALID_REASON));
42    }
43
44    suffix
45        .parse::<u64>()
46        .map(InodeId)
47        .map_err(|_| PublicInodeIdError::new(value, INVALID_REASON))
48}
49
50/// Serializes an inode ID as a public API string.
51pub fn serialize<S>(id: &InodeId, serializer: S) -> Result<S::Ok, S::Error>
52where
53    S: Serializer,
54{
55    encode(*id).serialize(serializer)
56}
57
58/// Deserializes an inode ID from its public API string.
59pub fn deserialize<'de, D>(deserializer: D) -> Result<InodeId, D::Error>
60where
61    D: Deserializer<'de>,
62{
63    let value = String::deserialize(deserializer)?;
64    decode(&value).map_err(serde::de::Error::custom)
65}
66
67/// Serde support for optional public inode ID fields.
68pub mod option {
69    use super::{decode, encode};
70    use crate::InodeId;
71    use serde::{Deserialize, Deserializer, Serialize, Serializer};
72
73    /// Serializes an optional inode ID as a public API string.
74    pub fn serialize<S>(id: &Option<InodeId>, serializer: S) -> Result<S::Ok, S::Error>
75    where
76        S: Serializer,
77    {
78        id.map(encode).serialize(serializer)
79    }
80
81    /// Deserializes an optional inode ID from its public API string.
82    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<InodeId>, D::Error>
83    where
84        D: Deserializer<'de>,
85    {
86        Option::<String>::deserialize(deserializer)?
87            .map(|value| decode(&value))
88            .transpose()
89            .map_err(serde::de::Error::custom)
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    fn assert_rejected(value: &str) {
98        let error = decode(value).expect_err("invalid public inode id");
99
100        assert_eq!(error.value(), value);
101        assert_eq!(error.reason(), INVALID_REASON);
102        assert_eq!(
103            error.to_string(),
104            format!("invalid inode id {value:?}: {INVALID_REASON}")
105        );
106    }
107
108    #[test]
109    fn decode_accepts_public_inode_id_grammar() {
110        for (value, expected) in [
111            ("ino_1", InodeId(1)),
112            ("ino_42", InodeId(42)),
113            ("ino_18446744073709551615", InodeId(u64::MAX)),
114        ] {
115            assert_eq!(decode(value).expect("valid public inode id"), expected);
116            assert_eq!(encode(expected), value);
117        }
118    }
119
120    macro_rules! rejection_test {
121        ($name:ident, $value:literal) => {
122            #[test]
123            fn $name() {
124                assert_rejected($value);
125            }
126        };
127    }
128
129    rejection_test!(decode_rejects_zero, "ino_0");
130    rejection_test!(decode_rejects_leading_zero, "ino_01");
131    rejection_test!(decode_rejects_sign, "ino_-1");
132    rejection_test!(decode_rejects_uppercase_prefix, "INO_27");
133    rejection_test!(decode_rejects_mixed_case_prefix, "Ino_27");
134    rejection_test!(decode_rejects_missing_prefix, "27");
135    rejection_test!(decode_rejects_empty_suffix, "ino_");
136    rejection_test!(decode_rejects_suffix_trailing_text, "ino_27x");
137    rejection_test!(decode_rejects_leading_whitespace, " ino_27");
138    rejection_test!(decode_rejects_trailing_whitespace, "ino_27 ");
139    rejection_test!(decode_rejects_empty_string, "");
140    rejection_test!(decode_rejects_u64_overflow, "ino_18446744073709551616");
141
142    #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
143    struct RequiredField {
144        #[serde(with = "crate::public_inode_id")]
145        inode_id: InodeId,
146    }
147
148    #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
149    struct OptionalField {
150        #[serde(
151            default,
152            skip_serializing_if = "Option::is_none",
153            with = "crate::public_inode_id::option"
154        )]
155        inode_id: Option<InodeId>,
156    }
157
158    #[test]
159    fn serde_adapter_uses_only_the_string_form() {
160        let value = RequiredField {
161            inode_id: InodeId(42),
162        };
163
164        assert_eq!(
165            serde_json::to_string(&value).expect("serialize public inode id"),
166            r#"{"inode_id":"ino_42"}"#
167        );
168        assert_eq!(
169            serde_json::from_str::<RequiredField>(r#"{"inode_id":"ino_42"}"#)
170                .expect("deserialize public inode id"),
171            value
172        );
173        assert!(serde_json::from_str::<RequiredField>(r#"{"inode_id":42}"#).is_err());
174    }
175
176    #[test]
177    fn optional_serde_adapter_composes_with_skip_serializing_if() {
178        let present = OptionalField {
179            inode_id: Some(InodeId(42)),
180        };
181        let absent = OptionalField { inode_id: None };
182
183        assert_eq!(
184            serde_json::to_string(&present).expect("serialize present public inode id"),
185            r#"{"inode_id":"ino_42"}"#
186        );
187        assert_eq!(
188            serde_json::to_string(&absent).expect("serialize absent public inode id"),
189            "{}"
190        );
191        assert_eq!(
192            serde_json::from_str::<OptionalField>(r#"{"inode_id":"ino_42"}"#)
193                .expect("deserialize present public inode id"),
194            present
195        );
196        assert_eq!(
197            serde_json::from_str::<OptionalField>(r#"{"inode_id":null}"#)
198                .expect("deserialize null public inode id"),
199            absent
200        );
201        assert_eq!(
202            serde_json::from_str::<OptionalField>("{}").expect("deserialize missing inode id"),
203            absent
204        );
205        assert!(serde_json::from_str::<OptionalField>(r#"{"inode_id":42}"#).is_err());
206    }
207}