From 179928378ec8f368f4a38d2af67e080adc7fee19 Mon Sep 17 00:00:00 2001 From: Raj <36541669+rajarshimaitra@users.noreply.github.com> Date: Wed, 19 Jan 2022 19:12:58 +0530 Subject: [PATCH] refactor: add strictly typed tags * Add custom error variant This can be useful to propagate errors not conforming to available variants. Also to convert other errors in `crate::Error` without having explicit conversion defined, with `error.to_string()` * Implement `Tag` and define protocol serialization A Tag structure have been implemented with dedicated field types. Then custom serde serialization is derived to map the structure to current protocol json array as per NIP01. This adds compile and run time type checking to always ensure wrong string data are never stored or processed. With strict typed fields and custom serde derivation this checks can be done at time of serialization, saving work for internal handling of the actual data. tests for possible data violations are added, and gives good example of kind of errors it will through for different cases. * Use String for URL --- src/error.rs | 2 + src/lib.rs | 1 + src/tags.rs | 315 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 318 insertions(+) create mode 100644 src/tags.rs diff --git a/src/error.rs b/src/error.rs index a0842de..b42b16f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -40,6 +40,8 @@ pub enum Error { ConfigError(config::ConfigError), #[error("Data directory does not exist")] DatabaseDirError, + #[error("Custom Error : {0}")] + CustomError(String), } impl From for Error { diff --git a/src/lib.rs b/src/lib.rs index 741528f..e8d5b03 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,3 +7,4 @@ pub mod event; pub mod info; pub mod protostream; pub mod subscription; +pub mod tags; diff --git a/src/tags.rs b/src/tags.rs new file mode 100644 index 0000000..dcaecfd --- /dev/null +++ b/src/tags.rs @@ -0,0 +1,315 @@ +//! Tags used in events to link to another event or a pubkey +//! +//! Reference specification NIP01: https://github.com/fiatjaf/nostr/blob/master/nips/01.md#events-and-signatures +//! + +use bitcoin_hashes::hex::ToHex; +use bitcoin_hashes::sha256; +use secp256k1::XOnlyPublicKey; +use std::str::FromStr; + +use serde::de::Unexpected; +use serde::ser::SerializeSeq; +use serde::{Deserialize, Deserializer, Serializer}; +use serde_json::Value; + +use crate::error::Error; + +type EventId = sha256::Hash; + +#[derive(Debug, PartialEq)] +struct EventTag { + event_id: EventId, + recommended_url: Option, +} + +#[derive(Debug, PartialEq)] +struct PubkeyTag { + pubkey: XOnlyPublicKey, + recommended_url: Option, +} + +// Tag structure representing two possible types of tags +#[derive(Debug, PartialEq)] +enum Tag { + Event(EventTag), + Pubkey(PubkeyTag), +} + +// Custom json serialization into protocol network format +// Event tag : ["e", "<32 byte event-id>", "optional"] +// Pubkey tag : ["p", "<32 byte Xonly Pubkey>", "optional"] +impl serde::Serialize for Tag { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + Self::Event(event_tag) => { + let mut seq = serializer.serialize_seq(None)?; + seq.serialize_element("e")?; + seq.serialize_element(&event_tag.event_id.to_hex())?; + if let Some(url) = &event_tag.recommended_url { + seq.serialize_element(url)?; + } else { + } + seq.end() + } + Self::Pubkey(pubkey_tag) => { + let mut seq = serializer.serialize_seq(None)?; + seq.serialize_element("p")?; + seq.serialize_element(&pubkey_tag.pubkey.to_hex())?; + if let Some(url) = &pubkey_tag.recommended_url { + seq.serialize_element(url)?; + } else { + } + seq.end() + } + } + } +} + +// Custom json deserialization from protocol network format +impl<'de> serde::Deserialize<'de> for Tag { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + // Receive incoming data in a json object + let received: Value = Deserialize::deserialize(deserializer)?; + + // Check received data is a json array + let values = received.as_array().ok_or_else(|| { + serde::de::Error::invalid_type(Unexpected::Other("tag json object"), &"json array") + })?; + + // Check json array contains only string + let values = values + .iter() + .map(|value| { + value.as_str().ok_or_else(|| { + serde::de::Error::invalid_type( + Unexpected::Other("tag json data"), + &"json string", + ) + }) + }) + .collect::, _>>()?; + + // Check length is not more tha 3 + if values.len() > 3 { + Err(serde::de::Error::invalid_length( + values.len(), + &"tag length is 2 or 3", + )) + } else { + // Parse the json array into appropriate types + match values[0] { + // This denotes an event type tag + "e" => { + let event_id = EventId::from_str(values[1]) + .map_err(|e| serde::de::Error::custom(e.to_string()))?; + let recomended_url = if values.len() == 3 { + Some(values[2].into()) + } else { + None + }; + Ok(Tag::Event(EventTag { + event_id, + recommended_url: recomended_url, + })) + } + // This denotes a pubkey type tag + "p" => { + let pubkey = XOnlyPublicKey::from_str(values[1]) + .map_err(|e| serde::de::Error::custom(e.to_string()))?; + let recomended_url = if values.len() == 3 { + Some(values[2].into()) + } else { + None + }; + + Ok(Tag::Pubkey(PubkeyTag { + pubkey, + recommended_url: recomended_url, + })) + } + // Any other tag type is currently not supported + _ => Err(serde::de::Error::invalid_value( + Unexpected::Other("tag type flag"), + &"'e' or 'p'", + )), + } + } + } +} + +#[allow(dead_code)] +// Some api (not public currently) to use Tags +impl Tag { + fn new_event_tag(event_id: EventId, recomended_url: Option) -> Self { + Self::Event(EventTag { + event_id, + recommended_url: recomended_url, + }) + } + + fn new_pubkey_tag(pubkey: XOnlyPublicKey, recomended_url: Option) -> Self { + Self::Pubkey(PubkeyTag { + pubkey, + recommended_url: recomended_url, + }) + } + + fn get_recomended_url(&self) -> Option { + match self { + Self::Event(EventTag { + event_id: _, + recommended_url, + }) => recommended_url.clone(), + Self::Pubkey(PubkeyTag { + pubkey: _, + recommended_url, + }) => recommended_url.clone(), + } + } + + fn get_event_id(&self) -> Result { + match self { + Self::Event(EventTag { + event_id, + recommended_url: _, + }) => Ok(*event_id), + _ => Err(Error::CustomError("Expected event tag".to_string())), + } + } + + fn get_pubkey(&self) -> Result { + match self { + Self::Pubkey(PubkeyTag { + pubkey, + recommended_url: _, + }) => Ok(*pubkey), + _ => Err(Error::CustomError("Expected pubkey tag".to_string())), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use bitcoin_hashes::Hash; + + #[test] + fn serde_roundtrip() { + let pubkey = XOnlyPublicKey::from_str( + "18845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166", + ) + .unwrap(); + let url = "wss://rsslay.fiatjaf.com"; + static HASH_BYTES: [u8; 32] = [ + 0xef, 0x53, 0x7f, 0x25, 0xc8, 0x95, 0xbf, 0xa7, 0x82, 0x52, 0x65, 0x29, 0xa9, 0xb6, + 0x3d, 0x97, 0xaa, 0x63, 0x15, 0x64, 0xd5, 0xd7, 0x89, 0xc2, 0xb7, 0x65, 0x44, 0x8c, + 0x86, 0x35, 0xfb, 0x6c, + ]; + let event_id = EventId::from_slice(&HASH_BYTES).expect("right number of bytes"); + + let event_tag = Tag::new_event_tag(event_id, Some(url.to_string())); + let pubkey_tag = Tag::new_pubkey_tag(pubkey, Some(url.to_string())); + + let ser_event_tag = serde_json::to_string(&event_tag).unwrap(); + let ser_pubkey_tag = serde_json::to_string(&pubkey_tag).unwrap(); + + let deser_event_tag: Tag = serde_json::from_str(&ser_event_tag).unwrap(); + let deser_pubkey_tag: Tag = serde_json::from_str(&ser_pubkey_tag).unwrap(); + + assert_eq!(deser_event_tag, event_tag); + assert_eq!(deser_pubkey_tag, pubkey_tag); + } + + #[test] + fn invalid_pubkey() { + let test_string = r#"["p","845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166","wss://rsslay.fiatjaf.com"]"#; + let tag: Result = serde_json::from_str(test_string); + assert_eq!( + tag.err().expect("expect error").to_string(), + "secp: malformed public key".to_string() + ); + } + + #[test] + fn invalid_datatype() { + let test_string = r#"["p", 188457, "wss://rsslay.fiatjaf.com"]"#; + let tag: Result = serde_json::from_str(test_string); + assert_eq!( + tag.err().expect("expect error").to_string(), + "invalid type: tag json data, expected json string".to_string() + ); + } + + #[test] + fn invalid_tagbyte() { + let test_string = r#"["q", "18845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166", "wss://rsslay.fiatjaf.com"]"#; + let tag: Result = serde_json::from_str(test_string); + assert_eq!( + tag.err().expect("expect error").to_string(), + "invalid value: tag type flag, expected 'e' or 'p'".to_string() + ); + } + + #[test] + fn invalid_event_id() { + let test_string = r#"["e","ef537f25c895bfa782526529a9b63d97aa631564d5d78c8635fb6c","wss://rsslay.fiatjaf.com"]"#; + let tag: Result = serde_json::from_str(test_string); + assert_eq!( + tag.err().expect("expect error").to_string(), + "bad hex string length 54 (expected 64)".to_string() + ); + } + + #[test] + fn invalid_url_type() { + let test_string = r#"["e","ef537f25c895bfa782526529a9b63d97aa631564d5d789c2b765448c8635fb6c", 123456788]"#; + let tag: Result = serde_json::from_str(test_string); + assert_eq!( + tag.err().expect("expect error").to_string(), + "invalid type: tag json data, expected json string".to_string() + ); + } + + #[test] + fn invalid_length() { + let test_string = r#"["e","ef537f25c895bfa782526529a9b63d97aa631564d5d789c2b765448c8635fb6c","wss://rsslay.fiatjaf.com", "Random extra data"]"#; + let tag: Result = serde_json::from_str(test_string); + assert_eq!( + tag.err().expect("expect error").to_string(), + "invalid length 4, expected tag length is 2 or 3".to_string() + ); + } + + #[test] + fn invalid_json_type() { + let test_string = r#"{"type": "e","event": "ef537f25c895bfa782526529a9b63d97aa631564d5d789c2b765448c8635fb6c"}"#; + let tag: Result = serde_json::from_str(test_string); + assert_eq!( + tag.err().expect("expect error").to_string(), + "invalid type: tag json object, expected json array".to_string() + ); + } + + #[test] + fn evenrt_tag_missing_url() { + let test_string = + r#"["e","ef537f25c895bfa782526529a9b63d97aa631564d5d789c2b765448c8635fb6c"]"#; + let tag: Tag = serde_json::from_str(test_string).unwrap(); + assert!(tag.get_recomended_url().is_none()); + } + + #[test] + fn pubkey_tag_missing_url() { + let test_string = + r#"["p","18845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166"]"#; + let tag: Tag = serde_json::from_str(test_string).unwrap(); + assert!(tag.get_recomended_url().is_none()); + } +}