1use crate::db::sql_fns;
6use crate::sync::address::name_utils::{join_name_parts, NameParts};
7use error_support::debug;
8use rusqlite::{functions::FunctionFlags, Connection, Transaction};
9use sql_support::open_database::{ConnectionInitializer, Error, Result};
10
11pub const ADDRESS_COMMON_COLS: &str = "
12 guid,
13 name,
14 organization,
15 street_address,
16 address_level3,
17 address_level2,
18 address_level1,
19 postal_code,
20 country,
21 tel,
22 email,
23 time_created,
24 time_last_used,
25 time_last_modified,
26 times_used";
27
28pub const ADDRESS_COMMON_VALS: &str = "
29 :guid,
30 :name,
31 :organization,
32 :street_address,
33 :address_level3,
34 :address_level2,
35 :address_level1,
36 :postal_code,
37 :country,
38 :tel,
39 :email,
40 :time_created,
41 :time_last_used,
42 :time_last_modified,
43 :times_used";
44
45pub const CREDIT_CARD_COMMON_COLS: &str = "
46 guid,
47 cc_name,
48 cc_number_enc,
49 cc_number_last_4,
50 cc_exp_month,
51 cc_exp_year,
52 cc_type,
53 time_created,
54 time_last_used,
55 time_last_modified,
56 times_used";
57
58pub const CREDIT_CARD_COMMON_VALS: &str = "
59 :guid,
60 :cc_name,
61 :cc_number_enc,
62 :cc_number_last_4,
63 :cc_exp_month,
64 :cc_exp_year,
65 :cc_type,
66 :time_created,
67 :time_last_used,
68 :time_last_modified,
69 :times_used";
70
71pub const PASSPORT_COMMON_COLS: &str = "
72 guid,
73 name,
74 country,
75 passport_number,
76 issue_date_month,
77 issue_date_day,
78 issue_date_year,
79 expiry_date_month,
80 expiry_date_day,
81 expiry_date_year,
82 time_created,
83 time_last_used,
84 time_last_modified,
85 times_used";
86
87pub const PASSPORT_COMMON_VALS: &str = "
88 :guid,
89 :name,
90 :country,
91 :passport_number,
92 :issue_date_month,
93 :issue_date_day,
94 :issue_date_year,
95 :expiry_date_month,
96 :expiry_date_day,
97 :expiry_date_year,
98 :time_created,
99 :time_last_used,
100 :time_last_modified,
101 :times_used";
102
103const CREATE_SHARED_SCHEMA_SQL: &str = include_str!("../../sql/create_shared_schema.sql");
104const CREATE_SHARED_TRIGGERS_SQL: &str = include_str!("../../sql/create_shared_triggers.sql");
105const CREATE_SYNC_TEMP_TABLES_SQL: &str = include_str!("../../sql/create_sync_temp_tables.sql");
106
107pub struct AutofillConnectionInitializer;
108
109impl ConnectionInitializer for AutofillConnectionInitializer {
110 const NAME: &'static str = "autofill db";
111 const END_VERSION: u32 = 5;
112
113 fn prepare(&self, conn: &Connection, _db_empty: bool) -> Result<()> {
114 define_functions(conn)?;
115
116 sql_support::setup_sqlite_defaults(conn)?;
118
119 let initial_pragmas = "
120 -- autofill does not use foreign keys at present but this is probably a good pragma to set
121 PRAGMA foreign_keys = ON;
122 ";
123 conn.execute_batch(initial_pragmas)?;
124
125 conn.set_prepared_statement_cache_capacity(128);
126 Ok(())
127 }
128
129 fn init(&self, db: &Transaction<'_>) -> Result<()> {
130 Ok(db.execute_batch(CREATE_SHARED_SCHEMA_SQL)?)
131 }
132
133 fn upgrade_from(&self, db: &Transaction<'_>, version: u32) -> Result<()> {
134 match version {
135 0 => upgrade_from_v0(db),
138 1 => upgrade_from_v1(db),
139 2 => upgrade_from_v2(db),
140 3 => upgrade_from_v3(db),
141 4 => upgrade_from_v4(db),
142 _ => Err(Error::IncompatibleVersion(version)),
143 }
144 }
145
146 fn finish(&self, db: &Connection) -> Result<()> {
147 Ok(db.execute_batch(CREATE_SHARED_TRIGGERS_SQL)?)
148 }
149}
150
151fn define_functions(c: &Connection) -> Result<()> {
152 c.create_scalar_function(
153 "generate_guid",
154 0,
155 FunctionFlags::SQLITE_UTF8,
156 sql_fns::generate_guid,
157 )?;
158 c.create_scalar_function("now", 0, FunctionFlags::SQLITE_UTF8, sql_fns::now)?;
159
160 Ok(())
161}
162
163fn upgrade_from_v0(db: &Connection) -> Result<()> {
164 db.execute_batch(
173 "
174 DROP TABLE IF EXISTS credit_cards_data;
175 CREATE TABLE credit_cards_data (
176 guid TEXT NOT NULL PRIMARY KEY CHECK(length(guid) != 0),
177 cc_name TEXT NOT NULL,
178 cc_number_enc TEXT NOT NULL CHECK(length(cc_number_enc) > 20),
179 cc_number_last_4 TEXT NOT NULL CHECK(length(cc_number_last_4) <= 4),
180 cc_exp_month INTEGER,
181 cc_exp_year INTEGER,
182 cc_type TEXT NOT NULL,
183 time_created INTEGER NOT NULL,
184 time_last_used INTEGER,
185 time_last_modified INTEGER NOT NULL,
186 times_used INTEGER NOT NULL,
187 sync_change_counter INTEGER NOT NULL
188 );
189 ",
190 )?;
191 Ok(())
192}
193
194fn upgrade_from_v1(db: &Connection) -> Result<()> {
195 db.execute_batch(
201 "
202 CREATE TABLE new_credit_cards_data (
203 guid TEXT NOT NULL PRIMARY KEY CHECK(length(guid) != 0),
204 cc_name TEXT NOT NULL,
205 cc_number_enc TEXT NOT NULL CHECK(length(cc_number_enc) > 20 OR cc_number_enc == ''),
206 cc_number_last_4 TEXT NOT NULL CHECK(length(cc_number_last_4) <= 4),
207 cc_exp_month INTEGER,
208 cc_exp_year INTEGER,
209 cc_type TEXT NOT NULL,
210 time_created INTEGER NOT NULL,
211 time_last_used INTEGER,
212 time_last_modified INTEGER NOT NULL,
213 times_used INTEGER NOT NULL,
214 sync_change_counter INTEGER NOT NULL
215 );
216 INSERT INTO new_credit_cards_data(guid, cc_name, cc_number_enc, cc_number_last_4, cc_exp_month,
217 cc_exp_year, cc_type, time_created, time_last_used, time_last_modified, times_used,
218 sync_change_counter)
219 SELECT guid, cc_name, cc_number_enc, cc_number_last_4, cc_exp_month, cc_exp_year, cc_type,
220 time_created, time_last_used, time_last_modified, times_used, sync_change_counter
221 FROM credit_cards_data;
222 DROP TABLE credit_cards_data;
223 ALTER TABLE new_credit_cards_data RENAME to credit_cards_data;
224 ")?;
225 Ok(())
226}
227
228fn upgrade_from_v2(db: &Connection) -> Result<()> {
229 db.execute_batch("ALTER TABLE addresses_data ADD COLUMN name TEXT NOT NULL DEFAULT ''")?;
230
231 let mut stmt =
232 db.prepare("SELECT guid, given_name, additional_name, family_name FROM addresses_data")?;
233 let rows = stmt.query_map([], |row| {
234 Ok((
235 row.get::<_, String>("guid")?,
236 row.get::<_, String>("given_name")?,
237 row.get::<_, String>("additional_name")?,
238 row.get::<_, String>("family_name")?,
239 ))
240 })?;
241
242 for row in rows {
243 let (guid, given, middle, family) = row?;
244 let full_name = join_name_parts(&NameParts {
245 given,
246 middle,
247 family,
248 });
249
250 db.execute(
251 "UPDATE addresses_data SET name = (:name) WHERE guid = (:guid)",
252 rusqlite::named_params! { ":name": full_name, ":guid": guid},
253 )?;
254 }
255
256 db.execute_batch(
257 "
258 ALTER TABLE addresses_data DROP COLUMN given_name;
259 ALTER TABLE addresses_data DROP COLUMN additional_name;
260 ALTER TABLE addresses_data DROP COLUMN family_name;
261 ",
262 )?;
263
264 Ok(())
265}
266
267fn upgrade_from_v3(db: &Connection) -> Result<()> {
268 let migration_string: &str = include_str!("../../sql/migrations/v4_migration.sql");
269 db.execute_batch(migration_string)?;
270 Ok(())
271}
272
273fn upgrade_from_v4(db: &Connection) -> Result<()> {
274 db.execute_batch(CREATE_SHARED_SCHEMA_SQL)?;
279 Ok(())
280}
281
282pub fn create_empty_sync_temp_tables(db: &Connection) -> Result<()> {
283 debug!("Initializing sync temp tables");
284 db.execute_batch(CREATE_SYNC_TEMP_TABLES_SQL)?;
285 Ok(())
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use crate::db::addresses::get_address;
292 use crate::db::credit_cards::get_credit_card;
293 use crate::db::test::new_mem_db;
294 use crate::db::AutofillDb;
295 use sql_support::open_database::test_utils::MigratedDatabaseFile;
296 use sync_guid::Guid;
297 use types::Timestamp;
298
299 const CREATE_V0_DB: &str = include_str!("../../sql/tests/create_v0_db.sql");
300 const CREATE_V1_DB: &str = include_str!("../../sql/tests/create_v1_db.sql");
301 const CREATE_V2_DB: &str = include_str!("../../sql/tests/create_v2_db.sql");
302 const CREATE_V3_DB: &str = include_str!("../../sql/tests/create_v3_db.sql");
303 const CREATE_V4_DB: &str = include_str!("../../sql/tests/create_v4_db.sql");
304
305 #[test]
306 fn test_wal_size_is_bounded() {
307 let db_file = MigratedDatabaseFile::new(AutofillConnectionInitializer, "");
309 let db = AutofillDb::new(&db_file.path).expect("should open the database");
310
311 let journal_mode: String = db
312 .query_row("PRAGMA journal_mode", [], |row| row.get(0))
313 .unwrap();
314 assert_eq!(journal_mode, "wal");
315
316 let autocheckpoint: i64 = db
317 .query_row("PRAGMA wal_autocheckpoint", [], |row| row.get(0))
318 .unwrap();
319 assert!(autocheckpoint > 0, "auto-checkpointing is disabled");
320
321 let journal_size_limit: i64 = db
322 .query_row("PRAGMA journal_size_limit", [], |row| row.get(0))
323 .unwrap();
324 assert!(
325 journal_size_limit > 0,
326 "journal_size_limit is {journal_size_limit}, so the -wal file is never truncated"
327 );
328 }
329
330 #[test]
331 fn test_create_schema_twice() {
332 let db = new_mem_db();
333 db.execute_batch(CREATE_SHARED_SCHEMA_SQL)
334 .expect("should allow running main schema creation twice");
335 db.execute_batch(CREATE_SYNC_TEMP_TABLES_SQL)
337 .expect("should allow running sync temp tables first time");
338 db.execute_batch(CREATE_SYNC_TEMP_TABLES_SQL)
339 .expect("should allow running sync temp tables second time");
340 }
341
342 #[test]
343 fn test_all_upgrades() {
344 let db_file = MigratedDatabaseFile::new(AutofillConnectionInitializer, CREATE_V1_DB);
346 db_file.run_all_upgrades();
347 let conn = db_file.open();
348
349 let cc = get_credit_card(&conn, &Guid::new("A")).unwrap();
351 assert_eq!(cc.guid, "A");
352 assert_eq!(cc.cc_name, "Jane Doe");
353 assert_eq!(cc.cc_number_enc, "012345678901234567890");
354 assert_eq!(cc.cc_number_last_4, "1234");
355 assert_eq!(cc.cc_exp_month, 1);
356 assert_eq!(cc.cc_exp_year, 2020);
357 assert_eq!(cc.cc_type, "visa");
358 assert_eq!(cc.metadata.time_created, Timestamp(0));
359 assert_eq!(cc.metadata.time_last_used, Timestamp(1));
360 assert_eq!(cc.metadata.time_last_modified, Timestamp(2));
361 assert_eq!(cc.metadata.times_used, 3);
362 assert_eq!(cc.metadata.sync_change_counter, 0);
363
364 let address = get_address(&conn, &Guid::new("A")).unwrap();
365 assert_eq!(address.guid, "A");
366 assert_eq!(address.name, "Jane JaneDoe2 Doe");
367 assert_eq!(address.organization, "Mozilla");
368 assert_eq!(address.street_address, "123 Maple lane");
369 assert_eq!(address.address_level3, "Shelbyville");
370 assert_eq!(address.address_level2, "Springfield");
371 assert_eq!(address.address_level1, "MA");
372 assert_eq!(address.postal_code, "12345");
373 assert_eq!(address.country, "US");
374 assert_eq!(address.tel, "01-234-567-8000");
375 assert_eq!(address.email, "jane@hotmail.com");
376 assert_eq!(address.metadata.time_created, Timestamp(0));
377 assert_eq!(address.metadata.time_last_used, Timestamp(1));
378 assert_eq!(address.metadata.time_last_modified, Timestamp(2));
379 assert_eq!(address.metadata.times_used, 3);
380 assert_eq!(address.metadata.sync_change_counter, 0);
381 }
382
383 #[test]
384 fn test_upgrade_version_0() {
385 let db_file = MigratedDatabaseFile::new(AutofillConnectionInitializer, CREATE_V0_DB);
386 let select_cc_number_enc = "SELECT cc_number_enc from credit_cards_data";
389 db_file
390 .open()
391 .execute_batch(select_cc_number_enc)
392 .expect_err("select should fail due to bad field name");
393
394 db_file.upgrade_to(1);
395
396 db_file
397 .open()
398 .execute_batch(select_cc_number_enc)
399 .expect("select should now work");
400 }
401
402 #[test]
403 fn test_upgrade_version_1() {
404 let db_file = MigratedDatabaseFile::new(AutofillConnectionInitializer, CREATE_V1_DB);
405
406 db_file.upgrade_to(2);
407 let db = db_file.open();
408
409 db.execute("UPDATE credit_cards_data SET cc_number_enc=''", [])
411 .expect("blank cc_number_enc should be valid");
412 db.execute("UPDATE credit_cards_data SET cc_number_enc='x'", [])
413 .expect_err("cc_number_enc should be invalid");
414 }
415
416 #[test]
417 fn test_upgrade_version_2() {
418 let db_file = MigratedDatabaseFile::new(AutofillConnectionInitializer, CREATE_V2_DB);
419 let db = db_file.open();
420
421 db.execute_batch("SELECT name from addresses_data")
422 .expect_err("select should fail");
423 db.execute_batch("SELECT street_address from addresses_data")
424 .expect("street_address should work");
425 db.execute_batch("SELECT additional_name from addresses_data")
426 .expect("additional_name should work");
427 db.execute_batch("SELECT family_name from addresses_data")
428 .expect("family_name should work");
429
430 db_file.upgrade_to(3);
431
432 db.execute_batch("SELECT name from addresses_data")
433 .expect("select name should now work");
434 db.execute_batch("SELECT given_name from addresses_data")
435 .expect_err("given_name should fail");
436 db.execute_batch("SELECT additional_name from addresses_data")
437 .expect_err("additional_name should fail");
438 db.execute_batch("SELECT family_name from addresses_data")
439 .expect_err("family_name should fail");
440
441 let mut address = get_address(&db, &Guid::new("A")).unwrap();
442 assert_eq!(address.guid, "A");
443 assert_eq!(address.name, "Jane John Doe");
444
445 address = get_address(&db, &Guid::new("B")).unwrap();
446 assert_eq!(address.guid, "B");
447
448 assert_eq!(address.name, "");
450 }
451
452 #[test]
453 fn test_upgrade_version_3() {
454 let db_file = MigratedDatabaseFile::new(AutofillConnectionInitializer, CREATE_V3_DB);
455 let db = db_file.open();
456
457 let mut address = get_address(&db, &Guid::new("A")).unwrap();
459 assert_eq!(address.guid, "A");
460 assert_eq!(address.name, "Jane John Doe");
461 assert_eq!(address.address_level1, "Massachusetts");
462
463 address = get_address(&db, &Guid::new("B")).unwrap();
464 assert_eq!(address.guid, "B");
465 assert_eq!(address.address_level1, "Ontario");
466
467 db_file.upgrade_to(4);
468
469 address = get_address(&db, &Guid::new("A")).unwrap();
471 assert_eq!(address.guid, "A");
472 assert_eq!(address.name, "Jane John Doe");
473 assert_eq!(address.address_level1, "MA");
474
475 address = get_address(&db, &Guid::new("B")).unwrap();
476 assert_eq!(address.guid, "B");
477 assert_eq!(address.address_level1, "ON");
478 }
479
480 #[test]
481 fn test_upgrade_version_4() {
482 let db_file = MigratedDatabaseFile::new(AutofillConnectionInitializer, CREATE_V4_DB);
483 let db = db_file.open();
484
485 db.execute_batch("SELECT guid FROM passports_data")
487 .expect_err("passports_data should not exist at v4");
488
489 db_file.upgrade_to(5);
490
491 db.execute_batch("SELECT guid FROM passports_data")
493 .expect("passports_data should exist at v5");
494 }
495}