autopush_common/db/
models.rs

1use lazy_static::lazy_static;
2use regex::RegexSet;
3use std::collections::HashMap;
4
5use crate::errors::{ApcErrorKind, Result};
6use crate::notification::{STANDARD_NOTIFICATION_PREFIX, TOPIC_NOTIFICATION_PREFIX};
7use serde_derive::{Deserialize, Serialize};
8use uuid::Uuid;
9
10use crate::util::InsertOpt;
11
12/// Direct representation of an incoming subscription notification header set
13/// as we store it in the database.
14/// It is possible to have a "data free" notification, which does not have a
15/// message component, and thus, no headers.
16#[derive(Default, Deserialize, PartialEq, Debug, Clone, Serialize)]
17pub(crate) struct NotificationHeaders {
18    #[serde(skip_serializing_if = "Option::is_none")]
19    crypto_key: Option<String>,
20    #[serde(skip_serializing_if = "Option::is_none")]
21    encryption: Option<String>,
22    #[serde(skip_serializing_if = "Option::is_none")]
23    encryption_key: Option<String>,
24    #[serde(skip_serializing_if = "Option::is_none")]
25    encoding: Option<String>,
26}
27
28#[allow(clippy::implicit_hasher)]
29impl From<NotificationHeaders> for HashMap<String, String> {
30    fn from(val: NotificationHeaders) -> Self {
31        let mut map = Self::new();
32        map.insert_opt("crypto_key", val.crypto_key);
33        map.insert_opt("encryption", val.encryption);
34        map.insert_opt("encryption_key", val.encryption_key);
35        map.insert_opt("encoding", val.encoding);
36        map
37    }
38}
39
40impl From<HashMap<String, String>> for NotificationHeaders {
41    fn from(val: HashMap<String, String>) -> Self {
42        Self {
43            crypto_key: val.get("crypto_key").map(|v| v.to_string()),
44            encryption: val.get("encryption").map(|v| v.to_string()),
45            encryption_key: val.get("encryption_key").map(|v| v.to_string()),
46            encoding: val.get("encoding").map(|v| v.to_string()),
47        }
48    }
49}
50
51/// Contains some meta info regarding the message we're handling.
52#[derive(Debug)]
53pub(crate) struct RangeKey {
54    /// The channel_identifier
55    pub(crate) channel_id: Uuid,
56    /// The optional topic identifier
57    pub(crate) topic: Option<String>,
58    /// The encoded sortkey and timestamp
59    pub(crate) sortkey_timestamp: Option<u64>,
60    /// Which version of this message are we handling
61    #[allow(unused)]
62    pub(crate) legacy_version: Option<String>,
63}
64
65impl RangeKey {
66    /// read the custom sort_key and convert it into something the database can use.
67    pub(crate) fn parse_chidmessageid(key: &str) -> Result<RangeKey> {
68        lazy_static! {
69            static ref RE: RegexSet = RegexSet::new([
70                format!("^{TOPIC_NOTIFICATION_PREFIX}:\\S+:\\S+$").as_str(),
71                format!("^{STANDARD_NOTIFICATION_PREFIX}:\\d+:\\S+$").as_str(),
72                "^\\S{3,}:\\S+$"
73            ])
74            .unwrap();
75        }
76        if !RE.is_match(key) {
77            return Err(ApcErrorKind::GeneralError("Invalid chidmessageid".into()).into());
78        }
79
80        let v: Vec<&str> = key.split(':').collect();
81        match v[0] {
82            // This is a topic message (There Can Only Be One. <guitar riff>)
83            "01" => {
84                if v.len() != 3 {
85                    return Err(ApcErrorKind::GeneralError("Invalid topic key".into()).into());
86                }
87                let (channel_id, topic) = (v[1], v[2]);
88                let channel_id = Uuid::parse_str(channel_id)?;
89                Ok(RangeKey {
90                    channel_id,
91                    topic: Some(topic.to_string()),
92                    sortkey_timestamp: None,
93                    legacy_version: None,
94                })
95            }
96            // A "normal" pending message.
97            "02" => {
98                if v.len() != 3 {
99                    return Err(ApcErrorKind::GeneralError("Invalid topic key".into()).into());
100                }
101                let (sortkey, channel_id) = (v[1], v[2]);
102                let channel_id = Uuid::parse_str(channel_id)?;
103                Ok(RangeKey {
104                    channel_id,
105                    topic: None,
106                    sortkey_timestamp: Some(sortkey.parse()?),
107                    legacy_version: None,
108                })
109            }
110            // Ok, that's odd, but try to make some sense of it.
111            // (This is a bit of legacy code that we should be
112            // able to drop.)
113            _ => {
114                if v.len() != 2 {
115                    return Err(ApcErrorKind::GeneralError("Invalid topic key".into()).into());
116                }
117                let (channel_id, legacy_version) = (v[0], v[1]);
118                let channel_id = Uuid::parse_str(channel_id)?;
119                Ok(RangeKey {
120                    channel_id,
121                    topic: None,
122                    sortkey_timestamp: None,
123                    legacy_version: Some(legacy_version.to_string()),
124                })
125            }
126        }
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use crate::db::RangeKey;
133    use crate::util::us_since_epoch;
134    use uuid::Uuid;
135
136    #[test]
137    fn test_parse_sort_key_ver1() {
138        let chid = Uuid::new_v4();
139        let chidmessageid = format!("01:{}:mytopic", chid.hyphenated());
140        let key = RangeKey::parse_chidmessageid(&chidmessageid).unwrap();
141        assert_eq!(key.topic, Some("mytopic".to_string()));
142        assert_eq!(key.channel_id, chid);
143        assert_eq!(key.sortkey_timestamp, None);
144    }
145
146    #[test]
147    fn test_parse_sort_key_ver2() {
148        let chid = Uuid::new_v4();
149        let sortkey_timestamp = us_since_epoch();
150        let chidmessageid = format!("02:{}:{}", sortkey_timestamp, chid.hyphenated());
151        let key = RangeKey::parse_chidmessageid(&chidmessageid).unwrap();
152        assert_eq!(key.topic, None);
153        assert_eq!(key.channel_id, chid);
154        assert_eq!(key.sortkey_timestamp, Some(sortkey_timestamp));
155    }
156
157    #[test]
158    fn test_parse_sort_key_bad_values() {
159        for val in &["02j3i2o", "03:ffas:wef", "01::mytopic", "02:oops:ohnoes"] {
160            let key = RangeKey::parse_chidmessageid(val);
161            assert!(key.is_err());
162        }
163    }
164}