event_head.rs (3511B)
1 #[cfg(not(feature = "std"))] 2 use alloc::format; 3 #[cfg(not(feature = "std"))] 4 use alloc::{ 5 string::{String, ToString}, 6 vec::Vec, 7 }; 8 #[cfg(feature = "std")] 9 use std::{string::String, vec::Vec}; 10 11 use serde_json::Value; 12 use sha2::{Digest, Sha256}; 13 14 #[cfg(test)] 15 use crate::error::RadrootsReplicaEventsError; 16 17 #[cfg(test)] 18 mod failpoints { 19 use std::cell::Cell; 20 21 thread_local! { 22 static FORCE_ERROR: Cell<bool> = const { Cell::new(false) }; 23 } 24 25 pub(crate) fn set_error() { 26 FORCE_ERROR.with(|flag| flag.set(true)); 27 } 28 29 pub(crate) fn take_error() -> bool { 30 FORCE_ERROR.with(|flag| { 31 let value = flag.get(); 32 flag.set(false); 33 value 34 }) 35 } 36 } 37 38 pub fn event_head_key(kind: u32, pubkey: &str, d_tag: &str) -> String { 39 format!("{kind}:{pubkey}:{d_tag}") 40 } 41 42 fn event_content_hash_value(content: &str, tags: &[Vec<String>]) -> String { 43 let tags_json = Value::Array( 44 tags.iter() 45 .map(|tag| Value::Array(tag.iter().cloned().map(Value::String).collect())) 46 .collect(), 47 ) 48 .to_string(); 49 let mut hasher = Sha256::new(); 50 hasher.update(content.as_bytes()); 51 hasher.update(tags_json.as_bytes()); 52 hex::encode(hasher.finalize()) 53 } 54 55 #[cfg(test)] 56 pub fn event_content_hash( 57 content: &str, 58 tags: &[Vec<String>], 59 ) -> Result<String, RadrootsReplicaEventsError> { 60 #[cfg(test)] 61 if failpoints::take_error() { 62 return Err(RadrootsReplicaEventsError::InvalidData( 63 "content_hash".to_string(), 64 )); 65 } 66 Ok(event_content_hash_value(content, tags)) 67 } 68 69 #[cfg(not(test))] 70 pub fn event_content_hash(content: &str, tags: &[Vec<String>]) -> String { 71 event_content_hash_value(content, tags) 72 } 73 74 #[cfg(test)] 75 pub(crate) fn event_content_hash_fail_next() { 76 failpoints::set_error(); 77 } 78 79 pub fn tag_value<'a>(tags: &'a [Vec<String>], key: &str) -> Option<&'a str> { 80 tags.iter() 81 .find(|tag| tag.first().map(|v| v.as_str()) == Some(key)) 82 .and_then(|tag| tag.get(1)) 83 .map(|value| value.as_str()) 84 } 85 86 #[cfg(test)] 87 mod tests { 88 use super::{event_content_hash, event_head_key, tag_value}; 89 90 #[test] 91 fn event_head_key_formats_consistently() { 92 let key = event_head_key(30000, "author", "d-tag"); 93 assert_eq!(key, "30000:author:d-tag"); 94 } 95 96 #[test] 97 fn event_content_hash_is_stable_for_same_inputs() { 98 let tags = vec![vec!["d".to_string(), "tag".to_string()]]; 99 let first = event_content_hash("content", &tags).expect("hash first"); 100 let second = event_content_hash("content", &tags).expect("hash second"); 101 assert_eq!(first, second); 102 assert_eq!(first.len(), 64); 103 } 104 105 #[test] 106 fn event_content_hash_reports_failpoint_errors() { 107 super::failpoints::set_error(); 108 let tags = vec![vec!["d".to_string(), "tag".to_string()]]; 109 let err = event_content_hash("content", &tags).expect_err("failpoint"); 110 assert!(err.to_string().contains("content_hash")); 111 } 112 113 #[test] 114 fn tag_value_finds_and_misses_keys() { 115 let tags = vec![ 116 vec!["p".to_string(), "member".to_string()], 117 vec!["d".to_string(), "farm".to_string()], 118 vec!["x".to_string()], 119 ]; 120 assert_eq!(tag_value(&tags, "p"), Some("member")); 121 assert_eq!(tag_value(&tags, "d"), Some("farm")); 122 assert_eq!(tag_value(&tags, "x"), None); 123 assert_eq!(tag_value(&tags, "missing"), None); 124 } 125 }