use crate::ids::validation_error;
use crate::InodeId;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use thiserror::Error;
const PREFIX: &str = "ino_";
pub const PATTERN: &str = r"^ino_[1-9][0-9]*$";
pub const EXAMPLE: &str = "ino_123";
pub const DESCRIPTION: &str = "Stable inode ID within a namespace";
const INVALID_REASON: &str = "must use `ino_` followed by a nonzero u64 without leading zeroes";
validation_error!(PublicInodeIdError, "invalid inode id {value:?}: {reason}");
pub fn encode(id: InodeId) -> String {
format!("{PREFIX}{}", id.0)
}
pub fn decode(value: &str) -> Result<InodeId, PublicInodeIdError> {
let Some(suffix) = value.strip_prefix(PREFIX) else {
return Err(PublicInodeIdError::new(value, INVALID_REASON));
};
if suffix.is_empty()
|| suffix.starts_with('0')
|| !suffix.bytes().all(|byte| byte.is_ascii_digit())
{
return Err(PublicInodeIdError::new(value, INVALID_REASON));
}
suffix
.parse::<u64>()
.map(InodeId)
.map_err(|_| PublicInodeIdError::new(value, INVALID_REASON))
}
pub fn serialize<S>(id: &InodeId, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
encode(*id).serialize(serializer)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<InodeId, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
decode(&value).map_err(serde::de::Error::custom)
}
pub mod option {
use super::{decode, encode};
use crate::InodeId;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub fn serialize<S>(id: &Option<InodeId>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
id.map(encode).serialize(serializer)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<InodeId>, D::Error>
where
D: Deserializer<'de>,
{
Option::<String>::deserialize(deserializer)?
.map(|value| decode(&value))
.transpose()
.map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_rejected(value: &str) {
let error = decode(value).expect_err("invalid public inode id");
assert_eq!(error.value(), value);
assert_eq!(error.reason(), INVALID_REASON);
assert_eq!(
error.to_string(),
format!("invalid inode id {value:?}: {INVALID_REASON}")
);
}
#[test]
fn decode_accepts_public_inode_id_grammar() {
for (value, expected) in [
("ino_1", InodeId(1)),
("ino_42", InodeId(42)),
("ino_18446744073709551615", InodeId(u64::MAX)),
] {
assert_eq!(decode(value).expect("valid public inode id"), expected);
assert_eq!(encode(expected), value);
}
}
macro_rules! rejection_test {
($name:ident, $value:literal) => {
#[test]
fn $name() {
assert_rejected($value);
}
};
}
rejection_test!(decode_rejects_zero, "ino_0");
rejection_test!(decode_rejects_leading_zero, "ino_01");
rejection_test!(decode_rejects_sign, "ino_-1");
rejection_test!(decode_rejects_uppercase_prefix, "INO_27");
rejection_test!(decode_rejects_mixed_case_prefix, "Ino_27");
rejection_test!(decode_rejects_missing_prefix, "27");
rejection_test!(decode_rejects_empty_suffix, "ino_");
rejection_test!(decode_rejects_suffix_trailing_text, "ino_27x");
rejection_test!(decode_rejects_leading_whitespace, " ino_27");
rejection_test!(decode_rejects_trailing_whitespace, "ino_27 ");
rejection_test!(decode_rejects_empty_string, "");
rejection_test!(decode_rejects_u64_overflow, "ino_18446744073709551616");
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
struct RequiredField {
#[serde(with = "crate::public_inode_id")]
inode_id: InodeId,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
struct OptionalField {
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::public_inode_id::option"
)]
inode_id: Option<InodeId>,
}
#[test]
fn serde_adapter_uses_only_the_string_form() {
let value = RequiredField {
inode_id: InodeId(42),
};
assert_eq!(
serde_json::to_string(&value).expect("serialize public inode id"),
r#"{"inode_id":"ino_42"}"#
);
assert_eq!(
serde_json::from_str::<RequiredField>(r#"{"inode_id":"ino_42"}"#)
.expect("deserialize public inode id"),
value
);
assert!(serde_json::from_str::<RequiredField>(r#"{"inode_id":42}"#).is_err());
}
#[test]
fn optional_serde_adapter_composes_with_skip_serializing_if() {
let present = OptionalField {
inode_id: Some(InodeId(42)),
};
let absent = OptionalField { inode_id: None };
assert_eq!(
serde_json::to_string(&present).expect("serialize present public inode id"),
r#"{"inode_id":"ino_42"}"#
);
assert_eq!(
serde_json::to_string(&absent).expect("serialize absent public inode id"),
"{}"
);
assert_eq!(
serde_json::from_str::<OptionalField>(r#"{"inode_id":"ino_42"}"#)
.expect("deserialize present public inode id"),
present
);
assert_eq!(
serde_json::from_str::<OptionalField>(r#"{"inode_id":null}"#)
.expect("deserialize null public inode id"),
absent
);
assert_eq!(
serde_json::from_str::<OptionalField>("{}").expect("deserialize missing inode id"),
absent
);
assert!(serde_json::from_str::<OptionalField>(r#"{"inode_id":42}"#).is_err());
}
}