autopush_common/db/
models.rs

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