Bug 1791851 - mach vendor changes for tabs component r=teshaq,LougeniaBailey,supply-chain-reviewers

Differential Revision: https://phabricator.services.mozilla.com/D157978
This commit is contained in:
Sammy Khamis 2022-10-25 20:03:21 +00:00
parent f9aff9e98e
commit 91b6f8fd65
28 changed files with 1911 additions and 27 deletions

View File

@ -50,7 +50,7 @@ rev = "fb7a2b12ced3b43e6a268621989c6191d1ed7e39"
[source."https://github.com/mozilla/application-services"]
git = "https://github.com/mozilla/application-services"
replace-with = "vendored-sources"
rev = "d8503475f43dbf1d78eef4e23b0578d0fada3f39"
rev = "b09ffe23ee60a066176e5d7f9f2c6cd95c528ceb"
[source."https://github.com/mozilla-spidermonkey/jsparagus"]
git = "https://github.com/mozilla-spidermonkey/jsparagus"

41
Cargo.lock generated
View File

@ -1535,7 +1535,7 @@ dependencies = [
[[package]]
name = "error-support"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=d8503475f43dbf1d78eef4e23b0578d0fada3f39#d8503475f43dbf1d78eef4e23b0578d0fada3f39"
source = "git+https://github.com/mozilla/application-services?rev=b09ffe23ee60a066176e5d7f9f2c6cd95c528ceb#b09ffe23ee60a066176e5d7f9f2c6cd95c528ceb"
dependencies = [
"log",
]
@ -2185,6 +2185,7 @@ dependencies = [
"rust_minidump_writer_linux",
"static_prefs",
"storage",
"tabs",
"tokio-reactor",
"tokio-threadpool",
"unic-langid",
@ -2664,7 +2665,7 @@ dependencies = [
[[package]]
name = "interrupt-support"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=d8503475f43dbf1d78eef4e23b0578d0fada3f39#d8503475f43dbf1d78eef4e23b0578d0fada3f39"
source = "git+https://github.com/mozilla/application-services?rev=b09ffe23ee60a066176e5d7f9f2c6cd95c528ceb#b09ffe23ee60a066176e5d7f9f2c6cd95c528ceb"
dependencies = [
"lazy_static",
"parking_lot 0.12.999",
@ -3751,7 +3752,7 @@ dependencies = [
[[package]]
name = "nss_build_common"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=d8503475f43dbf1d78eef4e23b0578d0fada3f39#d8503475f43dbf1d78eef4e23b0578d0fada3f39"
source = "git+https://github.com/mozilla/application-services?rev=b09ffe23ee60a066176e5d7f9f2c6cd95c528ceb#b09ffe23ee60a066176e5d7f9f2c6cd95c528ceb"
[[package]]
name = "nsstring"
@ -4930,7 +4931,7 @@ dependencies = [
[[package]]
name = "sql-support"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=d8503475f43dbf1d78eef4e23b0578d0fada3f39#d8503475f43dbf1d78eef4e23b0578d0fada3f39"
source = "git+https://github.com/mozilla/application-services?rev=b09ffe23ee60a066176e5d7f9f2c6cd95c528ceb#b09ffe23ee60a066176e5d7f9f2c6cd95c528ceb"
dependencies = [
"ffi-support",
"interrupt-support",
@ -5112,7 +5113,7 @@ dependencies = [
[[package]]
name = "sync-guid"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=d8503475f43dbf1d78eef4e23b0578d0fada3f39#d8503475f43dbf1d78eef4e23b0578d0fada3f39"
source = "git+https://github.com/mozilla/application-services?rev=b09ffe23ee60a066176e5d7f9f2c6cd95c528ceb#b09ffe23ee60a066176e5d7f9f2c6cd95c528ceb"
dependencies = [
"base64",
"rand 0.8.5",
@ -5123,7 +5124,7 @@ dependencies = [
[[package]]
name = "sync15"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=d8503475f43dbf1d78eef4e23b0578d0fada3f39#d8503475f43dbf1d78eef4e23b0578d0fada3f39"
source = "git+https://github.com/mozilla/application-services?rev=b09ffe23ee60a066176e5d7f9f2c6cd95c528ceb#b09ffe23ee60a066176e5d7f9f2c6cd95c528ceb"
dependencies = [
"anyhow",
"error-support",
@ -5150,6 +5151,30 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "tabs"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=b09ffe23ee60a066176e5d7f9f2c6cd95c528ceb#b09ffe23ee60a066176e5d7f9f2c6cd95c528ceb"
dependencies = [
"anyhow",
"error-support",
"interrupt-support",
"lazy_static",
"log",
"rusqlite",
"serde",
"serde_derive",
"serde_json",
"sql-support",
"sync-guid",
"sync15",
"thiserror",
"uniffi",
"uniffi_build",
"uniffi_macros",
"url",
]
[[package]]
name = "tap"
version = "1.0.1"
@ -5858,7 +5883,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "viaduct"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=d8503475f43dbf1d78eef4e23b0578d0fada3f39#d8503475f43dbf1d78eef4e23b0578d0fada3f39"
source = "git+https://github.com/mozilla/application-services?rev=b09ffe23ee60a066176e5d7f9f2c6cd95c528ceb#b09ffe23ee60a066176e5d7f9f2c6cd95c528ceb"
dependencies = [
"ffi-support",
"log",
@ -6014,7 +6039,7 @@ dependencies = [
[[package]]
name = "webext-storage"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=d8503475f43dbf1d78eef4e23b0578d0fada3f39#d8503475f43dbf1d78eef4e23b0578d0fada3f39"
source = "git+https://github.com/mozilla/application-services?rev=b09ffe23ee60a066176e5d7f9f2c6cd95c528ceb#b09ffe23ee60a066176e5d7f9f2c6cd95c528ceb"
dependencies = [
"error-support",
"ffi-support",

View File

@ -157,11 +157,12 @@ midir = { git = "https://github.com/mozilla/midir.git", rev = "e1b4dcb767f9e69af
minidump_writer_linux = { git = "https://github.com/rust-minidump/minidump-writer.git", rev = "75ada456c92a429704691a85e1cb42fef8cafc0d" }
# application-services overrides to make updating them all simpler.
interrupt-support = { git = "https://github.com/mozilla/application-services", rev = "d8503475f43dbf1d78eef4e23b0578d0fada3f39" }
sql-support = { git = "https://github.com/mozilla/application-services", rev = "d8503475f43dbf1d78eef4e23b0578d0fada3f39" }
sync15 = { git = "https://github.com/mozilla/application-services", rev = "d8503475f43dbf1d78eef4e23b0578d0fada3f39" }
viaduct = { git = "https://github.com/mozilla/application-services", rev = "d8503475f43dbf1d78eef4e23b0578d0fada3f39" }
webext-storage = { git = "https://github.com/mozilla/application-services", rev = "d8503475f43dbf1d78eef4e23b0578d0fada3f39" }
interrupt-support = { git = "https://github.com/mozilla/application-services", rev = "b09ffe23ee60a066176e5d7f9f2c6cd95c528ceb" }
sql-support = { git = "https://github.com/mozilla/application-services", rev = "b09ffe23ee60a066176e5d7f9f2c6cd95c528ceb" }
sync15 = { git = "https://github.com/mozilla/application-services", rev = "b09ffe23ee60a066176e5d7f9f2c6cd95c528ceb" }
tabs = { git = "https://github.com/mozilla/application-services", rev = "b09ffe23ee60a066176e5d7f9f2c6cd95c528ceb" }
viaduct = { git = "https://github.com/mozilla/application-services", rev = "b09ffe23ee60a066176e5d7f9f2c6cd95c528ceb" }
webext-storage = { git = "https://github.com/mozilla/application-services", rev = "b09ffe23ee60a066176e5d7f9f2c6cd95c528ceb" }
# Patch mio 0.6 to use winapi 0.3 and miow 0.3, getting rid of winapi 0.2.
# There is not going to be new version of mio 0.6, mio now being >= 0.7.11.

View File

@ -150,6 +150,10 @@ notes = "We're not shipping this and have no plans to ship it."
audit-as-crates-io = false
notes = "This is a first-party crate which is entirely unrelated to the crates.io package of the same name."
[policy.tabs]
audit-as-crates-io = false
notes = "This is a first-party crate, maintained by the appservices team, which is entirely unrelated to the crates.io package of the same name."
[policy.viaduct]
audit-as-crates-io = false
notes = "This is a first-party crate, maintained by the appservices team, which is entirely unrelated to the crates.io package of the same name."

View File

@ -1 +1 @@
{"files":{"Cargo.toml":"855560578398739e78d5418fb58ed717cb338ee378b028a34739386299f5f717","android/build.gradle":"200fe9fcf26477ae4e941dd1e702c43deae9fb0a7252569bd7352eac1771efbe","android/src/main/AndroidManifest.xml":"4f8b16fa6a03120ac810c6438a3a60294075414d92e06caa7e85388e389e5d17","build.rs":"3c128073c7dece175e6e7117fb363e8047fb997b2cfa8ab29f7c2cc484cb7916","src/errorsupport.udl":"be379c47340f504ae9885ca20cf9849d273c7dadc2782c5a53c1b41d5f06f32b","src/handling.rs":"221e80f2d7c79bcc5af687c2a350cc0c93652c2329fad9ace2073a75db048651","src/lib.rs":"5d996f16d289bce2a44fe8d7c5c538597770c9f67f425bab06e2efa982381ca5","src/macros.rs":"30a56a9ddaabb8b0f794b2ee76623277bc6dc9da41040bca54fc2e276fc0322e","src/reporting.rs":"65ab92cff0980f594da2c8556cc050066f137615818dbbd52152438b15a87816","uniffi.toml":"644fe81c12fe3c01ee81e017ca3c00d0e611f014b7eade51aadaf208179a3450"},"package":null}
{"files":{"Cargo.toml":"bfb27ec0065630fe2fb81346586eae8aef85ad093713679b8c3c87f10c2359b7","android/build.gradle":"200fe9fcf26477ae4e941dd1e702c43deae9fb0a7252569bd7352eac1771efbe","android/src/main/AndroidManifest.xml":"4f8b16fa6a03120ac810c6438a3a60294075414d92e06caa7e85388e389e5d17","build.rs":"3c128073c7dece175e6e7117fb363e8047fb997b2cfa8ab29f7c2cc484cb7916","src/errorsupport.udl":"be379c47340f504ae9885ca20cf9849d273c7dadc2782c5a53c1b41d5f06f32b","src/handling.rs":"eaf83a921116e3443d932582bb68871b8ffa336238f16f5d026b1fe75cea1d01","src/lib.rs":"5d996f16d289bce2a44fe8d7c5c538597770c9f67f425bab06e2efa982381ca5","src/macros.rs":"30a56a9ddaabb8b0f794b2ee76623277bc6dc9da41040bca54fc2e276fc0322e","src/reporting.rs":"65ab92cff0980f594da2c8556cc050066f137615818dbbd52152438b15a87816","uniffi.toml":"644fe81c12fe3c01ee81e017ca3c00d0e611f014b7eade51aadaf208179a3450"},"package":null}

View File

@ -9,8 +9,8 @@ license = "MPL-2.0"
log = "0.4"
lazy_static = { version = "1.4", optional = true }
parking_lot = { version = ">=0.11,<=0.12", optional = true }
uniffi = { version = "^0.20", optional = true }
uniffi_macros = { version = "^0.20", optional = true }
uniffi = { version = "^0.21", optional = true }
uniffi_macros = { version = "^0.21", optional = true }
[dependencies.backtrace]
optional = true
@ -21,4 +21,4 @@ default = []
reporting = ["lazy_static", "parking_lot", "uniffi", "uniffi_macros", "uniffi_build"]
[build-dependencies]
uniffi_build = { version = "^0.20", features=["builtin-bindgen"], optional = true }
uniffi_build = { version = "^0.21", features=["builtin-bindgen"], optional = true }

View File

@ -58,11 +58,16 @@ impl<E> ErrorHandling<E> {
// Convenience functions for the most common error reports
/// Add reporting to an ErrorHandling instance and also log an Error
/// log a warning
pub fn log_warning(self) -> Self {
self.log(log::Level::Warn)
}
/// log an info
pub fn log_info(self) -> Self {
self.log(log::Level::Info)
}
/// Add reporting to an ErrorHandling instance and also log an Error
pub fn report_error(self, report_class: impl Into<String>) -> Self {
Self {

View File

@ -1 +1 @@
{"files":{"Cargo.toml":"1f11acaa90a112979205b4c7af9ba0c015afab5f3141dd082d58c862c84490e3","README.md":"6d4ff5b079ac5340d18fa127f583e7ad793c5a2328b8ecd12c3fc723939804f2","src/bso_record.rs":"1983a4ed506e8ea3e749aca93aad672464cd6f370ff18f6108bda51f4a357260","src/client/coll_state.rs":"b0c47e44168ea2c7017cd8531f76bb230f9be66b119bb7416537b8693a1d0a0a","src/client/coll_update.rs":"021144c8606f8a7114b194ed830f4f756c75105146620f36b7ff9c37237d49f4","src/client/collection_keys.rs":"847296c161773931d3b9dcd6e1ec5ac740e69acc032faa15bb1eed6a300c6336","src/client/mod.rs":"9500b1d22a5064bbbd6a3d6bcc63fc4191e8ea4605ded359bc6c2dc2887626a3","src/client/request.rs":"b8996ebd27127c71c1ecfd329e925859df71caa5529f906b0ce2b565cf4362b6","src/client/state.rs":"590b8fc7458b7973d81878075e6cf65c5c529f9d9c9794e30e4158c8ded26727","src/client/status.rs":"f445a8765dac9789444e23b5145148413407bb1d18a15ef56682243997f591bf","src/client/storage_client.rs":"d2b52946f13a724a13f9f97b122ba84190cc334b30bb53c7c5791d35d115bf50","src/client/sync.rs":"ed7225c314df27793ed5de6da93cc4b75a98da1c14ac82e37a723a99821d4dc7","src/client/sync_multiple.rs":"a2f6372496cc37025b07b260f6990699323ceb460d8e44d320502ad8e858fa06","src/client/token.rs":"b268759d31e0fe17e0e2a428694cd9a317fcfbdd52f023d5d8c7cc6f00f1a102","src/client/util.rs":"71cc70ee41f821f53078675e636e9fad9c6046fa1a989e37f5487e340a2277d6","src/client_types.rs":"d4cdc44ab41cd82a1153eafa4d963d65114dfc18c7dd49138f924fce52d1a7f5","src/clients_engine/engine.rs":"856a099586af0e0d897437e6e2cea1244169b7406e0809e0d3f17b8970e0ad69","src/clients_engine/mod.rs":"461729e6f89b66b2cbd89b041a03d4d6a8ba582284ed4f3015cb13e1a0c6da97","src/clients_engine/record.rs":"59826b7f21b45d3dbee7b332abde774cb9cfa82eaa5e11a96ec95cb7d8f5a45f","src/clients_engine/ser.rs":"9796e44ed7daf04f22afbb51238ac25fd0de1438b72181351b4ca29fd70fd429","src/engine/bridged_engine.rs":"34e63cfa654b877fce6996823c60b9a74002cf07639781ca8b1cc22f3f944fdd","src/engine/changeset.rs":"442aa92b5130ec0f8f2b0054acb399c547380e0060015cbf4ca7a72027440d54","src/engine/mod.rs":"67d0d7b05ab7acff03180ce0337340297111697b96eb876046e24314f14226c5","src/engine/request.rs":"f40bac0b3f5286446a4056de885fd81e4fa77e4dc7d5bbb6aa644b93201046de","src/engine/sync_engine.rs":"4d034a0f03a87bb0034509c16853de5de2ad30bfc6a25b815b578f9b5f7ae44e","src/error.rs":"a45cfe02e6301f473c34678b694943c1a04308b8c292c6e0448bf495194c3b5e","src/key_bundle.rs":"7991905758c730e7e100064559b7661c36bb8be15476467cf94f65a417f1a28a","src/lib.rs":"a6df9f32ecd622c0286582cf859072b51bc233caf9c8f7bda861a03d8fddea84","src/payload.rs":"98710dda512d5f7eccecf84c7c1cd3af37a8b360166de20ae0aca37e7461454c","src/record_types.rs":"02bb3d352fb808131d298f9b90d9c95b7e9e0138b97c5401f3b9fdacc5562f44","src/server_timestamp.rs":"ff45c59ff0be51a6de6d0ea43d6d6aa6806ada9847446c3bb178e8f0a43a4f89","src/telemetry.rs":"3471aaaaca275496ec6880723e076ce39b44fb351ca88e53fe63750a43255c33"},"package":null}
{"files":{"Cargo.toml":"1f11acaa90a112979205b4c7af9ba0c015afab5f3141dd082d58c862c84490e3","README.md":"6d4ff5b079ac5340d18fa127f583e7ad793c5a2328b8ecd12c3fc723939804f2","src/bso_record.rs":"1983a4ed506e8ea3e749aca93aad672464cd6f370ff18f6108bda51f4a357260","src/client/coll_state.rs":"b0c47e44168ea2c7017cd8531f76bb230f9be66b119bb7416537b8693a1d0a0a","src/client/coll_update.rs":"021144c8606f8a7114b194ed830f4f756c75105146620f36b7ff9c37237d49f4","src/client/collection_keys.rs":"847296c161773931d3b9dcd6e1ec5ac740e69acc032faa15bb1eed6a300c6336","src/client/mod.rs":"9500b1d22a5064bbbd6a3d6bcc63fc4191e8ea4605ded359bc6c2dc2887626a3","src/client/request.rs":"b8996ebd27127c71c1ecfd329e925859df71caa5529f906b0ce2b565cf4362b6","src/client/state.rs":"590b8fc7458b7973d81878075e6cf65c5c529f9d9c9794e30e4158c8ded26727","src/client/status.rs":"f445a8765dac9789444e23b5145148413407bb1d18a15ef56682243997f591bf","src/client/storage_client.rs":"d2b52946f13a724a13f9f97b122ba84190cc334b30bb53c7c5791d35d115bf50","src/client/sync.rs":"ed7225c314df27793ed5de6da93cc4b75a98da1c14ac82e37a723a99821d4dc7","src/client/sync_multiple.rs":"a2f6372496cc37025b07b260f6990699323ceb460d8e44d320502ad8e858fa06","src/client/token.rs":"b268759d31e0fe17e0e2a428694cd9a317fcfbdd52f023d5d8c7cc6f00f1a102","src/client/util.rs":"71cc70ee41f821f53078675e636e9fad9c6046fa1a989e37f5487e340a2277d6","src/client_types.rs":"c53e6fa8e9d5c7b56a87c6803ec3fc808d471b1d8c20c0fbb4ec0c02571b21ba","src/clients_engine/engine.rs":"856a099586af0e0d897437e6e2cea1244169b7406e0809e0d3f17b8970e0ad69","src/clients_engine/mod.rs":"461729e6f89b66b2cbd89b041a03d4d6a8ba582284ed4f3015cb13e1a0c6da97","src/clients_engine/record.rs":"59826b7f21b45d3dbee7b332abde774cb9cfa82eaa5e11a96ec95cb7d8f5a45f","src/clients_engine/ser.rs":"9796e44ed7daf04f22afbb51238ac25fd0de1438b72181351b4ca29fd70fd429","src/engine/bridged_engine.rs":"f7bb70dbc2eec46fe5ba8952c867e20b794fc01a514dc360bb5a1f15508958f9","src/engine/changeset.rs":"442aa92b5130ec0f8f2b0054acb399c547380e0060015cbf4ca7a72027440d54","src/engine/mod.rs":"67d0d7b05ab7acff03180ce0337340297111697b96eb876046e24314f14226c5","src/engine/request.rs":"f40bac0b3f5286446a4056de885fd81e4fa77e4dc7d5bbb6aa644b93201046de","src/engine/sync_engine.rs":"5314d0163ccc93d78f5879d52cf2b60b9622e80722d84d3482cfa7c26df6bfdd","src/error.rs":"a45cfe02e6301f473c34678b694943c1a04308b8c292c6e0448bf495194c3b5e","src/key_bundle.rs":"7991905758c730e7e100064559b7661c36bb8be15476467cf94f65a417f1a28a","src/lib.rs":"a6df9f32ecd622c0286582cf859072b51bc233caf9c8f7bda861a03d8fddea84","src/payload.rs":"98710dda512d5f7eccecf84c7c1cd3af37a8b360166de20ae0aca37e7461454c","src/record_types.rs":"02bb3d352fb808131d298f9b90d9c95b7e9e0138b97c5401f3b9fdacc5562f44","src/server_timestamp.rs":"ff45c59ff0be51a6de6d0ea43d6d6aa6806ada9847446c3bb178e8f0a43a4f89","src/telemetry.rs":"3471aaaaca275496ec6880723e076ce39b44fb351ca88e53fe63750a43255c33"},"package":null}

View File

@ -10,14 +10,14 @@ use std::collections::HashMap;
/// Argument to Store::prepare_for_sync. See comment there for more info. Only
/// really intended to be used by tabs engine.
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ClientData {
pub local_client_id: String,
pub recent_clients: HashMap<String, RemoteClient>,
}
/// Information about a remote client in the clients collection.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub struct RemoteClient {
pub fxa_device_id: Option<String>,
pub device_name: String,

View File

@ -49,6 +49,13 @@ pub trait BridgedEngine {
/// sync.
fn ensure_current_sync_id(&self, new_sync_id: &str) -> Result<String, Self::Error>;
/// Tells the tabs engine about recent FxA devices. A bit of a leaky abstration as it only
/// makes sense for tabs.
/// The arg is a json serialized `ClientData` struct.
fn prepare_for_sync(&self, _client_data: &str) -> Result<(), Self::Error> {
Ok(())
}
/// Indicates that the engine is about to start syncing. This is called
/// once per sync, and always before `store_incoming`.
fn sync_started(&self) -> Result<(), Self::Error>;

View File

@ -31,24 +31,30 @@ pub enum EngineSyncAssociation {
/// The concrete `SyncEngine` implementations
#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub enum SyncEngineId {
// Note that we've derived PartialOrd etc, which uses lexicographic ordering
// of the variants. We leverage that such that the higher priority engines
// are listed first.
// This order matches desktop.
Passwords,
History,
Bookmarks,
Tabs,
Bookmarks,
Addresses,
CreditCards,
History,
}
impl SyncEngineId {
// Iterate over all possible engines
// Iterate over all possible engines. Note that we've made a policy decision
// that this should enumerate in "order" as defined by PartialCmp, and tests
// enforce this.
pub fn iter() -> impl Iterator<Item = SyncEngineId> {
[
Self::Passwords,
Self::History,
Self::Bookmarks,
Self::Tabs,
Self::Bookmarks,
Self::Addresses,
Self::CreditCards,
Self::History,
]
.into_iter()
}
@ -195,3 +201,35 @@ pub trait SyncEngine {
fn wipe(&self) -> Result<()>;
}
#[cfg(test)]
mod test {
use super::*;
use std::iter::zip;
#[test]
fn test_engine_priority() {
fn sorted(mut engines: Vec<SyncEngineId>) -> Vec<SyncEngineId> {
engines.sort();
engines
}
assert_eq!(
vec![SyncEngineId::Passwords, SyncEngineId::Tabs],
sorted(vec![SyncEngineId::Passwords, SyncEngineId::Tabs])
);
assert_eq!(
vec![SyncEngineId::Passwords, SyncEngineId::Tabs],
sorted(vec![SyncEngineId::Tabs, SyncEngineId::Passwords])
);
}
#[test]
fn test_engine_enum_order() {
let unsorted = SyncEngineId::iter().collect::<Vec<SyncEngineId>>();
let mut sorted = SyncEngineId::iter().collect::<Vec<SyncEngineId>>();
sorted.sort();
// iterating should supply identical elements in each.
assert!(zip(unsorted, sorted).fold(true, |acc, (a, b)| acc && (a == b)))
}
}

View File

@ -0,0 +1 @@
{"files":{"Cargo.toml":"1fcfae54aa01f8344623621747bc61a338adc333106871f8f9fd44d0c53ab2a1","README.md":"c48b8f391ef822c4f3971b5f453a1e7b43bea232752d520460d2f04803aead1a","build.rs":"024918c1d468c8dae03e4edaad14d827b7ebe7995809a8fe99efb1d9faa1206a","src/error.rs":"83a9a80b4b0405a3f62876ef9046bcbf769ce61889f9d1d3f43c2b697c1b0ec7","src/lib.rs":"fcd82e1c98ad6de8a1aa4a26a55d5dd8f65027b39db5eaf1c037b6c9b5b179a2","src/schema.rs":"0f1c847b44733bfe44b5aec5ff807771e605e3e7302bd9b31a103f530edc4355","src/storage.rs":"778224dd3bcf3ed93b2a8eaa58306e3b3cd0e7f9f40b238fcc20b381af0e6e21","src/store.rs":"ab0b6214b30b0f0fa7c6a89098ff3db1a8f76264f6711c4481c0be460afe522b","src/sync/bridge.rs":"18e890529cadd67b1cf62968e224efa986a996393fd6e3bfcc5bd335846ab5fa","src/sync/engine.rs":"64e01b9f187603bfa727bb54f547d0b7b4ce0f3e50a0e6f638788529345c9207","src/sync/full_sync.rs":"e7837722d7c250e1653fef453338dae322aaf25f96a399d001e2b1bfdea894c8","src/sync/mod.rs":"2ebf9281101988efdcbec98d712b515101412161cb30176624772fcb4a9fba02","src/sync/record.rs":"a3f7dd114a1e3f2e3483bbccc3f91737ae18e5c118a5437203523dd2793ef370","src/tabs.udl":"a40c17ef513cb3c86c3148e0f1bdafebe618025046bb97ca1ad511d45cc76d34","uniffi.toml":"5156701368f0b5856e658143714a43058385c8ac53bee72d7a5a332b576dfb82"},"package":null}

45
third_party/rust/tabs/Cargo.toml vendored Normal file
View File

@ -0,0 +1,45 @@
[package]
name = "tabs"
edition = "2021"
version = "0.1.0"
authors = ["application-services@mozilla.com"]
license = "MPL-2.0"
exclude = ["/android", "/ios"]
[features]
# When used in desktop we *do not* want the full-sync implementation so desktop
# doesn't get our crypto etc.
# When used on mobile, for simplicity we *do* still expose the unused "bridged engine"
# because none of the engine implementations need the crypto.
default = []
# TODO: we've enabled the "standalone-sync" feature - see the description
# of this feature in sync15's Cargo.toml for what we should do instead.
# (The short version here is that once tabs doesn't need to expose a `sync()`
# method for iOS, we can kill the `full-sync` feature entirely)
full-sync = ["sync15/standalone-sync"]
[dependencies]
anyhow = "1.0"
error-support = { path = "../support/error" }
interrupt-support = { path = "../support/interrupt" }
lazy_static = "1.4"
log = "0.4"
rusqlite = { version = "0.27.0", features = ["bundled", "unlock_notify"] }
serde = "1"
serde_derive = "1"
serde_json = "1"
sql-support = { path = "../support/sql" }
sync-guid = { path = "../support/guid", features = ["random"] }
sync15 = { path = "../sync15", features = ["sync-engine"] }
thiserror = "1.0"
uniffi = "^0.21"
uniffi_macros = "^0.21"
url = "2.1" # mozilla-central can't yet take 2.2 (see bug 1734538)
[dev-dependencies]
tempfile = "3.1"
env_logger = { version = "0.8.0", default-features = false, features = ["termcolor", "atty", "humantime"] }
[build-dependencies]
uniffi_build = { version = "^0.21", features = [ "builtin-bindgen" ]}

62
third_party/rust/tabs/README.md vendored Normal file
View File

@ -0,0 +1,62 @@
# Synced Tabs Component
![status-img](https://img.shields.io/static/v1?label=not%20implemented&message=Firefox%20Preview,%20Desktop,%20iOS&color=darkred)
## Implementation Overview
This crate implements an in-memory syncing engine for remote tabs.
## Directory structure
The relevant directories are as follows:
- `src`: The meat of the library. This contains cross-platform rust code that
implements the syncing of tabs.
- `ffi`: The Rust public FFI bindings. This is a (memory-unsafe, by necessity)
API that is exposed to Kotlin and Swift. It leverages the `ffi_support` crate
to avoid many issues and make it more safe than it otherwise would be.
It uses protocol buffers for marshalling data over the FFI.
- `android`: This contains android bindings to synced tabs, written in Kotlin. These
use JNA to call into to the code in `ffi`.
- `ios`: This contains the iOS binding to synced tabs, written in Swift. These use
Swift's native support for calling code written in C to call into the code in
`ffi`.
## Features
- Synchronization of the local and remote session states.
## Business Logic
### Storage
The storage is all done in memory for simplicity purposes. The host applications are free to persist the remote tabs list if it makes sense to them.
### Payload format
Every remote sync record is roughly a list of tabs with their URL history (think of the back button). There is one record for each client.
### Association with device IDs
Each remote tabs sync record is associated to a "client" using a `client_id` field, which is really a foreign-key to a `clients` collection record.
However, because we'd like to move away from the clients collection, which is why this crate associates these records with Firefox Accounts device ids.
Currently for platforms using the sync-manager provided in this repo, the `client_id` is really the Firefox Accounts device ID and all is well, however for older platforms it is a distinct ID, which is why we have to feed the `clients` collection to this Tabs Sync engine to associate the correct Firefox Account device id.
## Getting started
**Prerequisites**: Firefox account authentication is necessary to obtain the keys to decrypt synced tabs data. See the [android-components FxA Client readme](https://github.com/mozilla-mobile/android-components/blob/master/components/service/firefox-accounts/README.md) for details on how to implement on Android. For iOS, Firefox for iOS still implement the legacy oauth.
**Platform-specific details**:
- <TODO-ST> Android
- iOS: start with the [guide to consuming rust components on iOS](https://github.com/mozilla/application-services/blob/main/docs/howtos/consuming-rust-components-on-ios.md)
## API Documentation
- TODO
## Testing
<TODO-ST>
## Telemetry
<TODO-ST>
## Examples
<TODO-ST>

7
third_party/rust/tabs/build.rs vendored Normal file
View File

@ -0,0 +1,7 @@
/* 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/. */
fn main() {
uniffi_build::generate_scaffolding("./src/tabs.udl").unwrap();
}

37
third_party/rust/tabs/src/error.rs vendored Normal file
View File

@ -0,0 +1,37 @@
/* 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/. */
#[derive(Debug, thiserror::Error)]
pub enum TabsError {
#[cfg(feature = "full-sync")]
#[error("Error synchronizing: {0}")]
SyncAdapterError(#[from] sync15::Error),
// Note we are abusing this as a kind of "mis-matched feature" error.
// This works because when `full-sync` isn't enabled we don't actually
// handle any sync15 errors as the bridged-engine never returns them.
#[cfg(not(feature = "full-sync"))]
#[error("Sync feature is disabled: {0}")]
SyncAdapterError(String),
#[error("Sync reset error: {0}")]
SyncResetError(#[from] anyhow::Error),
#[error("Error parsing JSON data: {0}")]
JsonError(#[from] serde_json::Error),
#[error("Missing SyncUnlockInfo Local ID")]
MissingLocalIdError,
#[error("Error parsing URL: {0}")]
UrlParseError(#[from] url::ParseError),
#[error("Error executing SQL: {0}")]
SqlError(#[from] rusqlite::Error),
#[error("Error opening database: {0}")]
OpenDatabaseError(#[from] sql_support::open_database::Error),
}
pub type Result<T> = std::result::Result<T, TabsError>;

39
third_party/rust/tabs/src/lib.rs vendored Normal file
View File

@ -0,0 +1,39 @@
/* 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/. */
#![allow(unknown_lints)]
#![warn(rust_2018_idioms)]
#[macro_use]
pub mod error;
mod schema;
mod storage;
mod store;
mod sync;
uniffi_macros::include_scaffolding!("tabs");
// Our UDL uses a `Guid` type.
use sync_guid::Guid as TabsGuid;
impl UniffiCustomTypeConverter for TabsGuid {
type Builtin = String;
fn into_custom(val: Self::Builtin) -> uniffi::Result<TabsGuid> {
Ok(TabsGuid::new(val.as_str()))
}
fn from_custom(obj: Self) -> Self::Builtin {
obj.into()
}
}
pub use crate::storage::{ClientRemoteTabs, RemoteTabRecord, TabsDeviceType};
pub use crate::store::TabsStore;
use error::TabsError;
use sync15::DeviceType;
pub use crate::sync::engine::get_registered_sync_engine;
pub use crate::sync::bridge::TabsBridgedEngine;
pub use crate::sync::engine::TabsEngine;

70
third_party/rust/tabs/src/schema.rs vendored Normal file
View File

@ -0,0 +1,70 @@
/* 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/. */
// Tabs is a bit special - it's a trivial SQL schema and is only used as a persistent
// cache, and the semantics of the "tabs" collection means there's no need for
// syncChangeCounter/syncStatus nor a mirror etc.
use rusqlite::{Connection, Transaction};
use sql_support::open_database::{
ConnectionInitializer as MigrationLogic, Error as MigrationError, Result as MigrationResult,
};
// The payload is json and this module doesn't need to deserialize, so we just
// store each "payload" as a row.
// On each Sync we delete all local rows re-populate them with every record on
// the server. When we read the DB, we also read every single record.
// So we have no primary keys, no foreign keys, and really completely waste the
// fact we are using sql.
const CREATE_SCHEMA_SQL: &str = "
CREATE TABLE IF NOT EXISTS tabs (
payload TEXT NOT NULL
);
";
pub struct TabsMigrationLogin;
impl MigrationLogic for TabsMigrationLogin {
const NAME: &'static str = "tabs storage db";
const END_VERSION: u32 = 1;
fn prepare(&self, conn: &Connection) -> MigrationResult<()> {
let initial_pragmas = "
-- We don't care about temp tables being persisted to disk.
PRAGMA temp_store = 2;
-- we unconditionally want write-ahead-logging mode.
PRAGMA journal_mode=WAL;
-- foreign keys seem worth enforcing (and again, we don't care in practice)
PRAGMA foreign_keys = ON;
";
conn.execute_batch(initial_pragmas)?;
// This is where we'd define our sql functions if we had any!
conn.set_prepared_statement_cache_capacity(128);
Ok(())
}
fn init(&self, db: &Transaction<'_>) -> MigrationResult<()> {
log::debug!("Creating schema");
db.execute_batch(CREATE_SCHEMA_SQL)?;
Ok(())
}
fn upgrade_from(&self, _db: &Transaction<'_>, version: u32) -> MigrationResult<()> {
Err(MigrationError::IncompatibleVersion(version))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::TabsStorage;
#[test]
fn test_create_schema_twice() {
let mut db = TabsStorage::new_with_mem_path("test");
let conn = db.open_or_create().unwrap();
conn.execute_batch(CREATE_SCHEMA_SQL)
.expect("should allow running twice");
}
}

329
third_party/rust/tabs/src/storage.rs vendored Normal file
View File

@ -0,0 +1,329 @@
/* 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/. */
// From https://searchfox.org/mozilla-central/rev/ea63a0888d406fae720cf24f4727d87569a8cab5/services/sync/modules/constants.js#75
const URI_LENGTH_MAX: usize = 65536;
// https://searchfox.org/mozilla-central/rev/ea63a0888d406fae720cf24f4727d87569a8cab5/services/sync/modules/engines/tabs.js#8
const TAB_ENTRIES_LIMIT: usize = 5;
use crate::error::*;
use crate::DeviceType;
use rusqlite::{Connection, OpenFlags};
use serde_derive::{Deserialize, Serialize};
use sql_support::open_database::{self, open_database_with_flags};
use sql_support::ConnExt;
use std::cell::RefCell;
use std::path::{Path, PathBuf};
pub type TabsDeviceType = crate::DeviceType;
pub type RemoteTabRecord = RemoteTab;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RemoteTab {
pub title: String,
pub url_history: Vec<String>,
pub icon: Option<String>,
pub last_used: i64, // In ms.
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ClientRemoteTabs {
pub client_id: String, // Corresponds to the `clients` collection ID of the client.
pub client_name: String,
#[serde(
default = "devicetype_default_deser",
skip_serializing_if = "devicetype_is_unknown"
)]
pub device_type: DeviceType,
pub remote_tabs: Vec<RemoteTab>,
}
fn devicetype_default_deser() -> DeviceType {
// replace with `DeviceType::default_deser` once #4861 lands.
DeviceType::Unknown
}
// Unlike most other uses-cases, here we do allow serializing ::Unknown, but skip it.
fn devicetype_is_unknown(val: &DeviceType) -> bool {
matches!(val, DeviceType::Unknown)
}
// Tabs has unique requirements for storage:
// * The "local_tabs" exist only so we can sync them out. There's no facility to
// query "local tabs", so there's no need to store these persistently - ie, they
// are write-only.
// * The "remote_tabs" exist purely for incoming items via sync - there's no facility
// to set them locally - they are read-only.
// Note that this means a database is only actually needed after Sync fetches remote tabs,
// and because sync users are in the minority, the use of a database here is purely
// optional and created on demand. The implication here is that asking for the "remote tabs"
// when no database exists is considered a normal situation and just implies no remote tabs exist.
// (Note however we don't attempt to remove the database when no remote tabs exist, so having
// no remote tabs in an existing DB is also a normal situation)
pub struct TabsStorage {
local_tabs: RefCell<Option<Vec<RemoteTab>>>,
db_path: PathBuf,
db_connection: Option<Connection>,
}
impl TabsStorage {
pub fn new(db_path: impl AsRef<Path>) -> Self {
Self {
local_tabs: RefCell::default(),
db_path: db_path.as_ref().to_path_buf(),
db_connection: None,
}
}
/// Arrange for a new memory-based TabsStorage. As per other DB semantics, creating
/// this isn't enough to actually create the db!
pub fn new_with_mem_path(db_path: &str) -> Self {
let name = PathBuf::from(format!("file:{}?mode=memory&cache=shared", db_path));
Self::new(name)
}
/// If a DB file exists, open and return it.
pub fn open_if_exists(&mut self) -> Result<Option<&Connection>> {
if let Some(ref existing) = self.db_connection {
return Ok(Some(existing));
}
let flags = OpenFlags::SQLITE_OPEN_NO_MUTEX
| OpenFlags::SQLITE_OPEN_URI
| OpenFlags::SQLITE_OPEN_READ_WRITE;
match open_database_with_flags(
self.db_path.clone(),
flags,
&crate::schema::TabsMigrationLogin,
) {
Ok(conn) => {
self.db_connection = Some(conn);
Ok(self.db_connection.as_ref())
}
Err(open_database::Error::SqlError(rusqlite::Error::SqliteFailure(code, _)))
if code.code == rusqlite::ErrorCode::CannotOpen =>
{
Ok(None)
}
Err(e) => Err(e.into()),
}
}
/// Open and return the DB, creating it if necessary.
pub fn open_or_create(&mut self) -> Result<&Connection> {
if let Some(ref existing) = self.db_connection {
return Ok(existing);
}
let flags = OpenFlags::SQLITE_OPEN_NO_MUTEX
| OpenFlags::SQLITE_OPEN_URI
| OpenFlags::SQLITE_OPEN_READ_WRITE
| OpenFlags::SQLITE_OPEN_CREATE;
let conn = open_database_with_flags(
self.db_path.clone(),
flags,
&crate::schema::TabsMigrationLogin,
)?;
self.db_connection = Some(conn);
Ok(self.db_connection.as_ref().unwrap())
}
pub fn update_local_state(&mut self, local_state: Vec<RemoteTab>) {
self.local_tabs.borrow_mut().replace(local_state);
}
pub fn prepare_local_tabs_for_upload(&self) -> Option<Vec<RemoteTab>> {
if let Some(local_tabs) = self.local_tabs.borrow().as_ref() {
return Some(
local_tabs
.iter()
.cloned()
.filter_map(|mut tab| {
if tab.url_history.is_empty() || !is_url_syncable(&tab.url_history[0]) {
return None;
}
let mut sanitized_history = Vec::with_capacity(TAB_ENTRIES_LIMIT);
for url in tab.url_history {
if sanitized_history.len() == TAB_ENTRIES_LIMIT {
break;
}
if is_url_syncable(&url) {
sanitized_history.push(url);
}
}
tab.url_history = sanitized_history;
Some(tab)
})
.collect(),
);
}
None
}
pub fn get_remote_tabs(&mut self) -> Option<Vec<ClientRemoteTabs>> {
match self.open_if_exists() {
Err(e) => {
log::error!("Failed to read remote tabs: {}", e);
None
}
Ok(None) => None,
Ok(Some(c)) => {
match c.query_rows_and_then_cached(
"SELECT payload FROM tabs",
[],
|row| -> Result<_> { Ok(serde_json::from_str(&row.get::<_, String>(0)?)?) },
) {
Ok(crts) => Some(crts),
Err(e) => {
log::error!("Failed to read database: {}", e);
None
}
}
}
}
}
}
impl TabsStorage {
pub(crate) fn replace_remote_tabs(
&mut self,
new_remote_tabs: Vec<ClientRemoteTabs>,
) -> Result<()> {
let connection = self.open_or_create()?;
let tx = connection.unchecked_transaction()?;
// delete the world - we rebuild it from scratch every sync.
tx.execute_batch("DELETE FROM tabs")?;
for crt in new_remote_tabs {
tx.execute_cached(
"INSERT INTO tabs (payload) VALUES (:payload);",
rusqlite::named_params! {
":payload": serde_json::to_string(&crt).expect("tabs don't fail to serialize"),
},
)?;
}
tx.commit()?;
Ok(())
}
pub(crate) fn wipe_remote_tabs(&mut self) -> Result<()> {
if let Some(db) = self.open_if_exists()? {
db.execute_batch("DELETE FROM tabs")?;
}
Ok(())
}
pub(crate) fn wipe_local_tabs(&self) {
self.local_tabs.replace(None);
}
}
fn is_url_syncable(url: &str) -> bool {
url.len() <= URI_LENGTH_MAX
&& !(url.starts_with("about:")
|| url.starts_with("resource:")
|| url.starts_with("chrome:")
|| url.starts_with("wyciwyg:")
|| url.starts_with("blob:")
|| url.starts_with("file:")
|| url.starts_with("moz-extension:"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_url_syncable() {
assert!(is_url_syncable("https://bobo.com"));
assert!(is_url_syncable("ftp://bobo.com"));
assert!(!is_url_syncable("about:blank"));
// XXX - this smells wrong - we should insist on a valid complete URL?
assert!(is_url_syncable("aboutbobo.com"));
assert!(!is_url_syncable("file:///Users/eoger/bobo"));
}
#[test]
fn test_open_if_exists_no_file() {
let dir = tempfile::tempdir().unwrap();
let db_name = dir.path().join("test_open_for_read_no_file.db");
let mut storage = TabsStorage::new(db_name.clone());
assert!(storage.open_if_exists().unwrap().is_none());
storage.open_or_create().unwrap(); // will have created it.
// make a new storage, but leave the file alone.
let mut storage = TabsStorage::new(db_name);
// db file exists, so opening for read should open it.
assert!(storage.open_if_exists().unwrap().is_some());
}
#[test]
fn test_prepare_local_tabs_for_upload() {
let mut storage = TabsStorage::new_with_mem_path("test_prepare_local_tabs_for_upload");
assert_eq!(storage.prepare_local_tabs_for_upload(), None);
storage.update_local_state(vec![
RemoteTab {
title: "".to_owned(),
url_history: vec!["about:blank".to_owned(), "https://foo.bar".to_owned()],
icon: None,
last_used: 0,
},
RemoteTab {
title: "".to_owned(),
url_history: vec![
"https://foo.bar".to_owned(),
"about:blank".to_owned(),
"about:blank".to_owned(),
"about:blank".to_owned(),
"about:blank".to_owned(),
"about:blank".to_owned(),
"about:blank".to_owned(),
"about:blank".to_owned(),
],
icon: None,
last_used: 0,
},
RemoteTab {
title: "".to_owned(),
url_history: vec![
"https://foo.bar".to_owned(),
"about:blank".to_owned(),
"https://foo2.bar".to_owned(),
"https://foo3.bar".to_owned(),
"https://foo4.bar".to_owned(),
"https://foo5.bar".to_owned(),
"https://foo6.bar".to_owned(),
],
icon: None,
last_used: 0,
},
RemoteTab {
title: "".to_owned(),
url_history: vec![],
icon: None,
last_used: 0,
},
]);
assert_eq!(
storage.prepare_local_tabs_for_upload(),
Some(vec![
RemoteTab {
title: "".to_owned(),
url_history: vec!["https://foo.bar".to_owned()],
icon: None,
last_used: 0,
},
RemoteTab {
title: "".to_owned(),
url_history: vec![
"https://foo.bar".to_owned(),
"https://foo2.bar".to_owned(),
"https://foo3.bar".to_owned(),
"https://foo4.bar".to_owned(),
"https://foo5.bar".to_owned()
],
icon: None,
last_used: 0,
},
])
);
}
}

41
third_party/rust/tabs/src/store.rs vendored Normal file
View File

@ -0,0 +1,41 @@
/* 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::storage::{ClientRemoteTabs, RemoteTab, TabsStorage};
use std::path::Path;
use std::sync::Mutex;
pub struct TabsStore {
pub storage: Mutex<TabsStorage>,
}
impl TabsStore {
pub fn new(db_path: impl AsRef<Path>) -> Self {
Self {
storage: Mutex::new(TabsStorage::new(db_path)),
}
}
pub fn new_with_mem_path(db_path: &str) -> Self {
Self {
storage: Mutex::new(TabsStorage::new_with_mem_path(db_path)),
}
}
pub fn set_local_tabs(&self, local_state: Vec<RemoteTab>) {
self.storage.lock().unwrap().update_local_state(local_state);
}
// like remote_tabs, but serves the uniffi layer
pub fn get_all(&self) -> Vec<ClientRemoteTabs> {
match self.remote_tabs() {
Some(list) => list,
None => vec![],
}
}
pub fn remote_tabs(&self) -> Option<Vec<ClientRemoteTabs>> {
self.storage.lock().unwrap().get_remote_tabs()
}
}

445
third_party/rust/tabs/src/sync/bridge.rs vendored Normal file
View File

@ -0,0 +1,445 @@
/* 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 std::sync::{Arc, Mutex};
use crate::error::{Result, TabsError};
use crate::sync::engine::TabsSyncImpl;
use crate::sync::record::TabsRecord;
use crate::TabsStore;
use sync15::engine::{
ApplyResults, BridgedEngine, CollSyncIds, EngineSyncAssociation, IncomingEnvelope,
};
use sync15::{ClientData, Payload, ServerTimestamp};
use sync_guid::Guid as SyncGuid;
impl TabsStore {
// Returns a bridged sync engine for Desktop for this store.
pub fn bridged_engine(self: Arc<Self>) -> Arc<TabsBridgedEngine> {
let bridge_impl = crate::sync::bridge::BridgedEngineImpl::new(&self);
// This is a concrete struct exposed via uniffi.
let concrete = TabsBridgedEngine::new(bridge_impl);
Arc::new(concrete)
}
}
/// A bridged engine implements all the methods needed to make the
/// `storage.sync` store work with Desktop's Sync implementation.
/// Conceptually it's very similar to our SyncEngine, and once we have SyncEngine
/// and BridgedEngine using the same types we can probably combine them (or at least
/// implement this bridged engine purely in terms of SyncEngine)
/// See also #2841 and #5139
pub struct BridgedEngineImpl {
sync_impl: Mutex<TabsSyncImpl>,
incoming_payload: Mutex<Vec<IncomingEnvelope>>,
}
impl BridgedEngineImpl {
/// Creates a bridged engine for syncing.
pub fn new(store: &Arc<TabsStore>) -> Self {
Self {
sync_impl: Mutex::new(TabsSyncImpl::new(store.clone())),
incoming_payload: Mutex::default(),
}
}
}
impl BridgedEngine for BridgedEngineImpl {
type Error = TabsError;
fn last_sync(&self) -> Result<i64> {
Ok(self
.sync_impl
.lock()
.unwrap()
.last_sync
.unwrap_or_default()
.as_millis())
}
fn set_last_sync(&self, last_sync_millis: i64) -> Result<()> {
self.sync_impl.lock().unwrap().last_sync =
Some(ServerTimestamp::from_millis(last_sync_millis));
Ok(())
}
fn sync_id(&self) -> Result<Option<String>> {
Ok(match self.sync_impl.lock().unwrap().get_sync_assoc() {
EngineSyncAssociation::Connected(id) => Some(id.coll.to_string()),
EngineSyncAssociation::Disconnected => None,
})
}
fn reset_sync_id(&self) -> Result<String> {
let new_id = SyncGuid::random().to_string();
let new_coll_ids = CollSyncIds {
global: SyncGuid::empty(),
coll: new_id.clone().into(),
};
self.sync_impl
.lock()
.unwrap()
.reset(EngineSyncAssociation::Connected(new_coll_ids))?;
Ok(new_id)
}
fn ensure_current_sync_id(&self, sync_id: &str) -> Result<String> {
let mut sync_impl = self.sync_impl.lock().unwrap();
let assoc = sync_impl.get_sync_assoc();
if matches!(assoc, EngineSyncAssociation::Connected(c) if c.coll == sync_id) {
log::debug!("ensure_current_sync_id is current");
} else {
let new_coll_ids = CollSyncIds {
global: SyncGuid::empty(),
coll: sync_id.into(),
};
sync_impl.reset(EngineSyncAssociation::Connected(new_coll_ids))?;
}
Ok(sync_id.to_string()) // this is a bit odd, why the result?
}
fn prepare_for_sync(&self, client_data: &str) -> Result<()> {
let data: ClientData = serde_json::from_str(client_data)?;
Ok(self.sync_impl.lock().unwrap().prepare_for_sync(data)?)
}
fn sync_started(&self) -> Result<()> {
// This is a no-op for the Tabs Engine
Ok(())
}
fn store_incoming(&self, incoming_envelopes: &[IncomingEnvelope]) -> Result<()> {
// Store the incoming payload in memory so we can use it in apply
*(self.incoming_payload.lock().unwrap()) = incoming_envelopes.to_vec();
Ok(())
}
fn apply(&self) -> Result<ApplyResults> {
let incoming = self.incoming_payload.lock().unwrap();
// turn them into a TabRecord.
let mut records = Vec::with_capacity(incoming.len());
for inc in &*incoming {
// This error handling is a bit unfortunate, but will soon be removed as we
// move towards unifying the bridged_engine with a "real" engine.
let payload = match inc.payload() {
Ok(p) => p,
Err(e) => {
log::warn!("Ignoring invalid incoming envelope: {}", e);
continue;
}
};
let record = match TabsRecord::from_payload(payload) {
Ok(r) => r,
Err(e) => {
log::warn!("Ignoring invalid incoming tab record: {}", e);
continue;
}
};
records.push(record);
}
let mut sync_impl = self.sync_impl.lock().unwrap();
let outgoing = sync_impl.apply_incoming(records)?;
// Turn outgoing back into envelopes - a bit inefficient going via a Payload.
let mut outgoing_envelopes = Vec::with_capacity(1);
if let Some(outgoing_record) = outgoing {
let payload = Payload::from_record(outgoing_record)?;
outgoing_envelopes.push(payload.into());
}
Ok(ApplyResults {
envelopes: outgoing_envelopes,
num_reconciled: Some(0),
})
}
fn set_uploaded(&self, server_modified_millis: i64, ids: &[SyncGuid]) -> Result<()> {
Ok(self
.sync_impl
.lock()
.unwrap()
.sync_finished(ServerTimestamp::from_millis(server_modified_millis), ids)?)
}
fn sync_finished(&self) -> Result<()> {
*(self.incoming_payload.lock().unwrap()) = Vec::default();
Ok(())
}
fn reset(&self) -> Result<()> {
self.sync_impl
.lock()
.unwrap()
.reset(EngineSyncAssociation::Disconnected)?;
Ok(())
}
fn wipe(&self) -> Result<()> {
self.sync_impl.lock().unwrap().wipe()?;
Ok(())
}
}
// This is for uniffi to expose, and does nothing than delegate back to the trait.
pub struct TabsBridgedEngine {
bridge_impl: BridgedEngineImpl,
}
impl TabsBridgedEngine {
pub fn new(bridge_impl: BridgedEngineImpl) -> Self {
Self { bridge_impl }
}
pub fn last_sync(&self) -> Result<i64> {
self.bridge_impl.last_sync()
}
pub fn set_last_sync(&self, last_sync: i64) -> Result<()> {
self.bridge_impl.set_last_sync(last_sync)
}
pub fn sync_id(&self) -> Result<Option<String>> {
self.bridge_impl.sync_id()
}
pub fn reset_sync_id(&self) -> Result<String> {
self.bridge_impl.reset_sync_id()
}
pub fn ensure_current_sync_id(&self, sync_id: &str) -> Result<String> {
self.bridge_impl.ensure_current_sync_id(sync_id)
}
pub fn prepare_for_sync(&self, client_data: &str) -> Result<()> {
self.bridge_impl.prepare_for_sync(client_data)
}
pub fn sync_started(&self) -> Result<()> {
self.bridge_impl.sync_started()
}
pub fn store_incoming(&self, incoming: Vec<String>) -> Result<()> {
let mut envelopes = Vec::with_capacity(incoming.len());
for inc in incoming {
envelopes.push(serde_json::from_str::<IncomingEnvelope>(&inc)?);
}
self.bridge_impl.store_incoming(&envelopes)
}
pub fn apply(&self) -> Result<Vec<String>> {
let apply_results = self.bridge_impl.apply()?;
let mut envelopes = Vec::with_capacity(apply_results.envelopes.len());
for e in apply_results.envelopes {
envelopes.push(serde_json::to_string(&e)?);
}
Ok(envelopes)
}
pub fn set_uploaded(&self, server_modified_millis: i64, guids: Vec<SyncGuid>) -> Result<()> {
self.bridge_impl
.set_uploaded(server_modified_millis, &guids)
}
pub fn sync_finished(&self) -> Result<()> {
self.bridge_impl.sync_finished()
}
pub fn reset(&self) -> Result<()> {
self.bridge_impl.reset()
}
pub fn wipe(&self) -> Result<()> {
self.bridge_impl.wipe()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::RemoteTab;
use crate::sync::record::TabsRecordTab;
use serde_json::json;
use std::collections::HashMap;
use sync15::{ClientData, RemoteClient};
const TTL_1_YEAR: u32 = 31_622_400;
// A copy of the normal "engine" tests but which go via the bridge
#[test]
fn test_sync_via_bridge() {
env_logger::try_init().ok();
let store = Arc::new(TabsStore::new_with_mem_path("test-bridge_incoming"));
// Set some local tabs for our device.
let my_tabs = vec![
RemoteTab {
title: "my first tab".to_string(),
url_history: vec!["http://1.com".to_string()],
icon: None,
last_used: 0,
},
RemoteTab {
title: "my second tab".to_string(),
url_history: vec!["http://2.com".to_string()],
icon: None,
last_used: 1,
},
];
store.set_local_tabs(my_tabs.clone());
let bridge = store.bridged_engine();
let client_data = ClientData {
local_client_id: "my-device".to_string(),
recent_clients: HashMap::from([
(
"my-device".to_string(),
RemoteClient {
fxa_device_id: None,
device_name: "my device".to_string(),
device_type: None,
},
),
(
"device-no-tabs".to_string(),
RemoteClient {
fxa_device_id: None,
device_name: "device with no tabs".to_string(),
device_type: None,
},
),
(
"device-with-a-tab".to_string(),
RemoteClient {
fxa_device_id: None,
device_name: "device with a tab".to_string(),
device_type: None,
},
),
]),
};
bridge
.prepare_for_sync(&serde_json::to_string(&client_data).unwrap())
.expect("should work");
let records = vec![
// my-device should be ignored by sync - here it is acting as what our engine last
// wrote, but the actual tabs in our store we set above are what should be used.
json!({
"id": "my-device",
"clientName": "my device",
"tabs": [{
"title": "the title",
"urlHistory": [
"https://mozilla.org/"
],
"icon": "https://mozilla.org/icon",
"lastUsed": 1643764207
}]
}),
json!({
"id": "device-no-tabs",
"clientName": "device with no tabs",
"tabs": [],
}),
json!({
"id": "device-with-a-tab",
"clientName": "device with a tab",
"tabs": [{
"title": "the title",
"urlHistory": [
"https://mozilla.org/"
],
"icon": "https://mozilla.org/icon",
"lastUsed": 1643764207
}]
}),
// This has the main payload as OK but the tabs part invalid.
json!({
"id": "device-with-invalid-tab",
"clientName": "device with a tab",
"tabs": [{
"foo": "bar",
}]
}),
// We want this to be a valid payload but an invalid tab - so it needs an ID.
json!({
"id": "invalid-tab",
"foo": "bar"
}),
];
let mut incoming = Vec::new();
for record in records {
// Annoyingly we can't use `IncomingEnvelope` directly as it intentionally doesn't
// support Serialize - so need to use explicit json.
let envelope = json!({
"id": record.get("id"),
"modified": 0,
"payload": serde_json::to_string(&record).unwrap(),
});
incoming.push(serde_json::to_string(&envelope).unwrap());
}
bridge.store_incoming(incoming).expect("should store");
let out = bridge.apply().expect("should apply");
assert_eq!(out.len(), 1);
let ours = serde_json::from_str::<serde_json::Value>(&out[0]).unwrap();
// As above, can't use `OutgoingEnvelope` as it doesn't Deserialize.
// First, convert my_tabs from the local `RemoteTab` to the Sync specific `TabsRecord`
let expected_tabs: Vec<TabsRecordTab> =
my_tabs.into_iter().map(|t| t.to_record_tab()).collect();
let expected = json!({
"id": "my-device".to_string(),
"payload": json!({
// XXX - we aren't supposed to have the ID here, but this isn't a tabs
// issue, it's a pre-existing `Payload` issue.
"id": "my-device".to_string(),
"clientName": "my device",
"tabs": serde_json::to_value(expected_tabs).unwrap(),
}).to_string(),
"sortindex": (),
"ttl": TTL_1_YEAR,
});
assert_eq!(ours, expected);
bridge.set_uploaded(1234, vec![]).unwrap();
assert_eq!(bridge.last_sync().unwrap(), 1234);
}
#[test]
fn test_sync_meta() {
env_logger::try_init().ok();
let store = Arc::new(TabsStore::new_with_mem_path("test-meta"));
let bridge = store.bridged_engine();
bridge.set_last_sync(3).unwrap();
assert_eq!(bridge.last_sync().unwrap(), 3);
assert!(bridge.sync_id().unwrap().is_none());
bridge.ensure_current_sync_id("some_guid").unwrap();
assert_eq!(bridge.sync_id().unwrap(), Some("some_guid".to_string()));
// changing the sync ID should reset the timestamp
assert_eq!(bridge.last_sync().unwrap(), 0);
bridge.set_last_sync(3).unwrap();
bridge.reset_sync_id().unwrap();
// should now be a random guid.
assert_ne!(bridge.sync_id().unwrap(), Some("some_guid".to_string()));
// should have reset the last sync timestamp.
assert_eq!(bridge.last_sync().unwrap(), 0);
bridge.set_last_sync(3).unwrap();
// `reset` clears the guid and the timestamp
bridge.reset().unwrap();
assert_eq!(bridge.last_sync().unwrap(), 0);
assert!(bridge.sync_id().unwrap().is_none());
}
}

428
third_party/rust/tabs/src/sync/engine.rs vendored Normal file
View File

@ -0,0 +1,428 @@
/* 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::storage::{ClientRemoteTabs, RemoteTab};
use crate::store::TabsStore;
use crate::sync::record::{TabsRecord, TabsRecordTab};
use anyhow::Result;
use std::collections::HashMap;
use std::sync::{Arc, Mutex, Weak};
use sync15::engine::{
CollectionRequest, EngineSyncAssociation, IncomingChangeset, OutgoingChangeset, SyncEngine,
SyncEngineId,
};
use sync15::{telemetry, ClientData, DeviceType, Payload, RemoteClient, ServerTimestamp};
use sync_guid::Guid;
const TTL_1_YEAR: u32 = 31_622_400;
// Our "sync manager" will use whatever is stashed here.
lazy_static::lazy_static! {
// Mutex: just taken long enough to update the inner stuff
static ref STORE_FOR_MANAGER: Mutex<Weak<TabsStore>> = Mutex::new(Weak::new());
}
/// Called by the sync manager to get a sync engine via the store previously
/// registered with the sync manager.
pub fn get_registered_sync_engine(engine_id: &SyncEngineId) -> Option<Box<dyn SyncEngine>> {
let weak = STORE_FOR_MANAGER.lock().unwrap();
match weak.upgrade() {
None => None,
Some(store) => match engine_id {
SyncEngineId::Tabs => Some(Box::new(TabsEngine::new(Arc::clone(&store)))),
// panicing here seems reasonable - it's a static error if this
// it hit, not something that runtime conditions can influence.
_ => unreachable!("can't provide unknown engine: {}", engine_id),
},
}
}
impl ClientRemoteTabs {
fn from_record_with_remote_client(
client_id: String,
remote_client: &RemoteClient,
record: TabsRecord,
) -> Self {
Self {
client_id,
client_name: remote_client.device_name.clone(),
device_type: remote_client.device_type.unwrap_or(DeviceType::Unknown),
remote_tabs: record.tabs.iter().map(RemoteTab::from_record_tab).collect(),
}
}
fn from_record(client_id: String, record: TabsRecord) -> Self {
Self {
client_id,
client_name: record.client_name,
device_type: DeviceType::Unknown,
remote_tabs: record.tabs.iter().map(RemoteTab::from_record_tab).collect(),
}
}
fn to_record(&self) -> TabsRecord {
TabsRecord {
id: self.client_id.clone(),
client_name: self.client_name.clone(),
tabs: self
.remote_tabs
.iter()
.map(RemoteTab::to_record_tab)
.collect(),
ttl: TTL_1_YEAR,
}
}
}
impl RemoteTab {
fn from_record_tab(tab: &TabsRecordTab) -> Self {
Self {
title: tab.title.clone(),
url_history: tab.url_history.clone(),
icon: tab.icon.clone(),
last_used: tab.last_used.checked_mul(1000).unwrap_or_default(),
}
}
pub(super) fn to_record_tab(&self) -> TabsRecordTab {
TabsRecordTab {
title: self.title.clone(),
url_history: self.url_history.clone(),
icon: self.icon.clone(),
last_used: self.last_used.checked_div(1000).unwrap_or_default(),
}
}
}
// This is the implementation of syncing, which is used by the 2 different "sync engines"
// (We hope to get these 2 engines even closer in the future, but for now, we suck this up)
pub struct TabsSyncImpl {
pub(super) store: Arc<TabsStore>,
remote_clients: HashMap<String, RemoteClient>,
pub(super) last_sync: Option<ServerTimestamp>,
sync_store_assoc: EngineSyncAssociation,
pub(super) local_id: String,
}
impl TabsSyncImpl {
pub fn new(store: Arc<TabsStore>) -> Self {
Self {
store,
remote_clients: HashMap::new(),
last_sync: None,
sync_store_assoc: EngineSyncAssociation::Disconnected,
local_id: Default::default(),
}
}
pub fn prepare_for_sync(&mut self, client_data: ClientData) -> Result<()> {
self.remote_clients = client_data.recent_clients;
self.local_id = client_data.local_client_id;
Ok(())
}
pub fn apply_incoming(&mut self, inbound: Vec<TabsRecord>) -> Result<Option<TabsRecord>> {
let local_id = self.local_id.clone();
let mut remote_tabs = Vec::with_capacity(inbound.len());
for record in inbound {
if record.id == local_id {
// That's our own record, ignore it.
continue;
}
let id = record.id.clone();
let crt = if let Some(remote_client) = self.remote_clients.get(&id) {
ClientRemoteTabs::from_record_with_remote_client(
remote_client
.fxa_device_id
.as_ref()
.unwrap_or(&id)
.to_owned(),
remote_client,
record,
)
} else {
// A record with a device that's not in our remote clients seems unlikely, but
// could happen - in most cases though, it will be due to a disconnected client -
// so we really should consider just dropping it? (Sadly though, it does seem
// possible it's actually a very recently connected client, so we keep it)
log::info!(
"Storing tabs from a client that doesn't appear in the devices list: {}",
id,
);
ClientRemoteTabs::from_record(id, record)
};
remote_tabs.push(crt);
}
// We want to keep the mutex for as short as possible
let local_tabs = {
let mut storage = self.store.storage.lock().unwrap();
storage.replace_remote_tabs(remote_tabs)?;
storage.prepare_local_tabs_for_upload()
};
let outgoing = if let Some(local_tabs) = local_tabs {
let (client_name, device_type) = self
.remote_clients
.get(&local_id)
.map(|client| {
(
client.device_name.clone(),
client.device_type.unwrap_or(DeviceType::Unknown),
)
})
.unwrap_or_else(|| (String::new(), DeviceType::Unknown));
let local_record = ClientRemoteTabs {
client_id: local_id,
client_name,
device_type,
remote_tabs: local_tabs.to_vec(),
};
log::trace!("outgoing {:?}", local_record);
Some(local_record.to_record())
} else {
None
};
Ok(outgoing)
}
pub fn sync_finished(
&mut self,
new_timestamp: ServerTimestamp,
records_synced: &[Guid],
) -> Result<()> {
log::info!(
"sync completed after uploading {} records",
records_synced.len()
);
self.last_sync = Some(new_timestamp);
Ok(())
}
pub fn reset(&mut self, assoc: EngineSyncAssociation) -> Result<()> {
self.remote_clients.clear();
self.sync_store_assoc = assoc;
self.last_sync = None;
self.store.storage.lock().unwrap().wipe_remote_tabs()?;
Ok(())
}
pub fn wipe(&mut self) -> Result<()> {
self.reset(EngineSyncAssociation::Disconnected)?;
// not clear why we need to wipe the local tabs - the app is just going
// to re-add them?
self.store.storage.lock().unwrap().wipe_local_tabs();
Ok(())
}
pub fn get_sync_assoc(&self) -> &EngineSyncAssociation {
&self.sync_store_assoc
}
}
// This is the "SyncEngine" used when syncing via the Sync Manager.
pub struct TabsEngine {
pub sync_impl: Mutex<TabsSyncImpl>,
}
impl TabsEngine {
pub fn new(store: Arc<TabsStore>) -> Self {
Self {
sync_impl: Mutex::new(TabsSyncImpl::new(store)),
}
}
}
impl SyncEngine for TabsEngine {
fn collection_name(&self) -> std::borrow::Cow<'static, str> {
"tabs".into()
}
fn prepare_for_sync(&self, get_client_data: &dyn Fn() -> ClientData) -> Result<()> {
self.sync_impl
.lock()
.unwrap()
.prepare_for_sync(get_client_data())
}
fn apply_incoming(
&self,
inbound: Vec<IncomingChangeset>,
telem: &mut telemetry::Engine,
) -> Result<OutgoingChangeset> {
assert_eq!(inbound.len(), 1, "only requested one set of records");
let inbound = inbound.into_iter().next().unwrap();
let mut incoming_telemetry = telemetry::EngineIncoming::new();
let mut incoming_records = Vec::with_capacity(inbound.changes.len());
for incoming in inbound.changes {
let record = match TabsRecord::from_payload(incoming.0) {
Ok(record) => record,
Err(e) => {
log::warn!("Error deserializing incoming record: {}", e);
incoming_telemetry.failed(1);
continue;
}
};
incoming_records.push(record);
}
let outgoing_record = self
.sync_impl
.lock()
.unwrap()
.apply_incoming(incoming_records)?;
let mut outgoing = OutgoingChangeset::new("tabs", inbound.timestamp);
if let Some(outgoing_record) = outgoing_record {
let payload = Payload::from_record(outgoing_record)?;
outgoing.changes.push(payload);
}
telem.incoming(incoming_telemetry);
Ok(outgoing)
}
fn sync_finished(
&self,
new_timestamp: ServerTimestamp,
records_synced: Vec<Guid>,
) -> Result<()> {
self.sync_impl
.lock()
.unwrap()
.sync_finished(new_timestamp, &records_synced)
}
fn get_collection_requests(
&self,
server_timestamp: ServerTimestamp,
) -> Result<Vec<CollectionRequest>> {
let since = self.sync_impl.lock().unwrap().last_sync.unwrap_or_default();
Ok(if since == server_timestamp {
vec![]
} else {
vec![CollectionRequest::new("tabs").full().newer_than(since)]
})
}
fn get_sync_assoc(&self) -> Result<EngineSyncAssociation> {
Ok(self.sync_impl.lock().unwrap().get_sync_assoc().clone())
}
fn reset(&self, assoc: &EngineSyncAssociation) -> Result<()> {
self.sync_impl.lock().unwrap().reset(assoc.clone())
}
fn wipe(&self) -> Result<()> {
self.sync_impl.lock().unwrap().wipe()
}
}
impl crate::TabsStore {
// This allows the embedding app to say "make this instance available to
// the sync manager". The implementation is more like "offer to sync mgr"
// (thereby avoiding us needing to link with the sync manager) but
// `register_with_sync_manager()` is logically what's happening so that's
// the name it gets.
pub fn register_with_sync_manager(self: Arc<Self>) {
let mut state = STORE_FOR_MANAGER.lock().unwrap();
*state = Arc::downgrade(&self);
}
}
#[cfg(test)]
pub mod test {
use super::*;
use serde_json::json;
use sync15::DeviceType;
#[test]
fn test_incoming_tabs() {
env_logger::try_init().ok();
let engine = TabsEngine::new(Arc::new(TabsStore::new_with_mem_path("test-incoming")));
let records = vec![
json!({
"id": "device-no-tabs",
"clientName": "device with no tabs",
"tabs": [],
}),
json!({
"id": "device-with-a-tab",
"clientName": "device with a tab",
"tabs": [{
"title": "the title",
"urlHistory": [
"https://mozilla.org/"
],
"icon": "https://mozilla.org/icon",
"lastUsed": 1643764207
}]
}),
// This has the main payload as OK but the tabs part invalid.
json!({
"id": "device-with-invalid-tab",
"clientName": "device with a tab",
"tabs": [{
"foo": "bar",
}]
}),
// We want this to be a valid payload but an invalid tab - so it needs an ID.
json!({
"id": "invalid-tab",
"foo": "bar"
}),
];
let mut incoming = IncomingChangeset::new(engine.collection_name(), ServerTimestamp(0));
for record in records {
let payload = Payload::from_json(record).unwrap();
incoming.changes.push((payload, ServerTimestamp(0)));
}
let outgoing = engine
.apply_incoming(vec![incoming], &mut telemetry::Engine::new("tabs"))
.expect("Should apply incoming and stage outgoing records");
assert!(outgoing.changes.is_empty());
// now check the store has what we think it has.
let sync_impl = engine.sync_impl.lock().unwrap();
let mut storage = sync_impl.store.storage.lock().unwrap();
let mut crts = storage.get_remote_tabs().expect("should work");
crts.sort_by(|a, b| a.client_name.partial_cmp(&b.client_name).unwrap());
assert_eq!(crts.len(), 2, "we currently include devices with no tabs");
let crt = &crts[0];
assert_eq!(crt.client_name, "device with a tab");
assert_eq!(crt.device_type, DeviceType::Unknown);
assert_eq!(crt.remote_tabs.len(), 1);
assert_eq!(crt.remote_tabs[0].title, "the title");
let crt = &crts[1];
assert_eq!(crt.client_name, "device with no tabs");
assert_eq!(crt.device_type, DeviceType::Unknown);
assert_eq!(crt.remote_tabs.len(), 0);
}
#[test]
fn test_sync_manager_registration() {
let store = Arc::new(TabsStore::new_with_mem_path("test-registration"));
assert_eq!(Arc::strong_count(&store), 1);
assert_eq!(Arc::weak_count(&store), 0);
Arc::clone(&store).register_with_sync_manager();
assert_eq!(Arc::strong_count(&store), 1);
assert_eq!(Arc::weak_count(&store), 1);
let registered = STORE_FOR_MANAGER
.lock()
.unwrap()
.upgrade()
.expect("should upgrade");
assert!(Arc::ptr_eq(&store, &registered));
drop(registered);
// should be no new references
assert_eq!(Arc::strong_count(&store), 1);
assert_eq!(Arc::weak_count(&store), 1);
// dropping the registered object should drop the registration.
drop(store);
assert!(STORE_FOR_MANAGER.lock().unwrap().upgrade().is_none());
}
}

View File

@ -0,0 +1,67 @@
/* 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::{error::Result, TabsEngine, TabsStore};
use interrupt_support::NeverInterrupts;
use std::sync::Arc;
use sync15::client::{sync_multiple, MemoryCachedState, Sync15StorageClientInit};
use sync15::engine::{EngineSyncAssociation, SyncEngine};
use sync15::KeyBundle;
impl TabsStore {
pub fn reset(self: Arc<Self>) -> Result<()> {
let engine = TabsEngine::new(Arc::clone(&self));
engine.reset(&EngineSyncAssociation::Disconnected)?;
Ok(())
}
/// A convenience wrapper around sync_multiple.
pub fn sync(
self: Arc<Self>,
key_id: String,
access_token: String,
sync_key: String,
tokenserver_url: String,
local_id: String,
) -> Result<String> {
let mut mem_cached_state = MemoryCachedState::default();
let engine = TabsEngine::new(Arc::clone(&self));
// Since we are syncing without the sync manager, there's no
// command processor, therefore no clients engine, and in
// consequence `TabsStore::prepare_for_sync` is never called
// which means our `local_id` will never be set.
// Do it here.
engine.sync_impl.lock().unwrap().local_id = local_id;
let storage_init = &Sync15StorageClientInit {
key_id,
access_token,
tokenserver_url: url::Url::parse(tokenserver_url.as_str())?,
};
let root_sync_key = &KeyBundle::from_ksync_base64(sync_key.as_str())?;
let mut result = sync_multiple(
&[&engine],
&mut None,
&mut mem_cached_state,
storage_init,
root_sync_key,
&NeverInterrupts,
None,
);
// for b/w compat reasons, we do some dances with the result.
// XXX - note that this means telemetry isn't going to be reported back
// to the app - we need to check with lockwise about whether they really
// need these failures to be reported or whether we can loosen this.
if let Err(e) = result.result {
return Err(e.into());
}
match result.engine_results.remove("tabs") {
None | Some(Ok(())) => Ok(serde_json::to_string(&result.telemetry)?),
Some(Err(e)) => Err(e.into()),
}
}
}

35
third_party/rust/tabs/src/sync/mod.rs vendored Normal file
View File

@ -0,0 +1,35 @@
/* 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/. */
pub(crate) mod bridge;
pub(crate) mod engine;
mod record;
#[cfg(feature = "full-sync")]
pub mod full_sync;
// When full-sync isn't enabled we need stub versions for these UDL exposed functions.
#[cfg(not(feature = "full-sync"))]
impl crate::TabsStore {
pub fn reset(self: std::sync::Arc<Self>) -> crate::error::Result<()> {
log::error!("reset: feature not enabled");
Err(crate::error::TabsError::SyncAdapterError(
"reset".to_string(),
))
}
pub fn sync(
self: std::sync::Arc<Self>,
_key_id: String,
_access_token: String,
_sync_key: String,
_tokenserver_url: String,
_local_id: String,
) -> crate::error::Result<String> {
log::error!("sync: feature not enabled");
Err(crate::error::TabsError::SyncAdapterError(
"sync".to_string(),
))
}
}

View File

@ -0,0 +1,81 @@
/* 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 serde_derive::{Deserialize, Serialize};
use sync15::Payload;
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct TabsRecordTab {
pub title: String,
pub url_history: Vec<String>,
pub icon: Option<String>,
pub last_used: i64, // Seconds since epoch!
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TabsRecord {
// `String` instead of `SyncGuid` because some IDs are FxA device ID (XXX - that doesn't
// matter though - this could easily be a Guid!)
pub id: String,
pub client_name: String,
pub tabs: Vec<TabsRecordTab>,
#[serde(default)]
pub ttl: u32,
}
impl TabsRecord {
#[inline]
pub fn from_payload(payload: Payload) -> crate::error::Result<Self> {
let record: TabsRecord = payload.into_record()?;
Ok(record)
}
}
#[cfg(test)]
pub mod test {
use super::*;
use serde_json::json;
#[test]
fn test_simple() {
let payload = Payload::from_json(json!({
"id": "JkeBPC50ZI0m",
"clientName": "client name",
"tabs": [{
"title": "the title",
"urlHistory": [
"https://mozilla.org/"
],
"icon": "https://mozilla.org/icon",
"lastUsed": 1643764207
}]
}))
.expect("json is valid");
let record = TabsRecord::from_payload(payload).expect("payload is valid");
assert_eq!(record.id, "JkeBPC50ZI0m");
assert_eq!(record.client_name, "client name");
assert_eq!(record.tabs.len(), 1);
let tab = &record.tabs[0];
assert_eq!(tab.title, "the title");
assert_eq!(tab.icon, Some("https://mozilla.org/icon".to_string()));
assert_eq!(tab.last_used, 1643764207);
}
#[test]
fn test_extra_fields() {
let payload = Payload::from_json(json!({
"id": "JkeBPC50ZI0m",
"clientName": "client name",
"tabs": [],
// Let's say we agree on new tabs to record, we want old versions to
// ignore them!
"recentlyClosed": [],
}))
.expect("json is valid");
let record = TabsRecord::from_payload(payload).expect("payload is valid");
assert_eq!(record.id, "JkeBPC50ZI0m");
}
}

108
third_party/rust/tabs/src/tabs.udl vendored Normal file
View File

@ -0,0 +1,108 @@
[Custom]
typedef string TabsGuid;
namespace tabs {
};
[Error]
enum TabsError {
"SyncAdapterError",
"SyncResetError",
"JsonError",
"MissingLocalIdError",
"UrlParseError",
"SqlError",
"OpenDatabaseError",
};
interface TabsStore {
constructor(string path);
sequence<ClientRemoteTabs> get_all();
void set_local_tabs(sequence<RemoteTabRecord> remote_tabs);
[Self=ByArc]
void register_with_sync_manager();
[Throws=TabsError, Self=ByArc]
void reset();
[Throws=TabsError, Self=ByArc]
string sync(string key_id, string access_token, string sync_key, string tokenserver_url, string local_id);
[Self=ByArc]
TabsBridgedEngine bridged_engine();
};
// Note that this enum is duplicated in fxa-client.udl (although the underlying type *is*
// shared). This duplication exists because there's no direct dependency between that crate and
// this one. We can probably remove the duplication when sync15 gets a .udl file, then we could
// reference it via an `[Extern=...]typedef`
enum TabsDeviceType { "Desktop", "Mobile", "Tablet", "VR", "TV", "Unknown" };
dictionary RemoteTabRecord {
string title;
sequence<string> url_history;
string? icon;
i64 last_used;
};
dictionary ClientRemoteTabs {
string client_id;
string client_name;
TabsDeviceType device_type;
sequence<RemoteTabRecord> remote_tabs;
};
// Note the canonical docs for this are in https://searchfox.org/mozilla-central/source/services/interfaces/mozIBridgedSyncEngine.idl
// It's only actually used in desktop, but it's fine to expose this everywhere.
interface TabsBridgedEngine {
//readonly attribute long storageVersion;
// readonly attribute boolean allowSkippedRecord;
// XXX - better logging story than this?
// attribute mozIServicesLogSink logger;
[Throws=TabsError]
i64 last_sync();
[Throws=TabsError]
void set_last_sync(i64 last_sync);
[Throws=TabsError]
string? sync_id();
[Throws=TabsError]
string reset_sync_id();
[Throws=TabsError]
string ensure_current_sync_id([ByRef]string new_sync_id);
[Throws=TabsError]
void prepare_for_sync([ByRef]string client_data);
[Throws=TabsError]
void sync_started();
[Throws=TabsError]
void store_incoming(sequence<string> incoming_envelopes_as_json);
[Throws=TabsError]
sequence<string> apply();
[Throws=TabsError]
void set_uploaded(i64 new_timestamp, sequence<TabsGuid> uploaded_ids);
[Throws=TabsError]
void sync_finished();
[Throws=TabsError]
void reset();
[Throws=TabsError]
void wipe();
};

8
third_party/rust/tabs/uniffi.toml vendored Normal file
View File

@ -0,0 +1,8 @@
[bindings.kotlin]
package_name = "mozilla.appservices.remotetabs"
cdylib_name = "megazord"
[bindings.swift]
ffi_module_name = "MozillaRustComponents"
ffi_module_filename = "tabsFFI"
generate_module_map = false

View File

@ -107,6 +107,7 @@ mio = "=0.8.0"
[target.'cfg(not(target_os = "android"))'.dependencies]
viaduct = "0.1"
webext_storage_bridge = { path = "../../../components/extensions/storage/webext_storage_bridge" }
tabs = { version = "0.1" }
[target.'cfg(target_os = "windows")'.dependencies]
detect_win32k_conflicts = { path = "../../../xre/detect_win32k_conflicts" }