diff --git a/components/places/android/src/main/java/mozilla/appservices/places/LibPlacesFFI.kt b/components/places/android/src/main/java/mozilla/appservices/places/LibPlacesFFI.kt index 3360ecd963..1ccc7fcdb9 100644 --- a/components/places/android/src/main/java/mozilla/appservices/places/LibPlacesFFI.kt +++ b/components/places/android/src/main/java/mozilla/appservices/places/LibPlacesFFI.kt @@ -37,6 +37,12 @@ internal interface LibPlacesFFI : Library { out_err: RustError.ByReference ): PlacesConnectionHandle + fun places_history_import_from_fennec( + handle: PlacesApiHandle, + db_path: String, + out_err: RustError.ByReference + ) + fun places_note_observation( handle: PlacesConnectionHandle, json_observation_data: String, diff --git a/components/places/android/src/main/java/mozilla/appservices/places/PlacesConnection.kt b/components/places/android/src/main/java/mozilla/appservices/places/PlacesConnection.kt index 64e5ae7f8d..6bae945e69 100644 --- a/components/places/android/src/main/java/mozilla/appservices/places/PlacesConnection.kt +++ b/components/places/android/src/main/java/mozilla/appservices/places/PlacesConnection.kt @@ -411,6 +411,13 @@ class PlacesWriterConnection internal constructor(connHandle: Long, api: PlacesA deleteVisitsBetween(since, Long.MAX_VALUE) } + override fun importVisitsFromFennec(path: String) { + rustCall { error -> + LibPlacesFFI.INSTANCE.places_history_import_from_fennec( + this.handle.get(), path, error) + } + } + override fun deleteVisitsBetween(startTime: Long, endTime: Long) { rustCall { error -> LibPlacesFFI.INSTANCE.places_delete_visits_between( @@ -740,6 +747,16 @@ interface WritableHistoryConnection : ReadableHistoryConnection { */ fun deleteVisitsSince(since: Long) + /** + * Imports visits from a Fennec `browser.db` database. + * + * It has been designed exclusively for non-sync users and should + * be called before bookmarks import. + * + * @param path Path to the `browser.db` file database. + */ + fun importVisitsFromFennec(path: String) + /** * Equivalent to deleteVisitsSince, but takes an `endTime` as well. * diff --git a/components/places/ffi/src/lib.rs b/components/places/ffi/src/lib.rs index f59ca08485..cade76ad28 100644 --- a/components/places/ffi/src/lib.rs +++ b/components/places/ffi/src/lib.rs @@ -75,6 +75,7 @@ pub extern "C" fn places_connection_new( Ok(CONNECTIONS.insert(api.open_connection(conn_type)?)) }) } + #[no_mangle] pub extern "C" fn places_bookmarks_import_from_ios( api_handle: u64, @@ -88,6 +89,19 @@ pub extern "C" fn places_bookmarks_import_from_ios( }) } +#[no_mangle] +pub extern "C" fn places_history_import_from_fennec( + api_handle: u64, + db_path: FfiStr<'_>, + error: &mut ExternError, +) { + log::debug!("places_history_import_from_fennec"); + APIS.call_with_result(error, api_handle, |api| -> places::Result<_> { + places::import::import_fennec_history(api, db_path.as_str())?; + Ok(()) + }) +} + // Best effort, ignores failure. #[no_mangle] pub extern "C" fn places_api_return_write_conn( diff --git a/components/places/src/db/db.rs b/components/places/src/db/db.rs index 8d048a9d63..5087d2dc08 100644 --- a/components/places/src/db/db.rs +++ b/components/places/src/db/db.rs @@ -195,7 +195,7 @@ fn define_functions(c: &Connection) -> Result<()> { Ok(()) } -mod sql_fns { +pub(crate) mod sql_fns { use crate::api::matcher::{split_after_host_and_port, split_after_prefix}; use crate::hash; use crate::match_impl::{AutocompleteMatch, MatchBehavior, SearchBehavior}; diff --git a/components/places/src/error.rs b/components/places/src/error.rs index 7b1db4f5e9..11aa8ce20a 100644 --- a/components/places/src/error.rs +++ b/components/places/src/error.rs @@ -74,6 +74,9 @@ pub enum ErrorKind { #[fail(display = "Database cannot be upgraded")] DatabaseUpgradeError, + + #[fail(display = "Database version {} is not supported", _0)] + UnsupportedDatabaseVersion(i64), } error_support::define_error! { diff --git a/components/places/src/import/common.rs b/components/places/src/import/common.rs new file mode 100644 index 0000000000..287718b060 --- /dev/null +++ b/components/places/src/import/common.rs @@ -0,0 +1,112 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::api::places_api::SyncConn; +use crate::error::*; +use rusqlite::named_params; +use url::Url; + +pub mod sql_fns { + use crate::storage::URL_LENGTH_MAX; + use crate::types::Timestamp; + use rusqlite::{functions::Context, types::ValueRef, Result}; + use url::Url; + + #[inline(never)] + pub fn sanitize_timestamp(ctx: &Context<'_>) -> Result { + let now = Timestamp::now(); + Ok(if let Ok(ts) = ctx.get::(0) { + if Timestamp::EARLIEST < ts && ts < now { + ts + } else { + now + } + } else { + now + }) + } + + #[inline(never)] + pub fn validate_url(ctx: &Context<'_>) -> Result> { + let val = ctx.get_raw(0); + let href = if let ValueRef::Text(s) = val { + std::str::from_utf8(s)? + } else { + return Ok(None); + }; + if href.len() > URL_LENGTH_MAX { + return Ok(None); + } + if let Ok(url) = Url::parse(href) { + Ok(Some(url.into_string())) + } else { + Ok(None) + } + } + + #[inline(never)] + pub fn is_valid_url(ctx: &Context<'_>) -> Result> { + Ok(match ctx.get_raw(0) { + ValueRef::Text(s) if s.len() <= URL_LENGTH_MAX => { + if let Ok(s) = std::str::from_utf8(s) { + Some(Url::parse(s).is_ok()) + } else { + Some(false) + } + } + _ => Some(false), + }) + } +} + +pub fn attached_database<'a>( + conn: &'a SyncConn<'a>, + path: &Url, + db_alias: &'static str, +) -> Result> { + conn.execute_named( + "ATTACH DATABASE :path AS :db_alias", + named_params! { + ":path": path.as_str(), + ":db_alias": db_alias, + }, + )?; + Ok(ExecuteOnDrop { + conn, + sql: format!("DETACH DATABASE {};", db_alias), + }) +} + +/// We use/abuse the mirror to perform our import, but need to clean it up +/// afterwards. This is an RAII helper to do so. +/// +/// Ideally, you should call `execute_now` rather than letting this drop +/// automatically, as we can't report errors beyond logging when running +/// Drop. +pub struct ExecuteOnDrop<'a> { + conn: &'a SyncConn<'a>, + sql: String, +} + +impl<'a> ExecuteOnDrop<'a> { + pub fn new(conn: &'a SyncConn<'a>, sql: String) -> Self { + Self { conn, sql } + } + + pub fn execute_now(self) -> Result<()> { + self.conn.execute_batch(&self.sql)?; + // Don't run our `drop` function. + std::mem::forget(self); + Ok(()) + } +} + +impl Drop for ExecuteOnDrop<'_> { + fn drop(&mut self) { + if let Err(e) = self.conn.execute_batch(&self.sql) { + log::error!("Failed to clean up after import! {}", e); + log::debug!(" Failed query: {}", &self.sql); + } + } +} diff --git a/components/places/src/import/fennec.rs b/components/places/src/import/fennec.rs new file mode 100644 index 0000000000..380bdc8d42 --- /dev/null +++ b/components/places/src/import/fennec.rs @@ -0,0 +1,6 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +mod history; +pub use history::import as import_history; diff --git a/components/places/src/import/fennec/history.rs b/components/places/src/import/fennec/history.rs new file mode 100644 index 0000000000..d0fc920644 --- /dev/null +++ b/components/places/src/import/fennec/history.rs @@ -0,0 +1,121 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::api::places_api::PlacesApi; +use crate::bookmark_sync::store::BookmarksStore; +use crate::error::*; +use crate::import::common::attached_database; +use rusqlite::Connection; +use sql_support::ConnExt; +use url::Url; + +// From https://searchfox.org/mozilla-central/rev/597a69c70a5cce6f42f159eb54ad1ef6745f5432/mobile/android/base/java/org/mozilla/gecko/db/BrowserDatabaseHelper.java#73. +const FENNEC_DB_VERSION: i64 = 39; + +pub fn import(places_api: &PlacesApi, path: impl AsRef) -> Result<()> { + let url = crate::util::ensure_url_path(path)?; + do_import(places_api, url) +} + +fn do_import(places_api: &PlacesApi, android_db_file_url: Url) -> Result<()> { + let conn = places_api.open_sync_connection()?; + + let scope = conn.begin_interrupt_scope(); + + define_sql_functions(&conn)?; + + // Not sure why, but apparently beginning a transaction sometimes + // fails if we open the DB as read-only. Hopefully we don't + // unintentionally write to it anywhere... + // android_db_file_url.query_pairs_mut().append_pair("mode", "ro"); + + log::trace!("Attaching database {}", android_db_file_url); + let auto_detach = attached_database(&conn, &android_db_file_url, "fennec")?; + + let db_version = conn.db.query_one::("PRAGMA fennec.user_version")?; + if db_version != FENNEC_DB_VERSION { + return Err(ErrorKind::UnsupportedDatabaseVersion(db_version).into()); + } + + let tx = conn.begin_transaction()?; + + log::debug!("Populating missing entries in moz_places"); + conn.execute_batch(&FILL_MOZ_PLACES)?; + scope.err_if_interrupted()?; + + log::debug!("Inserting the history visits"); + conn.execute_batch(&INSERT_HISTORY_VISITS)?; + scope.err_if_interrupted()?; + + log::debug!("Committing..."); + tx.commit()?; + + // Note: update_frecencies manages its own transaction, which is fine, + // since nothing that bad will happen if it is aborted. + log::debug!("Updating frecencies"); + let store = BookmarksStore::new(&conn, &scope); + store.update_frecencies()?; + + log::info!("Successfully imported history visits!"); + + auto_detach.execute_now()?; + + Ok(()) +} + +lazy_static::lazy_static! { + // Insert any missing entries into moz_places that we'll need for this. + static ref FILL_MOZ_PLACES: &'static str = + "INSERT OR IGNORE INTO main.moz_places(guid, url, url_hash, title, frecency, sync_change_counter) + SELECT + IFNULL( + (SELECT p.guid FROM main.moz_places p WHERE p.url_hash = hash(h.url) AND p.url = h.url), + generate_guid() + ), + h.url, + hash(h.url), + h.title, + -1, + 1 + FROM fennec.history h + WHERE is_valid_url(h.url)" + ; + + // Insert history visits + static ref INSERT_HISTORY_VISITS: &'static str = + "INSERT OR IGNORE INTO main.moz_historyvisits(from_visit, place_id, visit_date, visit_type, is_local) + SELECT + NULL, -- Fenec does not store enough information to rebuild redirect chains. + (SELECT p.id FROM main.moz_places p WHERE p.url_hash = hash(h.url) AND p.url = h.url), + sanitize_timestamp(v.date), + v.visit_type, -- Fennec stores visit types maps 1:1 to ours. + v.is_local + FROM fennec.visits v + LEFT JOIN fennec.history h on v.history_guid = h.guid + WHERE is_valid_url(h.url)" + ; +} + +pub(super) fn define_sql_functions(c: &Connection) -> Result<()> { + c.create_scalar_function( + "is_valid_url", + 1, + true, + crate::import::common::sql_fns::is_valid_url, + )?; + c.create_scalar_function( + "sanitize_timestamp", + 1, + true, + crate::import::common::sql_fns::sanitize_timestamp, + )?; + c.create_scalar_function("hash", -1, true, crate::db::db::sql_fns::hash)?; + c.create_scalar_function( + "generate_guid", + 0, + false, + crate::db::db::sql_fns::generate_guid, + )?; + Ok(()) +} diff --git a/components/places/src/import/ios_bookmarks.rs b/components/places/src/import/ios_bookmarks.rs index f1107bd83b..2d88be5506 100644 --- a/components/places/src/import/ios_bookmarks.rs +++ b/components/places/src/import/ios_bookmarks.rs @@ -2,12 +2,13 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -use crate::api::places_api::{PlacesApi, SyncConn}; +use crate::api::places_api::PlacesApi; use crate::bookmark_sync::{ store::{BookmarksStore, Merger}, SyncedBookmarkKind, }; use crate::error::*; +use crate::import::common::{attached_database, ExecuteOnDrop}; use crate::types::SyncStatus; use rusqlite::{named_params, NO_PARAMS}; use sql_support::ConnExt; @@ -95,14 +96,11 @@ fn do_import_ios_bookmarks(places_api: &PlacesApi, ios_db_file_url: Url) -> Resu // ios_db_file_url.query_pairs_mut().append_pair("mode", "ro"); log::trace!("Attaching database {}", ios_db_file_url); - let auto_detach = attached_database(&conn, &ios_db_file_url)?; + let auto_detach = attached_database(&conn, &ios_db_file_url, "ios")?; let tx = conn.begin_transaction()?; - let clear_mirror_on_drop = ExecuteOnDrop { - conn: &conn, - sql: &WIPE_MIRROR, - }; + let clear_mirror_on_drop = ExecuteOnDrop::new(&conn, WIPE_MIRROR.to_string()); // Clear the mirror now, since we're about to fill it with data from the ios // connection. @@ -469,55 +467,9 @@ lazy_static::lazy_static! { ); } -fn attached_database<'a>(conn: &'a SyncConn<'a>, path: &Url) -> Result> { - conn.execute_named( - "ATTACH DATABASE :path AS ios", - named_params! { - ":path": path.as_str(), - }, - )?; - Ok(ExecuteOnDrop { - conn, - sql: "DETACH DATABASE ios;", - }) -} - -/// We use/abuse the mirror to perform our import, but need to clean it up -/// afterwards. This is an RAII helper to do so. -/// -/// Ideally, you should call `execute_now` rather than letting this drop -/// automatically, as we can't report errors beyond logging when running -/// Drop. -struct ExecuteOnDrop<'a> { - conn: &'a SyncConn<'a>, - // Logged on errors, so &'static helps discourage using anything - // that could have user data. - sql: &'static str, -} - -impl<'a> ExecuteOnDrop<'a> { - pub fn execute_now(self) -> Result<()> { - self.conn.execute_batch(self.sql)?; - // Don't run our `drop` function. - std::mem::forget(self); - Ok(()) - } -} - -impl Drop for ExecuteOnDrop<'_> { - fn drop(&mut self) { - if let Err(e) = self.conn.execute_batch(self.sql) { - log::error!("Failed to clean up after import! {}", e); - log::debug!(" Failed query: {}", self.sql); - } - } -} - mod sql_fns { - use crate::storage::URL_LENGTH_MAX; - use crate::types::Timestamp; - use rusqlite::{functions::Context, types::ValueRef, Connection, Result}; - use url::Url; + use crate::import::common::sql_fns::{is_valid_url, sanitize_timestamp, validate_url}; + use rusqlite::{Connection, Result}; pub(super) fn define_functions(c: &Connection) -> Result<()> { c.create_scalar_function("validate_url", 1, true, validate_url)?; @@ -525,52 +477,4 @@ mod sql_fns { c.create_scalar_function("sanitize_timestamp", 1, true, sanitize_timestamp)?; Ok(()) } - - #[inline(never)] - pub fn validate_url(ctx: &Context<'_>) -> Result> { - let val = ctx.get_raw(0); - let href = if let ValueRef::Text(s) = val { - std::str::from_utf8(s)? - } else { - return Ok(None); - }; - if href.len() > URL_LENGTH_MAX { - return Ok(None); - } - if let Ok(url) = Url::parse(href) { - Ok(Some(url.into_string())) - } else { - Ok(None) - } - } - - #[inline(never)] - pub fn is_valid_url(ctx: &Context<'_>) -> Result> { - Ok(match ctx.get_raw(0) { - ValueRef::Text(s) if s.len() <= URL_LENGTH_MAX => { - if let Ok(s) = std::str::from_utf8(s) { - Some(Url::parse(s).is_ok()) - } else { - Some(false) - } - } - // Should we do this? - // ValueRef::Null => None, - _ => Some(false), - }) - } - - #[inline(never)] - pub fn sanitize_timestamp(ctx: &Context<'_>) -> Result { - let now = Timestamp::now(); - Ok(if let Ok(ts) = ctx.get::(0) { - if Timestamp::EARLIEST < ts && ts < now { - ts - } else { - now - } - } else { - now - }) - } } diff --git a/components/places/src/import/mod.rs b/components/places/src/import/mod.rs index 4c526fdb23..47a156c4f0 100644 --- a/components/places/src/import/mod.rs +++ b/components/places/src/import/mod.rs @@ -2,5 +2,8 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +pub mod common; +pub mod fennec; +pub use fennec::import_history as import_fennec_history; pub mod ios_bookmarks; pub use ios_bookmarks::import_ios_bookmarks; diff --git a/components/places/tests/fennec_history.rs b/components/places/tests/fennec_history.rs new file mode 100644 index 0000000000..c3212150ab --- /dev/null +++ b/components/places/tests/fennec_history.rs @@ -0,0 +1,257 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use places::{api::places_api::PlacesApi, types::VisitTransition, ErrorKind, Result, Timestamp}; +use rusqlite::{Connection, NO_PARAMS}; +use std::path::Path; +use std::sync::atomic::{AtomicUsize, Ordering}; +use sync_guid::Guid; +use tempfile::tempdir; + +fn empty_fennec_db(path: &Path) -> Result { + let conn = Connection::open(path)?; + conn.execute_batch(include_str!("./fennec_history_schema.sql"))?; + Ok(conn) +} + +#[derive(Clone, Debug)] +struct FennecHistory { + title: Option, + url: String, + visits: u16, + visits_local: u16, + visits_remote: u16, + date: Timestamp, + date_local: Timestamp, + date_remote: Timestamp, + created: Timestamp, + modified: Timestamp, + guid: Guid, + deleted: bool, +} + +impl FennecHistory { + fn insert_into_db(&self, conn: &Connection) -> Result<()> { + let mut stmt = conn.prepare(& + "INSERT OR IGNORE INTO history(title, url, visits, visits_local, visits_remote, date, + date_local, date_remote, created, modified, guid, deleted) + VALUES (:title, :url, :visits, :visits_local, :visits_remote, :date, + :date_local, :date_remote, :created, :modified, :guid, :deleted)" + )?; + stmt.execute_named(rusqlite::named_params! { + ":title": self.title, + ":url": self.url, + ":visits": self.visits, + ":visits_local": self.visits_local, + ":visits_remote": self.visits_remote, + ":date": self.date, + ":date_local": self.date_local, + ":date_remote": self.date_remote, + ":created": self.created, + ":modified": self.modified, + ":guid": self.guid, + ":deleted": self.deleted, + })?; + Ok(()) + } +} + +#[derive(Clone, Debug)] +struct FennecVisit<'a> { + history: &'a FennecHistory, + visit_type: VisitTransition, + date: Timestamp, + is_local: bool, +} + +impl<'a> FennecVisit<'a> { + fn insert_into_db(&self, conn: &Connection) -> Result<()> { + let mut stmt = conn.prepare( + &"INSERT OR IGNORE INTO visits(history_guid, visit_type, date, is_local) + VALUES (:history_guid, :visit_type, :date, :is_local)", + )?; + stmt.execute_named(rusqlite::named_params! { + ":history_guid": self.history.guid, + ":visit_type": self.visit_type, + ":date": self.date, + ":is_local": self.is_local, + })?; + Ok(()) + } +} + +static ID_COUNTER: AtomicUsize = AtomicUsize::new(0); + +// Helps debugging to use these instead of actually random ones. +fn next_guid() -> Guid { + let c = ID_COUNTER.fetch_add(1, Ordering::SeqCst); + let v = format!("test{}_______", c); + let s = &v[..12]; + Guid::from(s) +} + +impl Default for FennecHistory { + fn default() -> Self { + Self { + title: None, + url: String::default(), + visits: 0, + visits_local: 0, + visits_remote: 0, + date: Timestamp::now(), + date_local: Timestamp::now(), + date_remote: Timestamp::now(), + created: Timestamp::now(), + modified: Timestamp::now(), + guid: next_guid(), + deleted: false, + } + } +} + +fn insert_history_and_visits( + conn: &Connection, + history: &[FennecHistory], + visits: &[FennecVisit], +) -> Result<()> { + for h in history { + h.insert_into_db(conn)?; + } + for v in visits { + v.insert_into_db(conn)?; + } + Ok(()) +} + +#[test] +fn test_import_unsupported_db_version() -> Result<()> { + let tmpdir = tempdir().unwrap(); + let fennec_path = tmpdir.path().join("browser.db"); + let fennec_db = empty_fennec_db(&fennec_path)?; + fennec_db.execute("PRAGMA user_version=99", NO_PARAMS)?; + let places_api = PlacesApi::new(tmpdir.path().join("places.sqlite"))?; + match places::import::import_fennec_history(&places_api, fennec_path) + .unwrap_err() + .kind() + { + ErrorKind::UnsupportedDatabaseVersion(_) => {} + _ => unreachable!("Should fail with UnsupportedDatabaseVersion!"), + } + Ok(()) +} + +#[test] +fn test_import() -> Result<()> { + let tmpdir = tempdir().unwrap(); + let fennec_path = tmpdir.path().join("browser.db"); + let fennec_db = empty_fennec_db(&fennec_path)?; + + let history = [ + FennecHistory { + title: Some("Welcome to bobo.com".to_owned()), + url: "https://bobo.com/".to_owned(), + ..Default::default() + }, + FennecHistory { + title: Some("Mozilla.org".to_owned()), + url: "https://mozilla.org/".to_owned(), + ..Default::default() + }, + FennecHistory { + url: "https://foo.bar/".to_owned(), + ..Default::default() + }, + FennecHistory { + url: "https://gonnacolide.guid".to_owned(), + guid: Guid::from("colidingguid"), // This GUID already exists in the DB, but with a different URL. + ..Default::default() + }, + FennecHistory { + url: "https://existing.guid".to_owned(), // This GUID already exists in the DB, with the same URL. + guid: Guid::from("existingguid"), + ..Default::default() + }, + FennecHistory { + url: "https://existing.url".to_owned(), // This URL already exists in the DB. + ..Default::default() + }, + FennecHistory { + url: "I'm a super invalid URL, yo".to_owned(), + ..Default::default() + }, + ]; + let visits = [ + FennecVisit { + history: &history[0], + visit_type: VisitTransition::Typed, + date: Timestamp::from(1_565_117_389_897), + is_local: true, + }, + FennecVisit { + history: &history[0], + visit_type: VisitTransition::Link, + date: Timestamp::from(1_565_117_389_898), + is_local: false, + }, + FennecVisit { + history: &history[1], + visit_type: VisitTransition::Link, + date: Timestamp::from(1), // Invalid timestamp should get corrected! + is_local: false, + }, + FennecVisit { + history: &history[3], + visit_type: VisitTransition::Link, + date: Timestamp::from(1_565_117_389_898), + is_local: true, + }, + FennecVisit { + history: &history[4], + visit_type: VisitTransition::Link, + date: Timestamp::from(1_565_117_389_898), + is_local: true, + }, + FennecVisit { + history: &history[5], + visit_type: VisitTransition::Link, + date: Timestamp::from(1_565_117_389_898), + is_local: true, + }, + ]; + insert_history_and_visits(&fennec_db, &history, &visits)?; + + let places_api = PlacesApi::new(tmpdir.path().join("places.sqlite"))?; + + // Insert some places with GUIDs that colide with the imported data. + let conn = places_api.open_connection(places::ConnectionType::ReadWrite)?; + conn.execute( + "INSERT INTO moz_places (guid, url, url_hash) + VALUES ('colidingguid', 'https://coliding.guid', hash('https://coliding.guid'))", + NO_PARAMS, + ) + .expect("should insert"); + conn.execute( + "INSERT INTO moz_places (guid, url, url_hash) + VALUES ('existingguid', 'https://existing.guid', hash('https://existing.guid'))", + NO_PARAMS, + ) + .expect("should insert"); + conn.execute( + "INSERT INTO moz_places (guid, url, url_hash) + VALUES ('boboguid1', 'https://existing.url', hash('https://existing.url'))", + NO_PARAMS, + ) + .expect("should insert"); + + places::import::import_fennec_history(&places_api, fennec_path)?; + + // Uncomment the following to debug with cargo test -- --nocapture. + // println!( + // "Places DB Path: {}", + // tmpdir.path().join("places.sqlite").to_str().unwrap() + // ); + // ::std::process::exit(0); + + Ok(()) +} diff --git a/components/places/tests/fennec_history_schema.sql b/components/places/tests/fennec_history_schema.sql new file mode 100644 index 0000000000..f5229a7bcf --- /dev/null +++ b/components/places/tests/fennec_history_schema.sql @@ -0,0 +1,36 @@ +PRAGMA user_version=39; +PRAGMA foreign_keys=ON; +PRAGMA synchronous=NORMAL; + +CREATE TABLE history ( + _id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT, + url TEXT NOT NULL, + visits INTEGER NOT NULL DEFAULT 0, + visits_local INTEGER NOT NULL DEFAULT 0, + visits_remote INTEGER NOT NULL DEFAULT 0, + favicon_id INTEGER, + date INTEGER, + date_local INTEGER NOT NULL DEFAULT 0, + date_remote INTEGER NOT NULL DEFAULT 0, + created INTEGER, + modified INTEGER, + guid TEXT NOT NULL, + deleted INTEGER NOT NULL DEFAULT 0 +); + +CREATE TABLE visits ( + _id INTEGER PRIMARY KEY AUTOINCREMENT, + history_guid TEXT NOT NULL, + visit_type TINYINT NOT NULL DEFAULT 1, + date INTEGER NOT NULL, + is_local TINYINT NOT NULL DEFAULT 1, + FOREIGN KEY (history_guid) REFERENCES history(guid) ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE UNIQUE INDEX history_guid_index ON history(guid); +CREATE INDEX history_modified_index ON history(modified); +CREATE INDEX history_url_index ON history(url); +CREATE INDEX history_visited_index ON history(date); +CREATE UNIQUE INDEX visits_history_guid_and_date_visited_index ON visits(history_guid,date); +CREATE INDEX visits_history_guid_index ON visits(history_guid); diff --git a/components/places/tests/mod.rs b/components/places/tests/mod.rs index 37b67fa580..7a55b16095 100644 --- a/components/places/tests/mod.rs +++ b/components/places/tests/mod.rs @@ -2,4 +2,5 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +mod fennec_history; mod ios_bookmarks;