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