autofill/db/
passports.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2* License, v. 2.0. If a copy of the MPL was not distributed with this
3* file, You can obtain one at http://mozilla.org/MPL/2.0/.
4*/
5
6use crate::db::{
7    models::{
8        passport::{InternalPassport, UpdatablePassportFields},
9        Metadata,
10    },
11    schema::{PASSPORT_COMMON_COLS, PASSPORT_COMMON_VALS},
12};
13use crate::error::*;
14
15use rusqlite::{Connection, Transaction};
16use sync_guid::Guid;
17use types::Timestamp;
18
19pub(crate) fn add_passport(
20    conn: &Connection,
21    new: UpdatablePassportFields,
22) -> Result<InternalPassport> {
23    let tx = conn.unchecked_transaction()?;
24    let now = Timestamp::now();
25
26    let passport = InternalPassport {
27        guid: Guid::random(),
28        name: new.name,
29        country: new.country,
30        passport_number: new.passport_number,
31        issue_date_month: new.issue_date_month,
32        issue_date_day: new.issue_date_day,
33        issue_date_year: new.issue_date_year,
34        expiry_date_month: new.expiry_date_month,
35        expiry_date_day: new.expiry_date_day,
36        expiry_date_year: new.expiry_date_year,
37        metadata: Metadata {
38            time_created: now,
39            time_last_modified: now,
40            ..Default::default()
41        },
42    };
43    add_internal_passport(&tx, &passport)?;
44    tx.commit()?;
45    Ok(passport)
46}
47
48fn add_internal_passport(tx: &Transaction<'_>, passport: &InternalPassport) -> Result<()> {
49    tx.execute(
50        &format!(
51            "INSERT INTO passports_data (
52                {common_cols},
53                sync_change_counter
54            ) VALUES (
55                {common_vals},
56                :sync_change_counter
57            )",
58            common_cols = PASSPORT_COMMON_COLS,
59            common_vals = PASSPORT_COMMON_VALS,
60        ),
61        rusqlite::named_params! {
62            ":guid": passport.guid,
63            ":name": passport.name,
64            ":country": passport.country,
65            ":passport_number": passport.passport_number,
66            ":issue_date_month": passport.issue_date_month,
67            ":issue_date_day": passport.issue_date_day,
68            ":issue_date_year": passport.issue_date_year,
69            ":expiry_date_month": passport.expiry_date_month,
70            ":expiry_date_day": passport.expiry_date_day,
71            ":expiry_date_year": passport.expiry_date_year,
72            ":time_created": passport.metadata.time_created,
73            ":time_last_used": passport.metadata.time_last_used,
74            ":time_last_modified": passport.metadata.time_last_modified,
75            ":times_used": passport.metadata.times_used,
76            ":sync_change_counter": passport.metadata.sync_change_counter,
77        },
78    )?;
79    Ok(())
80}
81
82pub(crate) fn get_passport(conn: &Connection, guid: &Guid) -> Result<InternalPassport> {
83    let sql = format!(
84        "SELECT
85            {common_cols},
86            sync_change_counter
87        FROM passports_data
88        WHERE guid = :guid",
89        common_cols = PASSPORT_COMMON_COLS
90    );
91    conn.query_row(&sql, [guid], InternalPassport::from_row)
92        .map_err(|e| match e {
93            rusqlite::Error::QueryReturnedNoRows => Error::NoSuchRecord(guid.to_string()),
94            e => e.into(),
95        })
96}
97
98pub(crate) fn get_all_passports(conn: &Connection) -> Result<Vec<InternalPassport>> {
99    let sql = format!(
100        "SELECT
101            {common_cols},
102            sync_change_counter
103        FROM passports_data",
104        common_cols = PASSPORT_COMMON_COLS
105    );
106    let mut stmt = conn.prepare(&sql)?;
107    let passports = stmt
108        .query_map([], InternalPassport::from_row)?
109        .collect::<std::result::Result<Vec<InternalPassport>, _>>()?;
110    Ok(passports)
111}
112
113pub(crate) fn count_all_passports(conn: &Connection) -> Result<i64> {
114    let sql = "SELECT COUNT(*) FROM passports_data";
115    let mut stmt = conn.prepare(sql)?;
116    let count: i64 = stmt.query_row([], |row| row.get(0))?;
117    Ok(count)
118}
119
120/// Updates just the "updatable" columns - suitable for exposure as a public
121/// API.
122pub(crate) fn update_passport(
123    conn: &Connection,
124    guid: &Guid,
125    passport: &UpdatablePassportFields,
126) -> Result<()> {
127    let tx = conn.unchecked_transaction()?;
128    tx.execute(
129        "UPDATE passports_data
130        SET name               = :name,
131            country            = :country,
132            passport_number    = :passport_number,
133            issue_date_month   = :issue_date_month,
134            issue_date_day     = :issue_date_day,
135            issue_date_year    = :issue_date_year,
136            expiry_date_month  = :expiry_date_month,
137            expiry_date_day    = :expiry_date_day,
138            expiry_date_year   = :expiry_date_year,
139            time_last_modified = :time_last_modified,
140            sync_change_counter = sync_change_counter + 1
141        WHERE guid             = :guid",
142        rusqlite::named_params! {
143            ":name": passport.name,
144            ":country": passport.country,
145            ":passport_number": passport.passport_number,
146            ":issue_date_month": passport.issue_date_month,
147            ":issue_date_day": passport.issue_date_day,
148            ":issue_date_year": passport.issue_date_year,
149            ":expiry_date_month": passport.expiry_date_month,
150            ":expiry_date_day": passport.expiry_date_day,
151            ":expiry_date_year": passport.expiry_date_year,
152            ":time_last_modified": Timestamp::now(),
153            ":guid": guid,
154        },
155    )?;
156    tx.commit()?;
157    Ok(())
158}
159
160pub(crate) fn delete_passport(conn: &Connection, guid: &Guid) -> Result<bool> {
161    let tx = conn.unchecked_transaction()?;
162    // execute returns how many rows were affected.
163    let exists = tx.execute(
164        "DELETE FROM passports_data WHERE guid = :guid",
165        rusqlite::named_params! { ":guid": guid },
166    )? != 0;
167    tx.commit()?;
168    Ok(exists)
169}
170
171pub fn touch(conn: &Connection, guid: &Guid) -> Result<()> {
172    let tx = conn.unchecked_transaction()?;
173    let now_ms = Timestamp::now();
174    tx.execute(
175        "UPDATE passports_data
176        SET time_last_used = :time_last_used,
177            times_used     = times_used + 1,
178            sync_change_counter = sync_change_counter + 1
179        WHERE guid         = :guid",
180        rusqlite::named_params! {
181            ":time_last_used": now_ms,
182            ":guid": guid,
183        },
184    )?;
185    tx.commit()?;
186    Ok(())
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192    use crate::db::test::new_mem_db;
193
194    fn sample_fields(name: &str, number: &str) -> UpdatablePassportFields {
195        UpdatablePassportFields {
196            name: name.to_string(),
197            country: "CA".to_string(),
198            passport_number: number.to_string(),
199            issue_date_month: 1,
200            issue_date_day: 15,
201            issue_date_year: 2020,
202            expiry_date_month: 1,
203            expiry_date_day: 15,
204            expiry_date_year: 2030,
205        }
206    }
207
208    /// Passports have no with-meta import and no sync engine, so the read path
209    /// is their only untrusted timestamp surface - but a value already on disk
210    /// still has to be repaired rather than propagated.
211    #[test]
212    fn test_passport_from_row_sanitizes_corrupt_timestamps() -> Result<()> {
213        let db = new_mem_db();
214
215        let saved = add_passport(&db, sample_fields("Jane Doe", "X1234567"))?;
216        db.execute(
217            // Three shapes that are not representable dates: the u64-reinterpreted
218            // value from bug 2066257, a raw negative, and MAX_DATE_MS + 1.
219            "UPDATE passports_data
220             SET time_created = 18446744071857664,
221                 time_last_used = -1,
222                 time_last_modified = 8640000000000001
223             WHERE guid = :guid",
224            rusqlite::named_params! { ":guid": saved.guid },
225        )?;
226
227        let retrieved = get_passport(&db, &saved.guid)?;
228        assert_eq!(retrieved.metadata.time_created.as_millis(), 0);
229        assert_eq!(retrieved.metadata.time_last_used.as_millis(), 0);
230        assert_eq!(retrieved.metadata.time_last_modified.as_millis(), 0);
231
232        Ok(())
233    }
234
235    #[test]
236    fn test_passport_create_and_read() -> Result<()> {
237        let db = new_mem_db();
238
239        let saved = add_passport(&db, sample_fields("Jane Doe", "X1234567"))?;
240
241        // add populated the guid and timestamps
242        assert_ne!(Guid::default(), saved.guid);
243        assert_ne!(0, saved.metadata.time_created.as_millis());
244        assert_ne!(0, saved.metadata.time_last_modified.as_millis());
245
246        let retrieved = get_passport(&db, &saved.guid)?;
247        assert_eq!(saved.guid, retrieved.guid);
248        assert_eq!(retrieved.name, "Jane Doe");
249        assert_eq!(retrieved.country, "CA");
250        assert_eq!(retrieved.passport_number, "X1234567");
251        assert_eq!(retrieved.issue_date_month, 1);
252        assert_eq!(retrieved.issue_date_day, 15);
253        assert_eq!(retrieved.issue_date_year, 2020);
254        assert_eq!(retrieved.expiry_date_year, 2030);
255
256        // deleting removes it
257        assert!(delete_passport(&db, &saved.guid)?);
258        assert!(get_passport(&db, &saved.guid).is_err());
259
260        Ok(())
261    }
262
263    #[test]
264    fn test_passport_missing_guid() {
265        let db = new_mem_db();
266        let guid = Guid::random();
267        let result = get_passport(&db, &guid);
268        assert_eq!(
269            result.unwrap_err().to_string(),
270            Error::NoSuchRecord(guid.to_string()).to_string()
271        );
272    }
273
274    #[test]
275    fn test_passport_read_all() -> Result<()> {
276        let db = new_mem_db();
277
278        let a = add_passport(&db, sample_fields("Jane Doe", "A1"))?;
279        let b = add_passport(&db, sample_fields("John Deer", "B2"))?;
280        let c = add_passport(&db, sample_fields("Abe Lincoln", "C3"))?;
281
282        assert!(delete_passport(&db, &c.guid)?);
283
284        let all = get_all_passports(&db)?;
285        assert_eq!(all.len(), 2);
286        assert_eq!(count_all_passports(&db)?, 2);
287
288        let guids = [all[0].guid.as_str(), all[1].guid.as_str()];
289        assert!(guids.contains(&a.guid.as_str()));
290        assert!(guids.contains(&b.guid.as_str()));
291
292        Ok(())
293    }
294
295    #[test]
296    fn test_passport_update() -> Result<()> {
297        let db = new_mem_db();
298        let saved = add_passport(&db, sample_fields("John Deer", "Z9"))?;
299        assert_eq!(saved.metadata.sync_change_counter, 0);
300
301        let mut fields = sample_fields("John Doe", "Z9");
302        fields.expiry_date_year = 2035;
303        update_passport(&db, &saved.guid, &fields)?;
304
305        let updated = get_passport(&db, &saved.guid)?;
306        assert_eq!(updated.name, "John Doe");
307        assert_eq!(updated.expiry_date_year, 2035);
308        // updating bumps the sync change counter
309        assert_eq!(updated.metadata.sync_change_counter, 1);
310
311        Ok(())
312    }
313
314    #[test]
315    fn test_passport_delete() -> Result<()> {
316        let db = new_mem_db();
317        let saved = add_passport(&db, sample_fields("Jane Doe", "D1"))?;
318
319        assert!(delete_passport(&db, &saved.guid)?);
320        // deleting a non-existent record returns false
321        assert!(!delete_passport(&db, &saved.guid)?);
322
323        Ok(())
324    }
325
326    #[test]
327    fn test_passport_touch() -> Result<()> {
328        let db = new_mem_db();
329        let saved = add_passport(&db, sample_fields("Jane Doe", "T1"))?;
330        assert_eq!(saved.metadata.times_used, 0);
331        assert_eq!(saved.metadata.sync_change_counter, 0);
332
333        touch(&db, &saved.guid)?;
334
335        let touched = get_passport(&db, &saved.guid)?;
336        assert_eq!(touched.metadata.times_used, 1);
337        assert!(touched.metadata.time_last_used.as_millis() > 0);
338        // touching bumps the sync change counter
339        assert_eq!(touched.metadata.sync_change_counter, 1);
340
341        Ok(())
342    }
343}