registry_v7.rs (10347B)
1 //! Frozen profile inbound semantics for event-contract registry v7. 2 3 #[cfg(not(feature = "std"))] 4 use alloc::{ 5 collections::BTreeMap, 6 string::{String, ToString}, 7 }; 8 #[cfg(feature = "std")] 9 use std::{collections::BTreeMap, string::String}; 10 11 use core::fmt; 12 13 use radroots_event::profile::{Nip05Identifier, RADROOTS_PROFILE_METADATA_MAX_CONTENT_BYTES}; 14 use serde::de::{IgnoredAny, MapAccess, SeqAccess, Visitor}; 15 use serde::{Deserialize, Deserializer}; 16 use serde_json::Value; 17 18 #[non_exhaustive] 19 #[derive(Clone, Debug, PartialEq, Eq)] 20 pub enum RadrootsProfileMetadataParseError { 21 ContentTooLarge { max: usize, actual: usize }, 22 InvalidJson, 23 RootNotObject, 24 DuplicateField(String), 25 } 26 27 impl RadrootsProfileMetadataParseError { 28 pub const fn code(&self) -> &'static str { 29 match self { 30 Self::ContentTooLarge { .. } => "content_too_large", 31 Self::InvalidJson => "invalid_json", 32 Self::RootNotObject => "root_not_object", 33 Self::DuplicateField(_) => "duplicate_field", 34 } 35 } 36 } 37 38 impl fmt::Display for RadrootsProfileMetadataParseError { 39 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 40 match self { 41 Self::ContentTooLarge { max, actual } => { 42 write!(f, "Profile metadata is {actual} bytes; max is {max}") 43 } 44 Self::InvalidJson => f.write_str("Profile metadata is invalid JSON"), 45 Self::RootNotObject => f.write_str("Profile metadata root must be a JSON object"), 46 Self::DuplicateField(field) => { 47 write!(f, "Profile metadata contains duplicate field {field:?}") 48 } 49 } 50 } 51 } 52 53 #[cfg(feature = "std")] 54 impl std::error::Error for RadrootsProfileMetadataParseError {} 55 56 /// A string media reference observed in inbound Profile metadata. 57 /// 58 /// Even a structurally valid Blossom URL remains unverified in this state. 59 #[derive(Clone, Debug, PartialEq, Eq)] 60 pub struct RadrootsUnverifiedProfileMediaReference(String); 61 62 impl RadrootsUnverifiedProfileMediaReference { 63 fn from_inbound(value: String) -> Self { 64 Self(value) 65 } 66 67 pub fn as_str(&self) -> &str { 68 &self.0 69 } 70 } 71 72 /// The content parser never performs NIP-05 network identity resolution. 73 /// 74 /// A future resolved identity must use a separate verified result type. 75 #[non_exhaustive] 76 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 77 pub enum RadrootsNip05IdentityVerification { 78 NotPerformed, 79 } 80 81 impl RadrootsNip05IdentityVerification { 82 pub const fn code(self) -> &'static str { 83 match self { 84 Self::NotPerformed => "not_performed", 85 } 86 } 87 } 88 89 impl fmt::Display for RadrootsUnverifiedProfileMediaReference { 90 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 91 f.write_str(self.as_str()) 92 } 93 } 94 95 /// Tolerant inbound Profile metadata with lossless raw and residual views. 96 /// 97 /// Correctly typed known fields are projected below. Unknown fields, known 98 /// fields with the wrong JSON type, and syntactically invalid NIP-05 strings 99 /// remain in `residual_fields` and in the complete raw object. This type does 100 /// not attest event kind, identifier, signature, or author. 101 #[derive(Clone, Debug, PartialEq)] 102 pub struct RadrootsInboundProfileMetadata { 103 raw_content: String, 104 raw_fields: BTreeMap<String, Value>, 105 residual_fields: BTreeMap<String, Value>, 106 name: Option<String>, 107 display_name: Option<String>, 108 about: Option<String>, 109 picture: Option<RadrootsUnverifiedProfileMediaReference>, 110 banner: Option<RadrootsUnverifiedProfileMediaReference>, 111 nip05: Option<Nip05Identifier>, 112 bot: Option<bool>, 113 } 114 115 impl RadrootsInboundProfileMetadata { 116 pub fn raw_content(&self) -> &str { 117 &self.raw_content 118 } 119 120 pub fn raw_fields(&self) -> &BTreeMap<String, Value> { 121 &self.raw_fields 122 } 123 124 pub fn residual_fields(&self) -> &BTreeMap<String, Value> { 125 &self.residual_fields 126 } 127 128 pub fn name(&self) -> Option<&str> { 129 self.name.as_deref() 130 } 131 132 pub fn display_name(&self) -> Option<&str> { 133 self.display_name.as_deref() 134 } 135 136 pub fn about(&self) -> Option<&str> { 137 self.about.as_deref() 138 } 139 140 pub fn picture(&self) -> Option<&RadrootsUnverifiedProfileMediaReference> { 141 self.picture.as_ref() 142 } 143 144 pub fn banner(&self) -> Option<&RadrootsUnverifiedProfileMediaReference> { 145 self.banner.as_ref() 146 } 147 148 /// Returns only a syntax-checked identifier; no NIP-05 resolution occurred. 149 pub fn nip05(&self) -> Option<&Nip05Identifier> { 150 self.nip05.as_ref() 151 } 152 153 pub const fn nip05_identity_verification(&self) -> RadrootsNip05IdentityVerification { 154 RadrootsNip05IdentityVerification::NotPerformed 155 } 156 157 pub const fn bot(&self) -> Option<bool> { 158 self.bot 159 } 160 } 161 162 /// Parses bounded, untrusted kind-0 metadata content after event verification. 163 /// 164 /// Successful parsing is not event admission. The caller must first require an 165 /// exact kind-0 event with a verified identifier and signature. Content larger 166 /// than [`RADROOTS_PROFILE_METADATA_MAX_CONTENT_BYTES`] is rejected before JSON 167 /// parsing. 168 pub fn parse_inbound_profile_metadata( 169 content: &str, 170 ) -> Result<RadrootsInboundProfileMetadata, RadrootsProfileMetadataParseError> { 171 parse_inbound_profile_metadata_registry_v7(content) 172 } 173 174 /// Parses Profile metadata with the behavior frozen for contract registry v7. 175 pub fn parse_inbound_profile_metadata_registry_v7( 176 content: &str, 177 ) -> Result<RadrootsInboundProfileMetadata, RadrootsProfileMetadataParseError> { 178 if content.len() > RADROOTS_PROFILE_METADATA_MAX_CONTENT_BYTES { 179 return Err(RadrootsProfileMetadataParseError::ContentTooLarge { 180 max: RADROOTS_PROFILE_METADATA_MAX_CONTENT_BYTES, 181 actual: content.len(), 182 }); 183 } 184 185 let root: ProfileMetadataRoot = serde_json::from_str(content) 186 .map_err(|_| RadrootsProfileMetadataParseError::InvalidJson)?; 187 let ProfileMetadataRoot::Object(unique) = root else { 188 return Err(RadrootsProfileMetadataParseError::RootNotObject); 189 }; 190 if let Some(field) = unique.duplicate { 191 return Err(RadrootsProfileMetadataParseError::DuplicateField(field)); 192 } 193 194 let raw_fields = unique.fields; 195 let mut residual_fields = raw_fields.clone(); 196 let name = project_string(&raw_fields, &mut residual_fields, "name"); 197 let display_name = project_string(&raw_fields, &mut residual_fields, "display_name"); 198 let about = project_string(&raw_fields, &mut residual_fields, "about"); 199 let picture = project_string(&raw_fields, &mut residual_fields, "picture") 200 .map(RadrootsUnverifiedProfileMediaReference::from_inbound); 201 let banner = project_string(&raw_fields, &mut residual_fields, "banner") 202 .map(RadrootsUnverifiedProfileMediaReference::from_inbound); 203 let nip05 = project_nip05(&raw_fields, &mut residual_fields); 204 let bot = project_bool(&raw_fields, &mut residual_fields, "bot"); 205 206 Ok(RadrootsInboundProfileMetadata { 207 raw_content: content.to_string(), 208 raw_fields, 209 residual_fields, 210 name, 211 display_name, 212 about, 213 picture, 214 banner, 215 nip05, 216 bot, 217 }) 218 } 219 220 fn project_string( 221 raw_fields: &BTreeMap<String, Value>, 222 residual_fields: &mut BTreeMap<String, Value>, 223 key: &'static str, 224 ) -> Option<String> { 225 let value = raw_fields.get(key)?.as_str()?.to_string(); 226 residual_fields.remove(key); 227 Some(value) 228 } 229 230 fn project_bool( 231 raw_fields: &BTreeMap<String, Value>, 232 residual_fields: &mut BTreeMap<String, Value>, 233 key: &'static str, 234 ) -> Option<bool> { 235 let value = raw_fields.get(key)?.as_bool()?; 236 residual_fields.remove(key); 237 Some(value) 238 } 239 240 fn project_nip05( 241 raw_fields: &BTreeMap<String, Value>, 242 residual_fields: &mut BTreeMap<String, Value>, 243 ) -> Option<Nip05Identifier> { 244 let value = raw_fields.get("nip05")?.as_str()?; 245 let identifier = Nip05Identifier::parse(value).ok()?; 246 residual_fields.remove("nip05"); 247 Some(identifier) 248 } 249 250 struct UniqueMetadataObject { 251 fields: BTreeMap<String, Value>, 252 duplicate: Option<String>, 253 } 254 255 enum ProfileMetadataRoot { 256 Object(UniqueMetadataObject), 257 NonObject, 258 } 259 260 impl<'de> Deserialize<'de> for ProfileMetadataRoot { 261 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 262 where 263 D: Deserializer<'de>, 264 { 265 deserializer.deserialize_any(ProfileMetadataRootVisitor) 266 } 267 } 268 269 struct ProfileMetadataRootVisitor; 270 271 impl<'de> Visitor<'de> for ProfileMetadataRootVisitor { 272 type Value = ProfileMetadataRoot; 273 274 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 275 formatter.write_str("Profile metadata JSON") 276 } 277 278 fn visit_bool<E>(self, _value: bool) -> Result<Self::Value, E> { 279 Ok(ProfileMetadataRoot::NonObject) 280 } 281 282 fn visit_i64<E>(self, _value: i64) -> Result<Self::Value, E> { 283 Ok(ProfileMetadataRoot::NonObject) 284 } 285 286 fn visit_u64<E>(self, _value: u64) -> Result<Self::Value, E> { 287 Ok(ProfileMetadataRoot::NonObject) 288 } 289 290 fn visit_f64<E>(self, _value: f64) -> Result<Self::Value, E> { 291 Ok(ProfileMetadataRoot::NonObject) 292 } 293 294 fn visit_str<E>(self, _value: &str) -> Result<Self::Value, E> { 295 Ok(ProfileMetadataRoot::NonObject) 296 } 297 298 fn visit_unit<E>(self) -> Result<Self::Value, E> { 299 Ok(ProfileMetadataRoot::NonObject) 300 } 301 302 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error> 303 where 304 A: SeqAccess<'de>, 305 { 306 loop { 307 if sequence.next_element::<IgnoredAny>()?.is_none() { 308 break; 309 } 310 } 311 Ok(ProfileMetadataRoot::NonObject) 312 } 313 314 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error> 315 where 316 A: MapAccess<'de>, 317 { 318 let mut fields = BTreeMap::new(); 319 let mut duplicate = None; 320 while let Some((key, value)) = map.next_entry::<String, Value>()? { 321 if fields.insert(key.clone(), value).is_some() && duplicate.is_none() { 322 duplicate = Some(key); 323 } 324 } 325 Ok(ProfileMetadataRoot::Object(UniqueMetadataObject { 326 fields, 327 duplicate, 328 })) 329 } 330 } 331 332 #[cfg(test)] 333 mod tests;