loonfs_api/
public_inode_id.rs1use crate::ids::validation_error;
7use crate::InodeId;
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9use thiserror::Error;
10
11const 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(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
50pub fn serialize<S>(id: &InodeId, serializer: S) -> Result<S::Ok, S::Error>
52where
53 S: Serializer,
54{
55 encode(*id).serialize(serializer)
56}
57
58pub 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
67pub mod option {
69 use super::{decode, encode};
70 use crate::InodeId;
71 use serde::{Deserialize, Deserializer, Serialize, Serializer};
72
73 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 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}