lib

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

manifest.rs (8383B)


      1 #![allow(clippy::module_name_repetitions)]
      2 #[cfg(not(feature = "std"))]
      3 use alloc::{string::String, vec::Vec};
      4 use core::fmt;
      5 
      6 #[cfg_attr(feature = "dto-bindgen", derive(dto_bindgen::Dto))]
      7 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
      8 #[derive(Clone, Debug, PartialEq, Eq)]
      9 pub struct RadrootsEventsIndexedShardMetadata {
     10     pub file: String,
     11     pub count: u32,
     12     pub first_id: String,
     13     pub last_id: String,
     14     pub first_published_at: u32,
     15     pub last_published_at: u32,
     16     pub sha256: String,
     17 }
     18 
     19 #[cfg_attr(feature = "dto-bindgen", derive(dto_bindgen::Dto))]
     20 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
     21 #[derive(Clone, Debug, PartialEq, Eq)]
     22 pub struct RadrootsEventsIndexedManifest {
     23     pub country: String,
     24     pub total: u32,
     25     pub shard_size: u32,
     26     pub first_published_at: u32,
     27     pub last_published_at: u32,
     28     pub shards: Vec<RadrootsEventsIndexedShardMetadata>,
     29 }
     30 
     31 #[derive(Debug, PartialEq, Eq)]
     32 pub enum RadrootsEventsIndexedManifestError {
     33     EmptyCountry,
     34     EmptyShards,
     35     EmptyFile(u32),
     36     InvalidSha256(u32),
     37     InconsistentTotals,
     38 }
     39 
     40 impl fmt::Display for RadrootsEventsIndexedManifestError {
     41     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
     42         match self {
     43             RadrootsEventsIndexedManifestError::EmptyCountry => write!(f, "country is empty"),
     44             RadrootsEventsIndexedManifestError::EmptyShards => write!(f, "no shards in manifest"),
     45             RadrootsEventsIndexedManifestError::EmptyFile(i) => {
     46                 write!(f, "shard {} has empty file name", i)
     47             }
     48             RadrootsEventsIndexedManifestError::InvalidSha256(i) => {
     49                 write!(f, "shard {} has invalid sha256", i)
     50             }
     51             RadrootsEventsIndexedManifestError::InconsistentTotals => {
     52                 write!(f, "total does not match sum of shard counts")
     53             }
     54         }
     55     }
     56 }
     57 
     58 #[cfg(feature = "std")]
     59 impl std::error::Error for RadrootsEventsIndexedManifestError {}
     60 
     61 pub fn validate_manifest(
     62     m: &RadrootsEventsIndexedManifest,
     63 ) -> Result<(), RadrootsEventsIndexedManifestError> {
     64     if m.country.trim().is_empty() {
     65         return Err(RadrootsEventsIndexedManifestError::EmptyCountry);
     66     }
     67     if m.shards.is_empty() {
     68         return Err(RadrootsEventsIndexedManifestError::EmptyShards);
     69     }
     70     let mut sum: u64 = 0;
     71     for (i, s) in m.shards.iter().enumerate() {
     72         if s.file.trim().is_empty() {
     73             return Err(RadrootsEventsIndexedManifestError::EmptyFile(i as u32));
     74         }
     75         if s.sha256.len() != 64 || !s.sha256.chars().all(|c| c.is_ascii_hexdigit()) {
     76             return Err(RadrootsEventsIndexedManifestError::InvalidSha256(i as u32));
     77         }
     78         sum += s.count as u64;
     79     }
     80     if sum > u32::MAX as u64 || sum != m.total as u64 {
     81         return Err(RadrootsEventsIndexedManifestError::InconsistentTotals);
     82     }
     83     Ok(())
     84 }
     85 
     86 #[cfg(test)]
     87 mod tests {
     88     use super::{
     89         RadrootsEventsIndexedManifest, RadrootsEventsIndexedManifestError,
     90         RadrootsEventsIndexedShardMetadata, validate_manifest,
     91     };
     92     #[cfg(not(feature = "std"))]
     93     use alloc::{format, string::String, vec, vec::Vec};
     94     #[cfg(feature = "std")]
     95     use std::{format, string::String, vec::Vec};
     96 
     97     fn shard(file: &str, count: u32, sha256: &str) -> RadrootsEventsIndexedShardMetadata {
     98         RadrootsEventsIndexedShardMetadata {
     99             file: String::from(file),
    100             count,
    101             first_id: String::from("a"),
    102             last_id: String::from("b"),
    103             first_published_at: 0,
    104             last_published_at: 0,
    105             sha256: String::from(sha256),
    106         }
    107     }
    108 
    109     fn base_manifest() -> RadrootsEventsIndexedManifest {
    110         RadrootsEventsIndexedManifest {
    111             country: String::from("us"),
    112             total: 1,
    113             shard_size: 1,
    114             first_published_at: 0,
    115             last_published_at: 0,
    116             shards: vec![shard(
    117                 "a.json",
    118                 1,
    119                 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
    120             )],
    121         }
    122     }
    123 
    124     #[test]
    125     fn validate_manifest_rejects_empty_country() {
    126         let mut m = base_manifest();
    127         m.country = String::from(" ");
    128         let err = validate_manifest(&m).unwrap_err();
    129         assert_eq!(err, RadrootsEventsIndexedManifestError::EmptyCountry);
    130     }
    131 
    132     #[test]
    133     fn validate_manifest_rejects_empty_shards() {
    134         let mut m = base_manifest();
    135         m.shards = Vec::new();
    136         let err = validate_manifest(&m).unwrap_err();
    137         assert_eq!(err, RadrootsEventsIndexedManifestError::EmptyShards);
    138     }
    139 
    140     #[test]
    141     fn validate_manifest_rejects_empty_file() {
    142         let mut m = base_manifest();
    143         m.shards[0].file = String::from("");
    144         let err = validate_manifest(&m).unwrap_err();
    145         assert_eq!(err, RadrootsEventsIndexedManifestError::EmptyFile(0));
    146     }
    147 
    148     #[test]
    149     fn validate_manifest_rejects_invalid_sha256() {
    150         let mut m = base_manifest();
    151         m.shards[0].sha256 = String::from("zz");
    152         let err = validate_manifest(&m).unwrap_err();
    153         assert_eq!(err, RadrootsEventsIndexedManifestError::InvalidSha256(0));
    154     }
    155 
    156     #[test]
    157     fn validate_manifest_rejects_invalid_sha256_with_valid_length() {
    158         let mut m = base_manifest();
    159         m.shards[0].sha256 =
    160             String::from("g123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef");
    161         let err = validate_manifest(&m).unwrap_err();
    162         assert_eq!(err, RadrootsEventsIndexedManifestError::InvalidSha256(0));
    163     }
    164 
    165     #[test]
    166     fn validate_manifest_rejects_total_overflow() {
    167         let m = RadrootsEventsIndexedManifest {
    168             country: String::from("us"),
    169             total: 1,
    170             shard_size: 1,
    171             first_published_at: 0,
    172             last_published_at: 0,
    173             shards: vec![
    174                 RadrootsEventsIndexedShardMetadata {
    175                     file: String::from("a.json"),
    176                     count: u32::MAX,
    177                     first_id: String::from("a"),
    178                     last_id: String::from("b"),
    179                     first_published_at: 0,
    180                     last_published_at: 0,
    181                     sha256: String::from(
    182                         "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
    183                     ),
    184                 },
    185                 RadrootsEventsIndexedShardMetadata {
    186                     file: String::from("b.json"),
    187                     count: 1,
    188                     first_id: String::from("c"),
    189                     last_id: String::from("d"),
    190                     first_published_at: 0,
    191                     last_published_at: 0,
    192                     sha256: String::from(
    193                         "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210",
    194                     ),
    195                 },
    196             ],
    197         };
    198 
    199         let err = validate_manifest(&m).unwrap_err();
    200         assert_eq!(err, RadrootsEventsIndexedManifestError::InconsistentTotals);
    201     }
    202 
    203     #[test]
    204     fn validate_manifest_rejects_mismatched_total_without_overflow() {
    205         let mut m = base_manifest();
    206         m.total = 2;
    207         let err = validate_manifest(&m).unwrap_err();
    208         assert_eq!(err, RadrootsEventsIndexedManifestError::InconsistentTotals);
    209     }
    210 
    211     #[test]
    212     fn validate_manifest_accepts_consistent_totals() {
    213         let m = base_manifest();
    214         let result = validate_manifest(&m);
    215         assert!(result.is_ok());
    216     }
    217 
    218     #[test]
    219     fn manifest_error_display_messages_are_stable() {
    220         assert_eq!(
    221             format!("{}", RadrootsEventsIndexedManifestError::EmptyCountry),
    222             "country is empty"
    223         );
    224         assert_eq!(
    225             format!("{}", RadrootsEventsIndexedManifestError::EmptyShards),
    226             "no shards in manifest"
    227         );
    228         assert_eq!(
    229             format!("{}", RadrootsEventsIndexedManifestError::EmptyFile(3)),
    230             "shard 3 has empty file name"
    231         );
    232         assert_eq!(
    233             format!("{}", RadrootsEventsIndexedManifestError::InvalidSha256(4)),
    234             "shard 4 has invalid sha256"
    235         );
    236         assert_eq!(
    237             format!("{}", RadrootsEventsIndexedManifestError::InconsistentTotals),
    238             "total does not match sum of shard counts"
    239         );
    240     }
    241 }