loonfs_api/
public_inode_id.rs1use crate::ids::validation_error;
7use crate::InodeId;
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9use thiserror::Error;
10
11pub const PREFIX: &str = "ino_";
13
14pub const PATTERN: &str = r"^ino_[1-9][0-9]*$";
16
17pub const EXAMPLE: &str = "ino_123";
19
20pub 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
27pub fn encode(id: InodeId) -> String {
29 format!("{PREFIX}{}", id.0)
30}
31
32pub fn decode(value: &str) -> Result<InodeId, PublicInodeIdError> {
34 let Some(suffix) = value.strip_prefix(PREFIX) else {
35 return Err(invalid(value));
36 };
37 if suffix.is_empty()
38 || suffix.starts_with('0')
39 || !suffix.bytes().all(|byte| byte.is_ascii_digit())
40 {
41 return Err(invalid(value));
42 }
43
44 suffix
45 .parse::<u64>()
46 .map(InodeId)
47 .map_err(|_| invalid(value))
48}
49
50fn invalid(value: &str) -> PublicInodeIdError {
51 PublicInodeIdError {
52 value: value.to_owned(),
53 reason: INVALID_REASON.to_owned(),
54 }
55}
56
57pub fn serialize<S>(id: &InodeId, serializer: S) -> Result<S::Ok, S::Error>
59where
60 S: Serializer,
61{
62 encode(*id).serialize(serializer)
63}
64
65pub fn deserialize<'de, D>(deserializer: D) -> Result<InodeId, D::Error>
67where
68 D: Deserializer<'de>,
69{
70 let value = String::deserialize(deserializer)?;
71 decode(&value).map_err(serde::de::Error::custom)
72}
73
74pub mod option {
76 use super::{decode, encode};
77 use crate::InodeId;
78 use serde::{Deserialize, Deserializer, Serialize, Serializer};
79
80 pub fn serialize<S>(id: &Option<InodeId>, serializer: S) -> Result<S::Ok, S::Error>
82 where
83 S: Serializer,
84 {
85 id.map(encode).serialize(serializer)
86 }
87
88 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<InodeId>, D::Error>
90 where
91 D: Deserializer<'de>,
92 {
93 Option::<String>::deserialize(deserializer)?
94 .map(|value| decode(&value))
95 .transpose()
96 .map_err(serde::de::Error::custom)
97 }
98}
99
100#[cfg(feature = "openapi")]
102#[allow(
103 deprecated,
104 reason = "the published schema uses the requested singular example field"
105)]
106pub fn schema() -> utoipa::openapi::schema::Object {
107 utoipa::openapi::schema::Object::builder()
108 .schema_type(utoipa::openapi::schema::Type::String)
109 .pattern(Some(PATTERN))
110 .example(Some(serde_json::json!(EXAMPLE)))
111 .description(Some(DESCRIPTION))
112 .build()
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 fn assert_rejected(value: &str) {
120 let error = decode(value).expect_err("invalid public inode id");
121
122 assert_eq!(error.value(), value);
123 assert_eq!(error.reason(), INVALID_REASON);
124 assert_eq!(
125 error.to_string(),
126 format!("invalid inode id {value:?}: {INVALID_REASON}")
127 );
128 }
129
130 #[test]
131 fn decode_accepts_public_inode_id_grammar() {
132 for (value, expected) in [
133 ("ino_1", InodeId(1)),
134 ("ino_42", InodeId(42)),
135 ("ino_18446744073709551615", InodeId(u64::MAX)),
136 ] {
137 assert_eq!(decode(value).expect("valid public inode id"), expected);
138 assert_eq!(encode(expected), value);
139 }
140 }
141
142 macro_rules! rejection_test {
143 ($name:ident, $value:literal) => {
144 #[test]
145 fn $name() {
146 assert_rejected($value);
147 }
148 };
149 }
150
151 rejection_test!(decode_rejects_zero, "ino_0");
152 rejection_test!(decode_rejects_leading_zero, "ino_01");
153 rejection_test!(decode_rejects_sign, "ino_-1");
154 rejection_test!(decode_rejects_uppercase_prefix, "INO_27");
155 rejection_test!(decode_rejects_mixed_case_prefix, "Ino_27");
156 rejection_test!(decode_rejects_missing_prefix, "27");
157 rejection_test!(decode_rejects_empty_suffix, "ino_");
158 rejection_test!(decode_rejects_suffix_trailing_text, "ino_27x");
159 rejection_test!(decode_rejects_leading_whitespace, " ino_27");
160 rejection_test!(decode_rejects_trailing_whitespace, "ino_27 ");
161 rejection_test!(decode_rejects_empty_string, "");
162 rejection_test!(decode_rejects_u64_overflow, "ino_18446744073709551616");
163
164 #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
165 struct RequiredField {
166 #[serde(with = "crate::public_inode_id")]
167 inode_id: InodeId,
168 }
169
170 #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
171 struct OptionalField {
172 #[serde(
173 default,
174 skip_serializing_if = "Option::is_none",
175 with = "crate::public_inode_id::option"
176 )]
177 inode_id: Option<InodeId>,
178 }
179
180 #[test]
181 fn serde_adapter_uses_only_the_string_form() {
182 let value = RequiredField {
183 inode_id: InodeId(42),
184 };
185
186 assert_eq!(
187 serde_json::to_string(&value).expect("serialize public inode id"),
188 r#"{"inode_id":"ino_42"}"#
189 );
190 assert_eq!(
191 serde_json::from_str::<RequiredField>(r#"{"inode_id":"ino_42"}"#)
192 .expect("deserialize public inode id"),
193 value
194 );
195 assert!(serde_json::from_str::<RequiredField>(r#"{"inode_id":42}"#).is_err());
196 }
197
198 #[test]
199 fn optional_serde_adapter_composes_with_skip_serializing_if() {
200 let present = OptionalField {
201 inode_id: Some(InodeId(42)),
202 };
203 let absent = OptionalField { inode_id: None };
204
205 assert_eq!(
206 serde_json::to_string(&present).expect("serialize present public inode id"),
207 r#"{"inode_id":"ino_42"}"#
208 );
209 assert_eq!(
210 serde_json::to_string(&absent).expect("serialize absent public inode id"),
211 "{}"
212 );
213 assert_eq!(
214 serde_json::from_str::<OptionalField>(r#"{"inode_id":"ino_42"}"#)
215 .expect("deserialize present public inode id"),
216 present
217 );
218 assert_eq!(
219 serde_json::from_str::<OptionalField>(r#"{"inode_id":null}"#)
220 .expect("deserialize null public inode id"),
221 absent
222 );
223 assert_eq!(
224 serde_json::from_str::<OptionalField>("{}").expect("deserialize missing inode id"),
225 absent
226 );
227 assert!(serde_json::from_str::<OptionalField>(r#"{"inode_id":42}"#).is_err());
228 }
229}