identifier.rs (11094B)
1 //! Validated identifiers for canonical service-instance paths. 2 3 use core::{fmt, str::FromStr}; 4 5 use serde::{Deserialize, Deserializer, Serialize, Serializer}; 6 use thiserror::Error; 7 8 /// Maximum encoded length of a service identifier. 9 pub const SERVICE_ID_MAX_BYTES: usize = 128; 10 11 /// Maximum encoded length of an instance identifier. 12 pub const INSTANCE_ID_MAX_BYTES: usize = 128; 13 14 /// Identifies which service-instance path component failed validation. 15 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 16 pub enum ServiceIdentityKind { 17 Service, 18 Instance, 19 } 20 21 impl fmt::Display for ServiceIdentityKind { 22 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 23 match self { 24 Self::Service => formatter.write_str("service"), 25 Self::Instance => formatter.write_str("instance"), 26 } 27 } 28 } 29 30 /// Validation failure for a service or instance identifier. 31 #[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] 32 pub enum ServiceIdentityError { 33 #[error("{kind} identifier must not be empty")] 34 Empty { kind: ServiceIdentityKind }, 35 #[error("{kind} identifier exceeds its {maximum}-byte limit")] 36 TooLong { 37 kind: ServiceIdentityKind, 38 maximum: usize, 39 }, 40 #[error("{kind} identifier must start and end with a lowercase ASCII letter or digit")] 41 InvalidBoundary { kind: ServiceIdentityKind }, 42 #[error("{kind} identifier contains a forbidden character")] 43 InvalidCharacter { kind: ServiceIdentityKind }, 44 } 45 46 fn validate( 47 value: &str, 48 kind: ServiceIdentityKind, 49 maximum: usize, 50 ) -> Result<(), ServiceIdentityError> { 51 if value.is_empty() { 52 return Err(ServiceIdentityError::Empty { kind }); 53 } 54 if value.len() > maximum { 55 return Err(ServiceIdentityError::TooLong { kind, maximum }); 56 } 57 58 let is_alphanumeric = |byte: u8| byte.is_ascii_lowercase() || byte.is_ascii_digit(); 59 let bytes = value.as_bytes(); 60 if !is_alphanumeric(bytes[0]) || !is_alphanumeric(bytes[bytes.len() - 1]) { 61 return Err(ServiceIdentityError::InvalidBoundary { kind }); 62 } 63 if !bytes 64 .iter() 65 .all(|byte| is_alphanumeric(*byte) || matches!(*byte, b'-' | b'_')) 66 { 67 return Err(ServiceIdentityError::InvalidCharacter { kind }); 68 } 69 70 Ok(()) 71 } 72 73 macro_rules! service_identity { 74 ($name:ident, $kind:expr, $maximum:ident) => { 75 #[doc = concat!("A validated canonical ", stringify!($name), " path component.")] 76 #[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] 77 pub struct $name(String); 78 79 impl $name { 80 /// Parses and validates a canonical identifier. 81 pub fn new(value: impl AsRef<str>) -> Result<Self, ServiceIdentityError> { 82 let value = value.as_ref(); 83 validate(value, $kind, $maximum)?; 84 Ok(Self(value.to_owned())) 85 } 86 87 fn from_string(value: String) -> Result<Self, ServiceIdentityError> { 88 validate(&value, $kind, $maximum)?; 89 Ok(Self(value)) 90 } 91 92 /// Returns the canonical identifier text. 93 #[must_use] 94 pub fn as_str(&self) -> &str { 95 self.0.as_str() 96 } 97 98 /// Consumes the identifier and returns its canonical text. 99 #[must_use] 100 pub fn into_string(self) -> String { 101 self.0 102 } 103 } 104 105 impl AsRef<str> for $name { 106 fn as_ref(&self) -> &str { 107 self.as_str() 108 } 109 } 110 111 impl fmt::Display for $name { 112 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 113 formatter.write_str(self.as_str()) 114 } 115 } 116 117 impl FromStr for $name { 118 type Err = ServiceIdentityError; 119 120 fn from_str(value: &str) -> Result<Self, Self::Err> { 121 Self::new(value) 122 } 123 } 124 125 impl TryFrom<String> for $name { 126 type Error = ServiceIdentityError; 127 128 fn try_from(value: String) -> Result<Self, Self::Error> { 129 Self::from_string(value) 130 } 131 } 132 133 impl From<$name> for String { 134 fn from(value: $name) -> Self { 135 value.into_string() 136 } 137 } 138 139 impl Serialize for $name { 140 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 141 where 142 S: Serializer, 143 { 144 serializer.serialize_str(self.as_str()) 145 } 146 } 147 148 impl<'de> Deserialize<'de> for $name { 149 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 150 where 151 D: Deserializer<'de>, 152 { 153 struct Visitor; 154 155 impl<'de> serde::de::Visitor<'de> for Visitor { 156 type Value = $name; 157 158 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 159 formatter.write_str("a bounded canonical service identity") 160 } 161 162 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> 163 where 164 E: serde::de::Error, 165 { 166 $name::new(value).map_err(E::custom) 167 } 168 169 fn visit_string<E>(self, value: String) -> Result<Self::Value, E> 170 where 171 E: serde::de::Error, 172 { 173 $name::from_string(value).map_err(E::custom) 174 } 175 } 176 177 deserializer.deserialize_str(Visitor) 178 } 179 } 180 }; 181 } 182 183 service_identity!( 184 ServiceId, 185 ServiceIdentityKind::Service, 186 SERVICE_ID_MAX_BYTES 187 ); 188 service_identity!( 189 InstanceId, 190 ServiceIdentityKind::Instance, 191 INSTANCE_ID_MAX_BYTES 192 ); 193 194 #[cfg(test)] 195 mod tests { 196 use super::*; 197 198 #[test] 199 fn identifiers_accept_exact_boundaries_and_display_canonically() { 200 for service in ["a", "myc", "farm_service", "service-01"] { 201 let id = ServiceId::new(service).expect("valid service id"); 202 assert_eq!(id.as_str(), service); 203 assert_eq!(id.to_string(), service); 204 } 205 for instance in ["0", "default", "north_farm", "west-01"] { 206 let id = InstanceId::new(instance).expect("valid instance id"); 207 assert_eq!(id.as_str(), instance); 208 assert_eq!(id.to_string(), instance); 209 } 210 211 assert!(ServiceId::new("a".repeat(SERVICE_ID_MAX_BYTES)).is_ok()); 212 assert!(InstanceId::new("a".repeat(INSTANCE_ID_MAX_BYTES)).is_ok()); 213 } 214 215 #[test] 216 fn identifiers_reject_empty_overlong_and_noncanonical_text() { 217 assert_eq!( 218 ServiceId::new(""), 219 Err(ServiceIdentityError::Empty { 220 kind: ServiceIdentityKind::Service 221 }) 222 ); 223 assert_eq!( 224 InstanceId::new(""), 225 Err(ServiceIdentityError::Empty { 226 kind: ServiceIdentityKind::Instance 227 }) 228 ); 229 assert_eq!( 230 ServiceId::new("a".repeat(SERVICE_ID_MAX_BYTES + 1)), 231 Err(ServiceIdentityError::TooLong { 232 kind: ServiceIdentityKind::Service, 233 maximum: SERVICE_ID_MAX_BYTES, 234 }) 235 ); 236 assert_eq!( 237 InstanceId::new("a".repeat(INSTANCE_ID_MAX_BYTES + 1)), 238 Err(ServiceIdentityError::TooLong { 239 kind: ServiceIdentityKind::Instance, 240 maximum: INSTANCE_ID_MAX_BYTES, 241 }) 242 ); 243 let very_large = "a".repeat(4 * 1024 * 1024); 244 assert!(matches!( 245 ServiceId::new(&very_large), 246 Err(ServiceIdentityError::TooLong { 247 kind: ServiceIdentityKind::Service, 248 maximum: SERVICE_ID_MAX_BYTES, 249 }) 250 )); 251 assert!(matches!( 252 InstanceId::new(&very_large), 253 Err(ServiceIdentityError::TooLong { 254 kind: ServiceIdentityKind::Instance, 255 maximum: INSTANCE_ID_MAX_BYTES, 256 }) 257 )); 258 259 let service_json = serde_json::to_string(&very_large).expect("large service JSON"); 260 assert!(serde_json::from_str::<ServiceId>(&service_json).is_err()); 261 let instance_json = serde_json::to_string(&very_large).expect("large instance JSON"); 262 assert!(serde_json::from_str::<InstanceId>(&instance_json).is_err()); 263 264 for invalid in ["Myc", "café", "a.b", "a:b", "a b", "a%b", "a/b", r"a\b"] { 265 assert!(ServiceId::new(invalid).is_err(), "accepted `{invalid}`"); 266 assert!(InstanceId::new(invalid).is_err(), "accepted `{invalid}`"); 267 } 268 for invalid in ["-a", "a-", "_a", "a_"] { 269 assert!(ServiceId::new(invalid).is_err(), "accepted `{invalid}`"); 270 assert!(InstanceId::new(invalid).is_err(), "accepted `{invalid}`"); 271 } 272 } 273 274 #[test] 275 fn identifiers_reject_traversal_and_separators() { 276 for invalid in [".", "..", "../a", "a/../b", r"..\a", "%2e%2e", "a//b"] { 277 assert!(ServiceId::new(invalid).is_err(), "accepted `{invalid}`"); 278 assert!(InstanceId::new(invalid).is_err(), "accepted `{invalid}`"); 279 } 280 } 281 282 #[test] 283 fn serde_round_trips_revalidate_identifiers() { 284 let service = ServiceId::new("myc").expect("service id"); 285 let encoded = serde_json::to_string(&service).expect("serialize service id"); 286 assert_eq!(encoded, "\"myc\""); 287 assert_eq!( 288 serde_json::from_str::<ServiceId>(&encoded).expect("deserialize service id"), 289 service 290 ); 291 292 let instance = InstanceId::new("default-01").expect("instance id"); 293 let encoded = serde_json::to_string(&instance).expect("serialize instance id"); 294 assert_eq!( 295 serde_json::from_str::<InstanceId>(&encoded).expect("deserialize instance id"), 296 instance 297 ); 298 299 assert!(serde_json::from_str::<ServiceId>("\"../myc\"").is_err()); 300 assert!(serde_json::from_str::<InstanceId>("\"UPPER\"").is_err()); 301 } 302 303 #[test] 304 fn identifier_conversion_traits_preserve_validated_text() { 305 let service = "myc" 306 .parse::<ServiceId>() 307 .expect("parse service identifier"); 308 assert_eq!(service.as_ref(), "myc"); 309 assert_eq!(String::from(service), "myc"); 310 311 let service = 312 ServiceId::try_from(String::from("rhi")).expect("convert owned service identifier"); 313 assert_eq!(service.into_string(), "rhi"); 314 315 let instance = "primary" 316 .parse::<InstanceId>() 317 .expect("parse instance identifier"); 318 assert_eq!(instance.as_ref(), "primary"); 319 assert_eq!(String::from(instance), "primary"); 320 321 let instance = InstanceId::try_from(String::from("secondary")) 322 .expect("convert owned instance identifier"); 323 assert_eq!(instance.into_string(), "secondary"); 324 } 325 }