init_data_rs/
parse.rs

1use serde_json::Value;
2use std::collections::BTreeMap;
3use url::form_urlencoded;
4
5use crate::error::InitDataError;
6use crate::model::InitData;
7
8const STRING_PROPS: [&str; 1] = ["start_param"];
9
10/// Parse converts passed init data presented as query string to `InitData` object.
11///
12/// # Errors
13///
14/// This function returns an `Err` in one of the following cases:
15///
16/// - `auth_date` is missing
17/// - hash is missing
18/// - hash is invalid
19/// - init data has unexpected format
20/// - signature is invalid
21/// - the library has an internal error while hmac-ing the string. this should never happen
22///
23/// # Panics
24///
25/// This function will panic if the hash field is missing from the parameters after
26/// the initial existence check. This should never happen in normal usage as the
27/// function returns an error before reaching the unwrap call.
28pub fn parse(init_data: &str) -> Result<InitData, InitDataError> {
29    if init_data.is_empty() {
30        return Err(InitDataError::UnexpectedFormat("init_data is empty".to_string()));
31    }
32
33    if init_data.contains(';') || !init_data.contains('=') {
34        return Err(InitDataError::UnexpectedFormat(
35            "Invalid query string format".to_string(),
36        ));
37    }
38
39    let pairs = form_urlencoded::parse(init_data.as_bytes());
40    let mut params: BTreeMap<String, String> = BTreeMap::new();
41
42    for (key, value) in pairs {
43        params.insert(key.to_string(), value.into_owned());
44    }
45
46    if !params.contains_key("auth_date") {
47        return Err(InitDataError::AuthDateMissing);
48    }
49
50    if !params.contains_key("hash") {
51        return Err(InitDataError::HashMissing);
52    }
53
54    // Validate hash format (should be a 64-character hex string)
55    let hash = params.get("hash").unwrap(); // Safe to unwrap since we checked existence above
56    if hash.len() != 64 || !hash.chars().all(|c| c.is_ascii_hexdigit()) {
57        return Err(InitDataError::HashInvalid);
58    }
59
60    if let Some(signature) = params.get("signature") {
61        // Basic signature format validation (should be base64 URL-safe or hex)
62        // Allow base64 URL-safe characters (A-Z, a-z, 0-9, -, _) and standard base64 characters (+, /, =)
63        if !signature
64            .chars()
65            .all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '=' || c == '-' || c == '_')
66        {
67            return Err(InitDataError::SignatureInvalid("Invalid signature format".to_string()));
68        }
69    }
70
71    let json_pairs: Vec<String> = params
72        .iter()
73        .map(|(k, v)| {
74            if !STRING_PROPS.contains(&k.as_str()) && serde_json::from_str::<Value>(v).is_ok() {
75                format!("\"{k}\":{v}")
76            } else {
77                format!("\"{k}\":\"{}\"", v.replace('\"', "\\\""))
78            }
79        })
80        .collect();
81
82    let json_str = format!("{{{}}}", json_pairs.join(","));
83
84    let result =
85        serde_json::from_str::<InitData>(&json_str).map_err(|err| InitDataError::UnexpectedFormat(err.to_string()))?;
86
87    Ok(result)
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use crate::model::ChatType;
94
95    const PARSE_TEST_INIT_DATA: &str = "user=%7B%22id%22%3A6601562775%2C%22first_name%22%3A%22%29%22%2C%22last_name%22%3A%22%22%2C%22username%22%3A%22trogloditik%22%2C%22language_code%22%3A%22en%22%2C%22allows_write_to_pm%22%3Atrue%2C%22photo_url%22%3A%22https%3A%5C%2F%5C%2Ft.me%5C%2Fi%5C%2Fuserpic%5C%2F320%5C%2FqABgrvbhV8g_iUjd_pSUuX1bBuXefFmspMjb57gedoGAKDPx5fxwEMIF8k62mWhS.svg%22%7D&chat_instance=-8599080687359297588&chat_type=sender&auth_date=1748683232&signature=5rhZg9sshLtKrdTSwGvXA60MRmqtfU0RPTmUIAdcOEAm2n1XRfQhf0hvQNZo9Nwx4G3Kk92RSelu_CrPzra7Aw&hash=c8fdc0e1608154171a77ef4ce838d114b0229d891ee55ac1ee566f14551433e8";
96
97    #[test]
98    fn test_parse_invalid_format() {
99        let result = parse(&format!("{PARSE_TEST_INIT_DATA};"));
100        assert!(matches!(result, Err(InitDataError::UnexpectedFormat(_))));
101
102        assert!(matches!(parse("invalid"), Err(InitDataError::UnexpectedFormat(_))));
103        assert!(matches!(parse("a;b;c"), Err(InitDataError::UnexpectedFormat(_))));
104    }
105
106    #[test]
107    fn test_parse_valid_data() {
108        let result = parse(PARSE_TEST_INIT_DATA).unwrap();
109
110        assert_eq!(result.query_id, None);
111        assert_eq!(result.auth_date, 1748683232);
112        assert_eq!(result.start_param, None);
113        assert_eq!(
114            result.hash,
115            "c8fdc0e1608154171a77ef4ce838d114b0229d891ee55ac1ee566f14551433e8"
116        );
117
118        if let Some(user) = result.user {
119            assert_eq!(user.id, 6601562775);
120            assert_eq!(user.first_name, ")");
121            assert_eq!(user.last_name, Some(String::new()));
122            assert_eq!(user.username, Some("trogloditik".to_string()));
123            assert_eq!(user.language_code, Some("en".to_string()));
124            assert_eq!(user.is_premium, None);
125        } else {
126            panic!("User should be present");
127        }
128    }
129
130    #[test]
131    fn test_parse_empty_data() {
132        let result = parse("");
133        assert!(matches!(result, Err(InitDataError::UnexpectedFormat(_))));
134    }
135
136    #[test]
137    fn test_parse_with_chat() {
138        let init_data = "chat=%7B%22id%22%3A-100123456789%2C%22type%22%3A%22supergroup%22%2C%22title%22%3A%22Test%20Group%22%7D&auth_date=1748683232&signature=abc&hash=c8fdc0e1608154171a77ef4ce838d114b0229d891ee55ac1ee566f14551433e8";
139        let result = parse(init_data).unwrap();
140
141        if let Some(chat) = result.chat {
142            assert_eq!(chat.id, -100123456789);
143            assert!(matches!(chat.chat_type, ChatType::Supergroup));
144            assert_eq!(chat.title, "Test Group");
145        } else {
146            panic!("Chat should be present");
147        }
148    }
149
150    #[test]
151    fn test_parse_start_param() {
152        let init_data = "start_param=test123&auth_date=1748683232&signature=abc&hash=c8fdc0e1608154171a77ef4ce838d114b0229d891ee55ac1ee566f14551433e8";
153        let result = parse(init_data).unwrap();
154        assert_eq!(result.start_param, Some("test123".to_string()));
155    }
156
157    #[test]
158    fn test_parse_missing_auth_date() {
159        let init_data = "hash=c8fdc0e1608154171a77ef4ce838d114b0229d891ee55ac1ee566f14551433e8";
160        let result = parse(init_data);
161        assert!(matches!(result, Err(InitDataError::AuthDateMissing)));
162    }
163
164    #[test]
165    fn test_parse_missing_hash() {
166        let init_data = "auth_date=1662771648";
167        let result = parse(init_data);
168        assert!(matches!(result, Err(InitDataError::HashMissing)));
169    }
170
171    #[test]
172    fn test_parse_invalid_hash() {
173        let init_data = "auth_date=1662771648&hash=invalid_hash";
174        let result = parse(init_data);
175        assert!(matches!(result, Err(InitDataError::HashInvalid)));
176    }
177
178    #[test]
179    fn test_parse_invalid_signature() {
180        let init_data = "auth_date=1662771648&hash=c8fdc0e1608154171a77ef4ce838d114b0229d891ee55ac1ee566f14551433e8&signature=invalid!signature";
181        let result = parse(init_data);
182        assert!(matches!(result, Err(InitDataError::SignatureInvalid(_))));
183    }
184
185    #[test]
186    fn test_parse_invalid_auth_date_format() {
187        let init_data = "auth_date=not_a_number&hash=c8fdc0e1608154171a77ef4ce838d114b0229d891ee55ac1ee566f14551433e8";
188        let result = parse(init_data);
189        assert!(matches!(result, Err(InitDataError::UnexpectedFormat(_))));
190    }
191}