lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

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 }