mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-02-26 20:30:41 +00:00
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:
parent
f9aff9e98e
commit
91b6f8fd65
@ -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
41
Cargo.lock
generated
@ -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",
|
||||
|
11
Cargo.toml
11
Cargo.toml
@ -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.
|
||||
|
@ -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."
|
||||
|
@ -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}
|
6
third_party/rust/error-support/Cargo.toml
vendored
6
third_party/rust/error-support/Cargo.toml
vendored
@ -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 }
|
||||
|
@ -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 {
|
||||
|
2
third_party/rust/sync15/.cargo-checksum.json
vendored
2
third_party/rust/sync15/.cargo-checksum.json
vendored
@ -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}
|
4
third_party/rust/sync15/src/client_types.rs
vendored
4
third_party/rust/sync15/src/client_types.rs
vendored
@ -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,
|
||||
|
@ -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>;
|
||||
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
1
third_party/rust/tabs/.cargo-checksum.json
vendored
Normal file
1
third_party/rust/tabs/.cargo-checksum.json
vendored
Normal 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
45
third_party/rust/tabs/Cargo.toml
vendored
Normal 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
62
third_party/rust/tabs/README.md
vendored
Normal file
@ -0,0 +1,62 @@
|
||||
# Synced Tabs Component
|
||||
|
||||

|
||||
|
||||
## 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
7
third_party/rust/tabs/build.rs
vendored
Normal 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
37
third_party/rust/tabs/src/error.rs
vendored
Normal 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
39
third_party/rust/tabs/src/lib.rs
vendored
Normal 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
70
third_party/rust/tabs/src/schema.rs
vendored
Normal 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
329
third_party/rust/tabs/src/storage.rs
vendored
Normal 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
41
third_party/rust/tabs/src/store.rs
vendored
Normal 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
445
third_party/rust/tabs/src/sync/bridge.rs
vendored
Normal 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
428
third_party/rust/tabs/src/sync/engine.rs
vendored
Normal 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, ®istered));
|
||||
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());
|
||||
}
|
||||
}
|
67
third_party/rust/tabs/src/sync/full_sync.rs
vendored
Normal file
67
third_party/rust/tabs/src/sync/full_sync.rs
vendored
Normal 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
35
third_party/rust/tabs/src/sync/mod.rs
vendored
Normal 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(),
|
||||
))
|
||||
}
|
||||
}
|
81
third_party/rust/tabs/src/sync/record.rs
vendored
Normal file
81
third_party/rust/tabs/src/sync/record.rs
vendored
Normal 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
108
third_party/rust/tabs/src/tabs.udl
vendored
Normal 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
8
third_party/rust/tabs/uniffi.toml
vendored
Normal 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
|
@ -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" }
|
||||
|
Loading…
x
Reference in New Issue
Block a user