imessage_database/util/
query_context.rs1use std::collections::BTreeSet;
5
6use chrono::prelude::*;
7
8use crate::{
9 error::query_context::QueryContextError,
10 util::dates::{TIMESTAMP_FACTOR, get_offset},
11};
12
13#[derive(Debug, Default, PartialEq, Eq)]
14pub struct QueryContext {
16 pub start: Option<i64>,
18 pub end: Option<i64>,
20 pub selected_handle_ids: Option<BTreeSet<i32>>,
22 pub selected_chat_ids: Option<BTreeSet<i32>>,
24}
25
26impl QueryContext {
27 pub fn set_start(&mut self, start: &str) -> Result<(), QueryContextError> {
37 let timestamp = QueryContext::sanitize_date(start)
38 .ok_or(QueryContextError::InvalidDate(start.to_string()))?;
39 self.start = Some(timestamp);
40 Ok(())
41 }
42
43 pub fn set_end(&mut self, end: &str) -> Result<(), QueryContextError> {
53 let timestamp = QueryContext::sanitize_date(end)
54 .ok_or(QueryContextError::InvalidDate(end.to_string()))?;
55 self.end = Some(timestamp);
56 Ok(())
57 }
58
59 pub fn set_selected_handle_ids(&mut self, selected_handle_ids: BTreeSet<i32>) {
71 self.selected_handle_ids = (!selected_handle_ids.is_empty()).then_some(selected_handle_ids);
72 }
73
74 pub fn set_selected_chat_ids(&mut self, selected_chat_ids: BTreeSet<i32>) {
86 self.selected_chat_ids = (!selected_chat_ids.is_empty()).then_some(selected_chat_ids);
87 }
88
89 fn sanitize_date(date: &str) -> Option<i64> {
91 if date.len() < 9 {
92 return None;
93 }
94
95 let year = date.get(0..4)?.parse::<i32>().ok()?;
96
97 if !date.get(4..5)?.eq("-") {
98 return None;
99 }
100
101 let month = date.get(5..7)?.parse::<u32>().ok()?;
102 if month > 12 {
103 return None;
104 }
105
106 if !date.get(7..8)?.eq("-") {
107 return None;
108 }
109
110 let day = date.get(8..)?.parse::<u32>().ok()?;
111 if day > 31 {
112 return None;
113 }
114
115 let local = Local.with_ymd_and_hms(year, month, day, 0, 0, 0).single()?;
116 let stamp = local.timestamp_nanos_opt().unwrap_or(0);
117
118 Some(stamp - (get_offset() * TIMESTAMP_FACTOR))
119 }
120
121 #[must_use]
134 pub fn has_filters(&self) -> bool {
135 self.start.is_some()
136 || self.end.is_some()
137 || self.selected_chat_ids.is_some()
138 || self.selected_handle_ids.is_some()
139 }
140}
141
142#[cfg(test)]
143mod use_tests {
144 use chrono::prelude::*;
145
146 use crate::util::{
147 dates::{TIMESTAMP_FACTOR, format, get_offset},
148 query_context::QueryContext,
149 };
150
151 #[test]
152 fn can_create() {
153 let context = QueryContext::default();
154 assert!(context.start.is_none());
155 assert!(context.end.is_none());
156 assert!(!context.has_filters());
157 }
158
159 #[test]
160 fn can_create_start() {
161 let mut context = QueryContext::default();
162 context.set_start("2020-01-01").unwrap();
163
164 let from_timestamp = DateTime::from_timestamp(
165 (context.start.unwrap() / TIMESTAMP_FACTOR) + get_offset(),
166 0,
167 )
168 .unwrap()
169 .naive_utc();
170 let local = Local.from_utc_datetime(&from_timestamp);
171
172 assert_eq!(format(&Ok(local)), "Jan 01, 2020 12:00:00 AM");
173 assert!(context.start.is_some());
174 assert!(context.end.is_none());
175 assert!(context.has_filters());
176 }
177
178 #[test]
179 fn can_create_end() {
180 let mut context = QueryContext::default();
181 context.set_end("2020-01-01").unwrap();
182
183 let from_timestamp =
184 DateTime::from_timestamp((context.end.unwrap() / TIMESTAMP_FACTOR) + get_offset(), 0)
185 .unwrap()
186 .naive_utc();
187 let local = Local.from_utc_datetime(&from_timestamp);
188
189 assert_eq!(format(&Ok(local)), "Jan 01, 2020 12:00:00 AM");
190 assert!(context.start.is_none());
191 assert!(context.end.is_some());
192 assert!(context.has_filters());
193 }
194
195 #[test]
196 fn can_create_both() {
197 let mut context = QueryContext::default();
198 context.set_start("2020-01-01").unwrap();
199 context.set_end("2020-02-02").unwrap();
200
201 let from_timestamp = DateTime::from_timestamp(
202 (context.start.unwrap() / TIMESTAMP_FACTOR) + get_offset(),
203 0,
204 )
205 .unwrap()
206 .naive_utc();
207 let local_start = Local.from_utc_datetime(&from_timestamp);
208
209 let from_timestamp =
210 DateTime::from_timestamp((context.end.unwrap() / TIMESTAMP_FACTOR) + get_offset(), 0)
211 .unwrap()
212 .naive_utc();
213 let local_end = Local.from_utc_datetime(&from_timestamp);
214
215 assert_eq!(format(&Ok(local_start)), "Jan 01, 2020 12:00:00 AM");
216 assert_eq!(format(&Ok(local_end)), "Feb 02, 2020 12:00:00 AM");
217 assert!(context.start.is_some());
218 assert!(context.end.is_some());
219 assert!(context.has_filters());
220 }
221}
222
223#[cfg(test)]
224mod id_tests {
225 use std::collections::BTreeSet;
226
227 use crate::util::query_context::QueryContext;
228
229 #[test]
230 fn test_can_set_selected_chat_ids() {
231 let mut qc = QueryContext::default();
232 qc.set_selected_chat_ids(BTreeSet::from([1, 2, 3]));
233
234 assert_eq!(qc.selected_chat_ids, Some(BTreeSet::from([1, 2, 3])));
235 assert!(qc.has_filters());
236 }
237
238 #[test]
239 fn test_can_set_selected_chat_ids_empty() {
240 let mut qc = QueryContext::default();
241 qc.set_selected_chat_ids(BTreeSet::new());
242
243 assert_eq!(qc.selected_chat_ids, None);
244 assert!(!qc.has_filters());
245 }
246
247 #[test]
248 fn test_can_overwrite_selected_chat_ids_empty() {
249 let mut qc = QueryContext::default();
250 qc.set_selected_chat_ids(BTreeSet::from([1, 2, 3]));
251 qc.set_selected_chat_ids(BTreeSet::new());
252
253 assert_eq!(qc.selected_chat_ids, None);
254 assert!(!qc.has_filters());
255 }
256
257 #[test]
258 fn test_can_set_selected_handle_ids() {
259 let mut qc = QueryContext::default();
260 qc.set_selected_handle_ids(BTreeSet::from([1, 2, 3]));
261
262 assert_eq!(qc.selected_handle_ids, Some(BTreeSet::from([1, 2, 3])));
263 assert!(qc.has_filters());
264 }
265
266 #[test]
267 fn test_can_set_selected_handle_ids_empty() {
268 let mut qc = QueryContext::default();
269 qc.set_selected_handle_ids(BTreeSet::new());
270
271 assert_eq!(qc.selected_handle_ids, None);
272 assert!(!qc.has_filters());
273 }
274
275 #[test]
276 fn test_can_overwrite_selected_handle_ids_empty() {
277 let mut qc = QueryContext::default();
278 qc.set_selected_handle_ids(BTreeSet::from([1, 2, 3]));
279 qc.set_selected_handle_ids(BTreeSet::new());
280
281 assert_eq!(qc.selected_handle_ids, None);
282 assert!(!qc.has_filters());
283 }
284}
285
286#[cfg(test)]
287mod sanitize_tests {
288 use crate::util::query_context::QueryContext;
289
290 #[test]
291 fn can_sanitize_good() {
292 let res = QueryContext::sanitize_date("2020-01-01");
293 assert!(res.is_some());
294 }
295
296 #[test]
297 fn can_reject_bad_short() {
298 let res = QueryContext::sanitize_date("1-1-20");
299 assert!(res.is_none());
300 }
301
302 #[test]
303 fn can_reject_bad_order() {
304 let res = QueryContext::sanitize_date("01-01-2020");
305 assert!(res.is_none());
306 }
307
308 #[test]
309 fn can_reject_bad_month() {
310 let res = QueryContext::sanitize_date("2020-31-01");
311 assert!(res.is_none());
312 }
313
314 #[test]
315 fn can_reject_bad_day() {
316 let res = QueryContext::sanitize_date("2020-01-32");
317 assert!(res.is_none());
318 }
319
320 #[test]
321 fn can_reject_bad_data() {
322 let res = QueryContext::sanitize_date("2020-AB-CD");
323 assert!(res.is_none());
324 }
325
326 #[test]
327 fn can_reject_wrong_hyphen() {
328 let res = QueryContext::sanitize_date("2020–01–01");
329 assert!(res.is_none());
330 }
331}