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
10pub 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 let hash = params.get("hash").unwrap(); 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 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}