decode.rs (10476B)
1 #[cfg(not(feature = "std"))] 2 use alloc::{ 3 string::{String, ToString}, 4 vec::Vec, 5 }; 6 7 use radroots_events::{ 8 RadrootsNostrEvent, 9 farm_crdt::RadrootsFarmCrdtDocumentKind, 10 farm_file::{ 11 KIND_FARM_FILE_METADATA, RadrootsFarmFileDimensions, RadrootsFarmFileMetadata, 12 RadrootsFarmFileSource, 13 }, 14 farm_workspace::KIND_FARM_WORKSPACE_MANIFEST, 15 tags::{TAG_A, TAG_D, TAG_H, TAG_MIME, TAG_ORIGINAL_SHA256, TAG_SHA256, TAG_URL}, 16 }; 17 18 use crate::d_tag::validate_d_tag_tag; 19 use crate::error::EventParseError; 20 use crate::farm_file::encode::validate_metadata; 21 use crate::field_helpers::{ 22 optional_tag_value, parse_address_tag_with_kind, required_tag_value, tag_values, 23 validate_lowercase_hex_64_tag, validate_non_empty_tag_value, 24 }; 25 use crate::parsed::{RadrootsParsedData, RadrootsParsedEvent}; 26 27 const EXPECTED_KIND: &str = "1063"; 28 const TAG_ALT: &str = "alt"; 29 const TAG_BLURHASH: &str = "blurhash"; 30 const TAG_DIMENSIONS: &str = "dim"; 31 const TAG_FALLBACK: &str = "fallback"; 32 const TAG_IMAGE: &str = "image"; 33 const TAG_OWNER_DOCUMENT: &str = "radroots:owner_document"; 34 const TAG_SIZE: &str = "size"; 35 const TAG_THUMB: &str = "thumb"; 36 37 pub fn farm_file_metadata_from_event( 38 kind: u32, 39 tags: &[Vec<String>], 40 content: &str, 41 ) -> Result<RadrootsFarmFileMetadata, EventParseError> { 42 if kind != KIND_FARM_FILE_METADATA { 43 return Err(EventParseError::InvalidKind { 44 expected: EXPECTED_KIND, 45 got: kind, 46 }); 47 } 48 let d_tag = required_single_tag_value(tags, TAG_D)?; 49 validate_d_tag_tag(&d_tag, TAG_D)?; 50 let farm_group_id = required_tag_value(tags, TAG_H)?; 51 let workspace_address = required_tag_value(tags, TAG_A)?; 52 let workspace = 53 parse_address_tag_with_kind(&workspace_address, KIND_FARM_WORKSPACE_MANIFEST, TAG_A)?; 54 let url = required_tag_value(tags, TAG_URL)?; 55 let mime_type = required_tag_value(tags, TAG_MIME)?; 56 let sha256 = required_tag_value(tags, TAG_SHA256)?; 57 validate_lowercase_hex_64_tag(&sha256, TAG_SHA256)?; 58 let original_sha256 = optional_hash_tag(tags, TAG_ORIGINAL_SHA256)?; 59 let (owner_document_id, owner_document_kind) = parse_owner_document(tags)?; 60 let size_bytes = parse_size(tags)?; 61 let dimensions = parse_dimensions_tag(tags)?; 62 let blurhash = optional_tag_value(tags, TAG_BLURHASH)?; 63 let thumb = parse_source_tag(tags, TAG_THUMB)?; 64 let image = parse_source_tag(tags, TAG_IMAGE)?; 65 let alt = optional_tag_value(tags, TAG_ALT)?; 66 let fallbacks = tag_values(tags, TAG_FALLBACK)?; 67 let caption = if content.is_empty() { 68 None 69 } else { 70 Some(content.to_string()) 71 }; 72 73 let metadata = RadrootsFarmFileMetadata { 74 d_tag, 75 workspace: radroots_events::farm_workspace::RadrootsFarmWorkspaceRef { 76 pubkey: workspace.pubkey, 77 d_tag: workspace.d_tag, 78 }, 79 farm_group_id, 80 owner_document_id, 81 owner_document_kind, 82 caption, 83 url, 84 mime_type, 85 sha256, 86 original_sha256, 87 size_bytes, 88 dimensions, 89 blurhash, 90 thumb, 91 image, 92 alt, 93 fallbacks, 94 }; 95 validate_metadata(&metadata).map_err(encode_error_to_parse_error)?; 96 Ok(metadata) 97 } 98 99 pub fn data_from_event( 100 id: String, 101 author: String, 102 published_at: u32, 103 kind: u32, 104 content: String, 105 tags: Vec<Vec<String>>, 106 ) -> Result<RadrootsParsedData<RadrootsFarmFileMetadata>, EventParseError> { 107 let metadata = farm_file_metadata_from_event(kind, &tags, &content)?; 108 Ok(RadrootsParsedData::new( 109 id, 110 author, 111 published_at, 112 kind, 113 metadata, 114 )) 115 } 116 117 pub fn parsed_from_event( 118 id: String, 119 author: String, 120 published_at: u32, 121 kind: u32, 122 content: String, 123 tags: Vec<Vec<String>>, 124 sig: String, 125 ) -> Result<RadrootsParsedEvent<RadrootsFarmFileMetadata>, EventParseError> { 126 let data = data_from_event( 127 id.clone(), 128 author.clone(), 129 published_at, 130 kind, 131 content.clone(), 132 tags.clone(), 133 )?; 134 Ok(RadrootsParsedEvent { 135 event: RadrootsNostrEvent { 136 id, 137 author, 138 created_at: published_at, 139 kind, 140 content, 141 tags, 142 sig, 143 }, 144 data, 145 }) 146 } 147 148 fn required_single_tag_value( 149 tags: &[Vec<String>], 150 key: &'static str, 151 ) -> Result<String, EventParseError> { 152 let values = tag_values(tags, key)?; 153 let Some(first) = values.first() else { 154 return Err(EventParseError::MissingTag(key)); 155 }; 156 if values.iter().any(|value| value != first) { 157 return Err(EventParseError::InvalidTag(key)); 158 } 159 Ok(first.clone()) 160 } 161 162 fn optional_hash_tag( 163 tags: &[Vec<String>], 164 key: &'static str, 165 ) -> Result<Option<String>, EventParseError> { 166 let Some(value) = optional_tag_value(tags, key)? else { 167 return Ok(None); 168 }; 169 validate_lowercase_hex_64_tag(&value, key)?; 170 Ok(Some(value)) 171 } 172 173 fn parse_owner_document( 174 tags: &[Vec<String>], 175 ) -> Result<(String, RadrootsFarmCrdtDocumentKind), EventParseError> { 176 let tag = tags 177 .iter() 178 .find(|tag| tag.first().map(|value| value.as_str()) == Some(TAG_OWNER_DOCUMENT)) 179 .ok_or(EventParseError::MissingTag(TAG_OWNER_DOCUMENT))?; 180 if tag.len() != 3 { 181 return Err(EventParseError::InvalidTag(TAG_OWNER_DOCUMENT)); 182 } 183 let document_id = tag[1].clone(); 184 validate_d_tag_tag(&document_id, TAG_OWNER_DOCUMENT)?; 185 let kind = parse_document_kind_tag(&tag[2])?; 186 Ok((document_id, kind)) 187 } 188 189 fn parse_document_kind_tag(value: &str) -> Result<RadrootsFarmCrdtDocumentKind, EventParseError> { 190 if value.trim().is_empty() { 191 Err(EventParseError::InvalidTag(TAG_OWNER_DOCUMENT)) 192 } else { 193 Ok(RadrootsFarmCrdtDocumentKind::from(value.to_string())) 194 } 195 } 196 197 fn parse_size(tags: &[Vec<String>]) -> Result<Option<u64>, EventParseError> { 198 let Some(value) = optional_tag_value(tags, TAG_SIZE)? else { 199 return Ok(None); 200 }; 201 value 202 .parse::<u64>() 203 .map(Some) 204 .map_err(|err| EventParseError::InvalidNumber(TAG_SIZE, err)) 205 } 206 207 fn parse_dimensions_tag( 208 tags: &[Vec<String>], 209 ) -> Result<Option<RadrootsFarmFileDimensions>, EventParseError> { 210 let Some(value) = optional_tag_value(tags, TAG_DIMENSIONS)? else { 211 return Ok(None); 212 }; 213 Ok(Some(parse_dimensions(&value, TAG_DIMENSIONS)?)) 214 } 215 216 fn parse_dimensions( 217 value: &str, 218 tag: &'static str, 219 ) -> Result<RadrootsFarmFileDimensions, EventParseError> { 220 let (w, h) = value 221 .split_once('x') 222 .ok_or(EventParseError::InvalidTag(tag))?; 223 let w = w 224 .parse::<u32>() 225 .map_err(|_| EventParseError::InvalidTag(tag))?; 226 let h = h 227 .parse::<u32>() 228 .map_err(|_| EventParseError::InvalidTag(tag))?; 229 if w == 0 || h == 0 { 230 return Err(EventParseError::InvalidTag(tag)); 231 } 232 Ok(RadrootsFarmFileDimensions { w, h }) 233 } 234 235 fn parse_source_tag( 236 tags: &[Vec<String>], 237 key: &'static str, 238 ) -> Result<Option<RadrootsFarmFileSource>, EventParseError> { 239 let Some(tag) = tags 240 .iter() 241 .find(|tag| tag.first().map(|value| value.as_str()) == Some(key)) 242 else { 243 return Ok(None); 244 }; 245 if tag.len() < 2 || tag.len() > 4 { 246 return Err(EventParseError::InvalidTag(key)); 247 } 248 let url = tag[1].clone(); 249 validate_non_empty_tag_value(&url, key)?; 250 let mut mime_type = None; 251 let mut dimensions = None; 252 if let Some(value) = tag.get(2) { 253 validate_non_empty_tag_value(value, key)?; 254 if value.contains('x') { 255 dimensions = Some(parse_dimensions(value, key)?); 256 } else { 257 mime_type = Some(value.clone()); 258 } 259 } 260 if let Some(value) = tag.get(3) { 261 validate_non_empty_tag_value(value, key)?; 262 dimensions = Some(parse_dimensions(value, key)?); 263 } 264 Ok(Some(RadrootsFarmFileSource { 265 url, 266 mime_type, 267 dimensions, 268 })) 269 } 270 271 fn encode_error_to_parse_error(error: crate::error::EventEncodeError) -> EventParseError { 272 match error { 273 crate::error::EventEncodeError::InvalidKind(kind) => EventParseError::InvalidKind { 274 expected: EXPECTED_KIND, 275 got: kind, 276 }, 277 crate::error::EventEncodeError::EmptyRequiredField(field) 278 | crate::error::EventEncodeError::InvalidField(field) => match field { 279 "d_tag" => EventParseError::InvalidTag(TAG_D), 280 "farm_group_id" => EventParseError::InvalidTag(TAG_H), 281 "workspace.pubkey" | "workspace.d_tag" => EventParseError::InvalidTag(TAG_A), 282 "owner_document_id" => EventParseError::InvalidTag(TAG_OWNER_DOCUMENT), 283 "url" => EventParseError::InvalidTag(TAG_URL), 284 "mime_type" => EventParseError::InvalidTag(TAG_MIME), 285 "sha256" => EventParseError::InvalidTag(TAG_SHA256), 286 "original_sha256" => EventParseError::InvalidTag(TAG_ORIGINAL_SHA256), 287 field => EventParseError::InvalidTag(field), 288 }, 289 crate::error::EventEncodeError::Json => EventParseError::InvalidTag("content"), 290 } 291 } 292 293 #[cfg(test)] 294 mod tests { 295 use super::*; 296 use crate::error::EventEncodeError; 297 298 #[test] 299 fn encode_error_mapper_covers_unreachable_decode_edges() { 300 let err = encode_error_to_parse_error(EventEncodeError::InvalidKind(99)); 301 assert!(matches!( 302 err, 303 EventParseError::InvalidKind { 304 expected: "1063", 305 got: 99 306 } 307 )); 308 309 let err = encode_error_to_parse_error(EventEncodeError::Json); 310 assert!(matches!(err, EventParseError::InvalidTag("content"))); 311 } 312 313 #[test] 314 fn encode_error_mapper_covers_invalid_field_tags() { 315 for (field, expected_tag) in [ 316 ("d_tag", TAG_D), 317 ("farm_group_id", TAG_H), 318 ("workspace.pubkey", TAG_A), 319 ("workspace.d_tag", TAG_A), 320 ("owner_document_id", TAG_OWNER_DOCUMENT), 321 ("url", TAG_URL), 322 ("mime_type", TAG_MIME), 323 ("sha256", TAG_SHA256), 324 ("original_sha256", TAG_ORIGINAL_SHA256), 325 ] { 326 let err = encode_error_to_parse_error(EventEncodeError::InvalidField(field)); 327 assert!(matches!(err, EventParseError::InvalidTag(tag) if tag == expected_tag)); 328 } 329 } 330 }