lib

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

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 }