diff --git a/.buildconfig-android.yml b/.buildconfig-android.yml index 8bf1d39d1..a6e0565f3 100644 --- a/.buildconfig-android.yml +++ b/.buildconfig-android.yml @@ -57,6 +57,13 @@ projects: - name: sync15 type: aar description: Shared Sync types for Kotlin. + syncmanager: + path: components/sync_manager/android + artifactId: syncmanager + publications: + - name: syncmanager + type: aar + description: Sync manager implementation lockbox-megazord: uploadSymbols: true path: megazords/lockbox/android diff --git a/Cargo.lock b/Cargo.lock index 3d5a78bf7..a8c207f18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -623,6 +623,7 @@ dependencies = [ "places-ffi 0.1.0", "push-ffi 0.1.0", "rc_log_ffi 0.1.0", + "sync_manager_ffi 0.1.0", "viaduct 0.1.0", ] @@ -1028,6 +1029,7 @@ dependencies = [ "fxaclient_ffi 0.1.0", "logins_ffi 0.1.0", "rc_log_ffi 0.1.0", + "sync_manager_ffi 0.1.0", "viaduct 0.1.0", ] @@ -1107,6 +1109,7 @@ dependencies = [ "places-ffi 0.1.0", "push-ffi 0.1.0", "rc_log_ffi 0.1.0", + "sync_manager_ffi 0.1.0", "viaduct 0.1.0", ] @@ -2351,6 +2354,37 @@ dependencies = [ "viaduct 0.1.0", ] +[[package]] +name = "sync_manager" +version = "0.1.0" +dependencies = [ + "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", + "error-support 0.1.0", + "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "ffi-support 0.3.5", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "logins 0.1.0", + "places 0.1.0", + "prost 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "prost-build 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "prost-derive 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "sync15 0.1.0", +] + +[[package]] +name = "sync_manager_ffi" +version = "0.1.0" +dependencies = [ + "ffi-support 0.3.5", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "logins_ffi 0.1.0", + "places-ffi 0.1.0", + "prost 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "sync15 0.1.0", + "sync_manager 0.1.0", +] + [[package]] name = "synstructure" version = "0.10.2" diff --git a/Cargo.toml b/Cargo.toml index 6650a880a..ea4895c98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,8 @@ members = [ "components/support/rc_crypto/nss/nss_sys", "components/viaduct", "components/sync15", + "components/sync_manager", + "components/sync_manager/ffi", "components/rc_log", "megazords/fenix", "megazords/full", diff --git a/components/logins/android/src/main/java/mozilla/appservices/logins/DatabaseLoginsStorage.kt b/components/logins/android/src/main/java/mozilla/appservices/logins/DatabaseLoginsStorage.kt index 90d0d8a5a..25683b653 100644 --- a/components/logins/android/src/main/java/mozilla/appservices/logins/DatabaseLoginsStorage.kt +++ b/components/logins/android/src/main/java/mozilla/appservices/logins/DatabaseLoginsStorage.kt @@ -29,6 +29,17 @@ class DatabaseLoginsStorage(private val dbPath: String) : AutoCloseable, LoginsS return handle } + /** + * Return the raw handle used to reference this logins database. + * + * Generally should only be used to pass the handle into `SyncManager.setLogins`. + * + * Note: handles do not remain valid after locking / unlocking the logins database. + */ + fun getHandle(): Long { + return this.raw.get() + } + @Synchronized @Throws(LoginsStorageException::class) override fun lock() { diff --git a/components/logins/ffi/src/lib.rs b/components/logins/ffi/src/lib.rs index 6c3e7b3ca..1e61ad141 100644 --- a/components/logins/ffi/src/lib.rs +++ b/components/logins/ffi/src/lib.rs @@ -14,9 +14,13 @@ use ffi_support::{ }; use logins::{Login, PasswordEngine, Result}; use std::os::raw::c_char; +use std::sync::{Arc, Mutex}; lazy_static::lazy_static! { - static ref ENGINES: ConcurrentHandleMap = ConcurrentHandleMap::new(); + // TODO: this is basically a RwLock>>>. + // but could just be a `RwLock>>>`. + // Find a way to express this cleanly in ffi_support? + pub static ref ENGINES: ConcurrentHandleMap>> = ConcurrentHandleMap::new(); } #[no_mangle] @@ -26,10 +30,10 @@ pub extern "C" fn sync15_passwords_state_new( error: &mut ExternError, ) -> u64 { log::debug!("sync15_passwords_state_new"); - ENGINES.insert_with_result(error, || { + ENGINES.insert_with_result(error, || -> logins::Result<_> { let path = db_path.as_str(); let key = encryption_key.as_str(); - PasswordEngine::new(path, Some(key)) + Ok(Arc::new(Mutex::new(PasswordEngine::new(path, Some(key))?))) }) } @@ -65,12 +69,15 @@ pub unsafe extern "C" fn sync15_passwords_state_new_with_hex_key( error: &mut ExternError, ) -> u64 { log::debug!("sync15_passwords_state_new_with_hex_key"); - ENGINES.insert_with_result(error, || { + ENGINES.insert_with_result(error, || -> logins::Result<_> { let path = db_path.as_str(); let key = bytes_to_key_string(encryption_key, encryption_key_len as usize); // We have a Option, but need an Option<&str>... let opt_key_ref = key.as_ref().map(String::as_str); - PasswordEngine::new(path, opt_key_ref) + Ok(Arc::new(Mutex::new(PasswordEngine::new( + path, + opt_key_ref, + )?))) }) } @@ -83,7 +90,7 @@ fn parse_url(url: &str) -> sync15::Result { pub extern "C" fn sync15_passwords_disable_mem_security(handle: u64, error: &mut ExternError) { log::debug!("sync15_passwords_disable_mem_security"); ENGINES.call_with_result(error, handle, |state| -> Result<()> { - state.disable_mem_security() + state.lock().unwrap().disable_mem_security() }) } @@ -98,7 +105,7 @@ pub extern "C" fn sync15_passwords_sync( ) -> *mut c_char { log::debug!("sync15_passwords_sync"); ENGINES.call_with_result(error, handle, |state| -> Result<_> { - let ping = state.sync( + let ping = state.lock().unwrap().sync( &sync15::Sync15StorageClientInit { key_id: key_id.into_string(), access_token: access_token.into_string(), @@ -113,7 +120,9 @@ pub extern "C" fn sync15_passwords_sync( #[no_mangle] pub extern "C" fn sync15_passwords_touch(handle: u64, id: FfiStr<'_>, error: &mut ExternError) { log::debug!("sync15_passwords_touch"); - ENGINES.call_with_result(error, handle, |state| state.touch(id.as_str())) + ENGINES.call_with_result(error, handle, |state| { + state.lock().unwrap().touch(id.as_str()) + }) } #[no_mangle] @@ -123,25 +132,27 @@ pub extern "C" fn sync15_passwords_delete( error: &mut ExternError, ) -> u8 { log::debug!("sync15_passwords_delete"); - ENGINES.call_with_result(error, handle, |state| state.delete(id.as_str())) + ENGINES.call_with_result(error, handle, |state| { + state.lock().unwrap().delete(id.as_str()) + }) } #[no_mangle] pub extern "C" fn sync15_passwords_wipe(handle: u64, error: &mut ExternError) { log::debug!("sync15_passwords_wipe"); - ENGINES.call_with_result(error, handle, |state| state.wipe()) + ENGINES.call_with_result(error, handle, |state| state.lock().unwrap().wipe()) } #[no_mangle] pub extern "C" fn sync15_passwords_wipe_local(handle: u64, error: &mut ExternError) { log::debug!("sync15_passwords_wipe_local"); - ENGINES.call_with_result(error, handle, |state| state.wipe_local()) + ENGINES.call_with_result(error, handle, |state| state.lock().unwrap().wipe_local()) } #[no_mangle] pub extern "C" fn sync15_passwords_reset(handle: u64, error: &mut ExternError) { log::debug!("sync15_passwords_reset"); - ENGINES.call_with_result(error, handle, |state| state.reset()) + ENGINES.call_with_result(error, handle, |state| state.lock().unwrap().reset()) } #[no_mangle] @@ -150,7 +161,9 @@ pub extern "C" fn sync15_passwords_new_interrupt_handle( error: &mut ExternError, ) -> *mut sql_support::SqlInterruptHandle { log::debug!("sync15_passwords_new_interrupt_handle"); - ENGINES.call_with_output(error, handle, |state| state.new_interrupt_handle()) + ENGINES.call_with_output(error, handle, |state| { + state.lock().unwrap().new_interrupt_handle() + }) } #[no_mangle] @@ -166,7 +179,7 @@ pub extern "C" fn sync15_passwords_interrupt( pub extern "C" fn sync15_passwords_get_all(handle: u64, error: &mut ExternError) -> *mut c_char { log::debug!("sync15_passwords_get_all"); ENGINES.call_with_result(error, handle, |state| -> Result { - let all_passwords = state.list()?; + let all_passwords = state.lock().unwrap().list()?; let result = serde_json::to_string(&all_passwords)?; Ok(result) }) @@ -179,7 +192,9 @@ pub extern "C" fn sync15_passwords_get_by_id( error: &mut ExternError, ) -> *mut c_char { log::debug!("sync15_passwords_get_by_id"); - ENGINES.call_with_result(error, handle, |state| state.get(id.as_str())) + ENGINES.call_with_result(error, handle, |state| { + state.lock().unwrap().get(id.as_str()) + }) } #[no_mangle] @@ -196,7 +211,7 @@ pub extern "C" fn sync15_passwords_add( parsed["id"] = serde_json::Value::String(String::default()); } let login: Login = serde_json::from_value(parsed)?; - state.add(login) + state.lock().unwrap().add(login) }) } @@ -222,7 +237,7 @@ pub extern "C" fn sync15_passwords_update( log::debug!("sync15_passwords_update"); ENGINES.call_with_result(error, handle, |state| { let parsed: Login = serde_json::from_str(record_json.as_str())?; - state.update(parsed) + state.lock().unwrap().update(parsed) }); } diff --git a/components/places/android/src/main/java/mozilla/appservices/places/PlacesConnection.kt b/components/places/android/src/main/java/mozilla/appservices/places/PlacesConnection.kt index 84160c8da..713018caa 100644 --- a/components/places/android/src/main/java/mozilla/appservices/places/PlacesConnection.kt +++ b/components/places/android/src/main/java/mozilla/appservices/places/PlacesConnection.kt @@ -45,6 +45,15 @@ class PlacesApi(path: String) : PlacesManager, AutoCloseable { private const val READ_WRITE: Int = 2 } + /** + * Return the raw handle used to reference this PlacesApi. + * + * Generally should only be used to pass the handle into `SyncManager.setPlaces` + */ + fun getHandle(): Long { + return this.handle.get() + } + override fun openReader(): PlacesReaderConnection { val connHandle = rustCall(this) { error -> LibPlacesFFI.INSTANCE.places_connection_new(handle.get(), READ_ONLY, error) diff --git a/components/places/ffi/src/lib.rs b/components/places/ffi/src/lib.rs index 9f1ac1433..9a018a3d4 100644 --- a/components/places/ffi/src/lib.rs +++ b/components/places/ffi/src/lib.rs @@ -30,7 +30,7 @@ fn parse_url(url: &str) -> places::Result { } lazy_static::lazy_static! { - static ref APIS: ConcurrentHandleMap> = ConcurrentHandleMap::new(); + pub static ref APIS: ConcurrentHandleMap> = ConcurrentHandleMap::new(); static ref CONNECTIONS: ConcurrentHandleMap = ConcurrentHandleMap::new(); } diff --git a/components/sync_manager/Cargo.toml b/components/sync_manager/Cargo.toml new file mode 100644 index 000000000..49cbcfbd8 --- /dev/null +++ b/components/sync_manager/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "sync_manager" +version = "0.1.0" +authors = ["application-services "] +edition = "2018" +license = "MPL-2.0" + +[dependencies] +sync15 = { path = "../sync15" } +places = { path = "../places", optional = true } +logins = { path = "../logins", optional = true } +ffi-support = { path = "../support/ffi" } +failure = "0.1.5" +error-support = { path = "../support/error" } +prost = "0.5.0" +prost-derive = "0.5.0" +bytes = "0.4.12" +lazy_static = "1.3.0" +log = "0.4.7" + +[build-dependencies] +prost-build = "0.5.0" diff --git a/components/sync_manager/android/build.gradle b/components/sync_manager/android/build.gradle new file mode 100644 index 000000000..0cc68b1fa --- /dev/null +++ b/components/sync_manager/android/build.gradle @@ -0,0 +1,120 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'com.google.protobuf' + +android { + compileSdkVersion rootProject.ext.build.compileSdkVersion + + defaultConfig { + minSdkVersion rootProject.ext.build['minSdkVersion'] + targetSdkVersion rootProject.ext.build['targetSdkVersion'] + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + buildConfigField("String", "LIBRARY_VERSION", "\"${rootProject.ext.library.version}\"") + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + consumerProguardFiles "$rootDir/proguard-rules-consumer-jna.pro" + } + } + + sourceSets { + test.resources.srcDirs += "$buildDir/rustJniLibs/desktop" + test.resources.srcDirs += "${project(':full-megazord').buildDir}/rustJniLibs/desktop" + + main { + proto { + srcDir '../src' + } + } + } +} + +configurations { + // There's an interaction between Gradle's resolution of dependencies with different types + // (@jar, @aar) for `implementation` and `testImplementation` and with Android Studio's built-in + // JUnit test runner. The runtime classpath in the built-in JUnit test runner gets the + // dependency from the `implementation`, which is type @aar, and therefore the JNA dependency + // doesn't provide the JNI dispatch libraries in the correct Java resource directories. I think + // what's happening is that @aar type in `implementation` resolves to the @jar type in + // `testImplementation`, and that it wins the dependency resolution battle. + // + // A workaround is to add a new configuration which depends on the @jar type and to reference + // the underlying JAR file directly in `testImplementation`. This JAR file doesn't resolve to + // the @aar type in `implementation`. This works when invoked via `gradle`, but also sets the + // correct runtime classpath when invoked with Android Studio's built-in JUnit test runner. + // Success! + jnaForTest +} +protobuf { + protoc { + artifact = 'com.google.protobuf:protoc:3.0.0' + } + plugins { + javalite { + artifact = 'com.google.protobuf:protoc-gen-javalite:3.0.0' + } + } + generateProtoTasks { + all().each { task -> + task.builtins { + remove java + } + task.plugins { + javalite { } + } + } + } +} + +dependencies { + // Part of the public API. + api project(':sync15') + + jnaForTest "net.java.dev.jna:jna:$jna_version@jar" + implementation "net.java.dev.jna:jna:$jna_version@aar" + + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + + implementation 'com.google.protobuf:protobuf-lite:3.0.0' + api project(":full-megazord") + implementation project(":native-support") + + // For reasons unknown, resolving the jnaForTest configuration directly + // trips a nasty issue with the Android-Gradle plugin 3.2.1, like `Cannot + // change attributes of configuration ':PROJECT:kapt' after it has been + // resolved`. I think that the configuration is being made a + // super-configuration of the testImplementation and then the `.files` is + // causing it to be resolved. Cloning first dissociates the configuration, + // avoiding other configurations from being resolved. Tricky! + testImplementation files(configurations.jnaForTest.copyRecursive().files) + testImplementation 'junit:junit:4.12' + testImplementation 'org.robolectric:robolectric:3.8' + testImplementation 'org.mockito:mockito-core:2.21.0' + + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' +} + +afterEvaluate { + // The `cargoBuild` task isn't available until after evaluation. + android.libraryVariants.all { variant -> + def productFlavor = "" + variant.productFlavors.each { + productFlavor += "${it.name.capitalize()}" + } + def buildType = "${variant.buildType.name.capitalize()}" + tasks["generate${productFlavor}${buildType}Assets"].dependsOn(project(':full-megazord').tasks["cargoBuild"]) + + // For unit tests. + tasks["process${productFlavor}${buildType}UnitTestJavaRes"].dependsOn(project(':full-megazord').tasks["cargoBuild"]) + } +} + +apply from: "$rootDir/publish.gradle" + +ext.configurePublish() diff --git a/components/sync_manager/android/proguard-rules.pro b/components/sync_manager/android/proguard-rules.pro new file mode 100644 index 000000000..f1b424510 --- /dev/null +++ b/components/sync_manager/android/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/components/sync_manager/android/src/main/AndroidManifest.xml b/components/sync_manager/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000..1092153a7 --- /dev/null +++ b/components/sync_manager/android/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/components/sync_manager/android/src/main/java/mozilla/appservices/syncmanager/Errors.kt b/components/sync_manager/android/src/main/java/mozilla/appservices/syncmanager/Errors.kt new file mode 100644 index 000000000..c2763af81 --- /dev/null +++ b/components/sync_manager/android/src/main/java/mozilla/appservices/syncmanager/Errors.kt @@ -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/. */ + +package mozilla.appservices.syncmanager + +/** + * Base class for sync manager errors. Generally should not + * have concrete instances. + */ +open class SyncManagerException(msg: String) : Exception(msg) + +/** + * General catch-all error, generally indicating API misuse of some sort. + */ +open class UnexpectedError(msg: String) : SyncManagerException(msg) + +/** + * The sync manager paniced. Please report these. + */ +open class InternalPanic(msg: String) : SyncManagerException(msg) + +/** + * We were asked to sync an engine which is either unknown, or which the sync + * manager was not compiled with support for (message will elaborate). + */ +open class UnsupportedEngine(msg: String) : SyncManagerException(msg) + +/** + * We were asked to sync an engine but we couldn't because the connection + * + * Note: When not syncing, the manager holds a weak reference to connection + * objects, and so performing something like: `SyncManager.setLogins(handle)`, + * closing/locking the logins connection, and then trying to sync logins will + * produce this error. + * + * TODO: Should this be an error reported in SyncResult and not cause the sync + * to fail? It's probably a bug in the caller (they should call `setBlah` first)... + */ +open class ClosedEngine(msg: String) : SyncManagerException(msg) + diff --git a/components/sync_manager/android/src/main/java/mozilla/appservices/syncmanager/LibSyncManagerFFI.kt b/components/sync_manager/android/src/main/java/mozilla/appservices/syncmanager/LibSyncManagerFFI.kt new file mode 100644 index 000000000..0a7cec834 --- /dev/null +++ b/components/sync_manager/android/src/main/java/mozilla/appservices/syncmanager/LibSyncManagerFFI.kt @@ -0,0 +1,32 @@ +/* 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/. */ + +package mozilla.appservices.syncmanager + +import com.sun.jna.Library +import com.sun.jna.Pointer +import com.sun.jna.PointerType +import com.sun.jna.StringArray +import mozilla.appservices.support.native.RustBuffer +import mozilla.appservices.support.native.loadIndirect +import org.mozilla.appservices.syncmanager.BuildConfig + +@Suppress("FunctionNaming", "FunctionParameterNaming", "LongParameterList", "TooGenericExceptionThrown") +internal interface LibSyncManagerFFI : Library { + companion object { + internal var INSTANCE: LibSyncManagerFFI = + loadIndirect(componentName = "syncmanager", componentVersion = BuildConfig.LIBRARY_VERSION) + } + fun sync_manager_set_places(handle: PlacesApiHandle, error: RustError.ByReference) + fun sync_manager_set_logins(handle: LoginsDbHandle, error: RustError.ByReference) + fun sync_manager_disconnect(error: RustError.ByReference) + + fun sync_manager_sync(data: Pointer, len: Int, error: RustError.ByReference): RustBuffer.ByValue + + fun sync_manager_destroy_string(s: Pointer) + fun sync_manager_destroy_bytebuffer(bb: RustBuffer.ByValue) +} + +internal typealias PlacesApiHandle = Long +internal typealias LoginsDbHandle = Long diff --git a/components/sync_manager/android/src/main/java/mozilla/appservices/syncmanager/RustError.kt b/components/sync_manager/android/src/main/java/mozilla/appservices/syncmanager/RustError.kt new file mode 100644 index 000000000..b039a7130 --- /dev/null +++ b/components/sync_manager/android/src/main/java/mozilla/appservices/syncmanager/RustError.kt @@ -0,0 +1,69 @@ +/* Copyright 2018 Mozilla + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ +package mozilla.appservices.syncmanager + +import com.sun.jna.Pointer +import com.sun.jna.Structure + +@Structure.FieldOrder("code", "message") +internal open class RustError : Structure() { + + class ByReference : RustError(), Structure.ByReference + + @JvmField var code: Int = 0 + @JvmField var message: Pointer? = null + + fun isSuccess(): Boolean { + return code == 0 + } + + fun isFailure(): Boolean { + return code != 0 + } + + @Suppress("ComplexMethod", "ReturnCount", "TooGenericExceptionThrown") + fun intoException(): SyncManagerException { + if (!isFailure()) { + // It's probably a bad idea to throw here! We're probably leaking something if this is + // ever hit! (But we shouldn't ever hit it?) + throw RuntimeException("[Bug] intoException called on non-failure!") + } + val message = this.consumeErrorMessage() + when (code) { + 2 -> return UnsupportedEngine(message) + 3 -> return ClosedEngine(message) + -1 -> return InternalPanic(message) + // Note: `1` is used as a generic catch all, but we + // might as well handle the others the same way. + else -> return UnexpectedError(message) + } + } + + /** + * Get and consume the error message, or null if there is none. + */ + fun consumeErrorMessage(): String { + val result = this.getMessage() + if (this.message != null) { + LibSyncManagerFFI.INSTANCE.sync_manager_destroy_string(this.message!!) + this.message = null + } + if (result == null) { + throw NullPointerException("consumeErrorMessage called with null message!") + } + return result + } + + /** + * Get the error message or null if there is none. + */ + fun getMessage(): String? { + return this.message?.getString(0, "utf8") + } +} diff --git a/components/sync_manager/android/src/main/java/mozilla/appservices/syncmanager/SyncManager.kt b/components/sync_manager/android/src/main/java/mozilla/appservices/syncmanager/SyncManager.kt new file mode 100644 index 000000000..8f07d1d1f --- /dev/null +++ b/components/sync_manager/android/src/main/java/mozilla/appservices/syncmanager/SyncManager.kt @@ -0,0 +1,72 @@ +/* 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/. */ + +package mozilla.appservices.syncmanager + +import com.sun.jna.Native +import mozilla.appservices.support.native.toNioDirectBuffer + +object SyncManager { + + /** + * Point the manager at the implementation of `PlacesApi` to use. + * + * @param placesApiHandle A value returned by `PlacesApi.getHandle()` + * @throws [UnsupportedEngine] If the manager was not compiled with places support. + */ + fun setPlaces(placesApiHandle: Long) { + rustCall { err -> + LibSyncManagerFFI.INSTANCE.sync_manager_set_places(placesApiHandle, err) + } + } + /** + * Point the manager at the implementation of `DatabaseLoginsStorage` to use. + * + * @param loginsDbHandle A value returned by `DatabaseLoginsStorage.getHandle()` + * @throws [UnsupportedEngine] If the manager was not compiled with logins support. + */ + fun setLogins(loginsDbHandle: Long) { + rustCall { err -> + LibSyncManagerFFI.INSTANCE.sync_manager_set_logins(loginsDbHandle, err) + } + } + + /** + * Disconnect this device from sync. This essentially clears shared state having to do with + * sync, as well as each engines sync-specific local state + */ + fun disconnect() { + rustCall { err -> + LibSyncManagerFFI.INSTANCE.sync_manager_disconnect(err) + } + } + /** + * Perform a sync. + */ + fun sync(params: SyncParams): SyncResult { + val buf = params.toProtobuf() + val (nioBuf, len) = buf.toNioDirectBuffer() + val rustBuf = rustCall { err -> + val ptr = Native.getDirectBufferPointer(nioBuf) + LibSyncManagerFFI.INSTANCE.sync_manager_sync(ptr, len, err) + } + + try { + val stream = rustBuf.asCodedInputStream() + return SyncResult.fromProtobuf(MsgTypes.SyncResult.parseFrom(stream)) + } finally { + LibSyncManagerFFI.INSTANCE.sync_manager_destroy_bytebuffer(rustBuf) + } + } +} + +internal inline fun rustCall(callback: (RustError.ByReference) -> U): U { + val e = RustError.ByReference() + val ret: U = callback(e) + if (e.isFailure()) { + throw e.intoException() + } else { + return ret + } +} diff --git a/components/sync_manager/android/src/main/java/mozilla/appservices/syncmanager/SyncParams.kt b/components/sync_manager/android/src/main/java/mozilla/appservices/syncmanager/SyncParams.kt new file mode 100644 index 000000000..d905b6a4a --- /dev/null +++ b/components/sync_manager/android/src/main/java/mozilla/appservices/syncmanager/SyncParams.kt @@ -0,0 +1,119 @@ +/* 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/. */ + +package mozilla.appservices.syncmanager + +/** + * Reason for syncing. + */ +enum class SyncReason { + /** + * This is a scheduled sync + */ + SCHEDULED, + /** + * This is a manually triggered sync invoked by the user. + */ + USER, + /** + * This is a sync that is running optimistically before + * the device goes to sleep / is backgrounded. + */ + PRE_SLEEP, + /** + * This is a sync that is run on application startup. + */ + STARTUP, + /** + * This is a sync that is being performed simply to update the + * enabled state of one or more engines. + */ + ENABLED_CHANGE, +} + +/** + * A class for providing the auth-related information needed to sync. + */ +data class SyncAuthInfo( + val kid: String, + val fxaAccessToken: String, + val syncKey: String, + val tokenserverURL: String +) + +/** + * Parameters to use for syncing. + */ +data class SyncParams( + /** + * The reason we're syncing. + */ + val reason: SyncReason, + /** + * The list of engines to sync. + * + * Engine names are lowercase, and refer to the server-side engine name, e.g. + * "passwords" (not "logins"!), "bookmarks", "history", etc. + * + * Requesting that we sync an unknown engine type will result in a + * [UnsupportedEngine] error. + * + * Passing `null` here is used to indicate that all known engines + * should be synced. + */ + val engines: List?, + + /** + * A map of engine name to new-enabled-state. That is, + * + * - The map should be empty to indicate "no changes" + * + * - The map should have `enginename: true` if an engine named + * `enginename` should be enabled. + * + * - The map should have `enginename: false` if an engine named + * `enginename` should be disabled. + */ + val enabledChanges: Map, + + /** + * The information used to authenticate with the sync server. + */ + val authInfo: SyncAuthInfo, + + /** + * The previously persisted sync state (from `SyncResult.persistedState`), + * if any exists. + */ + val persistedState: String? +) { + internal fun toProtobuf(): MsgTypes.SyncParams { + val builder = MsgTypes.SyncParams.newBuilder() + + this.engines?.let { + builder.addAllEnginesToSync(it) + builder.syncAllEngines = false + } ?: run { + // Null `engines`, sync everything. + builder.syncAllEngines = true + } + + builder.reason = when (this.reason) { + SyncReason.SCHEDULED -> MsgTypes.SyncReason.SCHEDULED + SyncReason.USER -> MsgTypes.SyncReason.USER + SyncReason.PRE_SLEEP -> MsgTypes.SyncReason.PRE_SLEEP + SyncReason.STARTUP -> MsgTypes.SyncReason.STARTUP + SyncReason.ENABLED_CHANGE -> MsgTypes.SyncReason.ENABLED_CHANGE + } + + builder.putAllEnginesToChangeState(this.enabledChanges) + + builder.acctAccessToken = this.authInfo.fxaAccessToken + builder.acctSyncKey = this.authInfo.syncKey + builder.acctKeyId = this.authInfo.kid + builder.acctTokenserverUrl = this.authInfo.tokenserverURL + + return builder.build() + } +} diff --git a/components/sync_manager/android/src/main/java/mozilla/appservices/syncmanager/SyncResult.kt b/components/sync_manager/android/src/main/java/mozilla/appservices/syncmanager/SyncResult.kt new file mode 100644 index 000000000..f6791a7aa --- /dev/null +++ b/components/sync_manager/android/src/main/java/mozilla/appservices/syncmanager/SyncResult.kt @@ -0,0 +1,150 @@ +/* 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/. */ + +package mozilla.appservices.syncmanager + +import mozilla.appservices.sync15.SyncTelemetryPing + +/** + * Indicates, at a high level whether the sync succeeded or failed. + */ +enum class SyncServiceStatus { + /** + * The sync did not fail. + */ + OK, + + /** + * The sync failed due to network problems. + */ + NETWORK_ERROR, + + /** + * The sync failed due to some apparent error with the servers. + */ + SERVICE_ERROR, + + /** + * The auth information we were provided was somehow invalid. Refreshing it + * with FxA may resolve this issue. + */ + AUTH_ERROR, + + /** + * Indicates that we declined to sync because the server had requested a + * backoff which has not yet expired. + */ + BACKED_OFF, + + /** + * Some other error occurred. + */ + OTHER_ERROR, +} + +/** + * The result of a sync. + */ +data class SyncResult( + /** + * The general health. + */ + val status: SyncServiceStatus, + + /** + * For engines which failed to sync, contains a string + * description of the error. + * + * The error strings are mostly provided for local debugging, + * and more robust information is present in e.g. telemetry. + */ + val failures: Map, + + /** + * The list of engines which synced without any errors. + */ + val successful: List, + + /** + * The state string that should be persisted by the caller, and + * used as the value for `SyncParams.persistedState` in subsequent + * calls to `SyncManager.sync`. + */ + val persistedState: String, + + /** + * The list of engines which have been declined by the user. + * + * Null if we didn't make it far enough to know. + */ + val declined: List?, + + /** + * The next time we're allowed to sync, in milliseconds since + * the unix epoch. + * + * If this value is in the future, then there's some kind of back-off. + * Note that it's not necessary for the app to enforce this, but should + * probably be used as an input in the application's sync scheduling logic. + * + * Syncs before this passes will generally fail with a BACKED_OFF error, + * unless they are syncs that were manually requested by the user (that + * is, they have the reason `SyncReason.USER`). + */ + val nextSyncAllowedAt: Long?, + + /** + * A bundle of telemetry information recorded during this sync. + */ + val telemetry: SyncTelemetryPing? +) { + companion object { + internal fun fromProtobuf(pb: MsgTypes.SyncResult): SyncResult { + val nextSyncAllowedAt = if (pb.hasNextSyncAllowedAt()) { + pb.nextSyncAllowedAt + } else { + null + } + + val declined = if (pb.haveDeclined) { + pb.declinedList + } else { + null + } + + val successful = pb.resultsMap.entries + .filter { it.value.isEmpty() } + .map { it.key } + .toList() + + val failures = pb.resultsMap.filter { it.value.isNotEmpty() } + + val telemetry = if (pb.hasTelemetryJson()) { + SyncTelemetryPing.fromJSONString(pb.telemetryJson) + } else { + null + } + + val status = when (pb.status) { + MsgTypes.ServiceStatus.OK -> SyncServiceStatus.OK + MsgTypes.ServiceStatus.NETWORK_ERROR -> SyncServiceStatus.NETWORK_ERROR + MsgTypes.ServiceStatus.SERVICE_ERROR -> SyncServiceStatus.SERVICE_ERROR + MsgTypes.ServiceStatus.AUTH_ERROR -> SyncServiceStatus.AUTH_ERROR + MsgTypes.ServiceStatus.BACKED_OFF -> SyncServiceStatus.BACKED_OFF + MsgTypes.ServiceStatus.OTHER_ERROR -> SyncServiceStatus.OTHER_ERROR + else -> SyncServiceStatus.OTHER_ERROR // impossible *sigh* + } + + return SyncResult( + status = status, + failures = failures, + successful = successful, + declined = declined, + telemetry = telemetry, + nextSyncAllowedAt = nextSyncAllowedAt, + persistedState = pb.persistedState + ) + } + } +} diff --git a/components/sync_manager/build.rs b/components/sync_manager/build.rs new file mode 100644 index 000000000..43d878d79 --- /dev/null +++ b/components/sync_manager/build.rs @@ -0,0 +1,8 @@ +/* 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() { + println!("cargo:rerun-if-changed=src/manager_msg_types.proto"); + prost_build::compile_protos(&["src/manager_msg_types.proto"], &["src/"]).unwrap(); +} diff --git a/components/sync_manager/ffi/Cargo.toml b/components/sync_manager/ffi/Cargo.toml new file mode 100644 index 000000000..8b3bf8f9b --- /dev/null +++ b/components/sync_manager/ffi/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "sync_manager_ffi" +version = "0.1.0" +authors = ["application-services "] +edition = "2018" +license = "MPL-2.0" + +[features] +logins = ["sync_manager/logins", "logins_ffi"] +places = ["sync_manager/places", "places-ffi"] + +[dependencies] +sync_manager = { path = ".." } +sync15 = { path = "../../sync15" } +ffi-support = { path = "../../support/ffi" } +places-ffi = { path = "../../places/ffi", optional = true } +logins_ffi = { path = "../../logins/ffi", optional = true } +prost = "0.5.0" +log = "0.4.7" diff --git a/components/sync_manager/ffi/src/lib.rs b/components/sync_manager/ffi/src/lib.rs new file mode 100644 index 000000000..c38537b8d --- /dev/null +++ b/components/sync_manager/ffi/src/lib.rs @@ -0,0 +1,84 @@ +/* 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)] +// Let's allow these in the FFI code, since it's usually just a coincidence if +// the closure is small. +#![allow(clippy::redundant_closure)] + +use ffi_support::{ExternError, HandleError}; +use sync_manager::Result as MgrResult; + +#[no_mangle] +pub extern "C" fn sync_manager_set_places(_places_api_handle: u64, error: &mut ExternError) { + ffi_support::call_with_result(error, || -> MgrResult<()> { + #[cfg(feature = "places")] + { + let api = places_ffi::APIS + .get_u64(_places_api_handle, |api| -> Result<_, HandleError> { + Ok(std::sync::Arc::clone(api)) + })?; + sync_manager::set_places(api); + Ok(()) + } + #[cfg(not(feature = "places"))] + { + log::error!("Sync manager not compiled with places support"); + Err(sync_manager::ErrorKind::UnsupportedFeature("places".to_string()).into()) + } + }) +} + +#[no_mangle] +pub extern "C" fn sync_manager_set_logins(_logins_handle: u64, error: &mut ExternError) { + ffi_support::call_with_result(error, || -> MgrResult<()> { + #[cfg(feature = "logins")] + { + let api = logins_ffi::ENGINES + .get_u64(_logins_handle, |api| -> Result<_, HandleError> { + Ok(std::sync::Arc::clone(api)) + })?; + sync_manager::set_logins(api); + Ok(()) + } + #[cfg(not(feature = "logins"))] + { + log::error!("Sync manager not compiled with logins support"); + Err(sync_manager::ErrorKind::UnsupportedFeature("logins".to_string()).into()) + } + }) +} + +#[no_mangle] +pub extern "C" fn sync_manager_disconnect(error: &mut ExternError) { + ffi_support::call_with_output(error, sync_manager::disconnect); +} + +unsafe fn get_buffer<'a>(data: *const u8, len: i32) -> &'a [u8] { + assert!(len >= 0, "Bad buffer len: {}", len); + if len == 0 { + // This will still fail, but as a bad protobuf format. + &[] + } else { + assert!(!data.is_null(), "Unexpected null data pointer"); + std::slice::from_raw_parts(data, len as usize) + } +} + +#[no_mangle] +pub unsafe extern "C" fn sync_manager_sync( + params_data: *const u8, + params_len: i32, + error: &mut ExternError, +) -> ffi_support::ByteBuffer { + ffi_support::call_with_result(error, || { + let buffer = get_buffer(params_data, params_len); + let params: sync_manager::msg_types::SyncParams = prost::Message::decode(buffer)?; + sync_manager::sync(params) + }) +} + +ffi_support::define_string_destructor!(sync_manager_destroy_string); +ffi_support::define_bytebuffer_destructor!(sync_manager_destroy_bytebuffer); diff --git a/components/sync_manager/src/error.rs b/components/sync_manager/src/error.rs new file mode 100644 index 000000000..9326b398f --- /dev/null +++ b/components/sync_manager/src/error.rs @@ -0,0 +1,25 @@ +/* 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 failure::Fail; + +#[derive(Debug, Fail)] +pub enum ErrorKind { + #[fail(display = "Unknown engine: {}", _0)] + UnknownEngine(String), + #[fail(display = "Manager was compiled without support for {:?}", _0)] + UnsupportedFeature(String), + #[fail(display = "Database connection for '{}' is not open", _0)] + ConnectionClosed(String), + #[fail(display = "Handle is invalid: {}", _0)] + InvalidHandle(#[fail(cause)] ffi_support::HandleError), + #[fail(display = "Protobuf decode error: {}", _0)] + ProtobufDecodeError(#[fail(cause)] prost::DecodeError), +} + +error_support::define_error! { + ErrorKind { + (InvalidHandle, ffi_support::HandleError), + (ProtobufDecodeError, prost::DecodeError), + } +} diff --git a/components/sync_manager/src/ffi.rs b/components/sync_manager/src/ffi.rs new file mode 100644 index 000000000..1a2270030 --- /dev/null +++ b/components/sync_manager/src/ffi.rs @@ -0,0 +1,49 @@ +/* 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::{Error, ErrorKind}; +use ffi_support::{ErrorCode, ExternError}; + +pub mod error_codes { + // Note: 0 (success) and -1 (panic) are reserved by ffi_support + pub const UNEXPECTED: i32 = 1; + + /// We were asked to sync an engine, but we either don't know what it is, + /// or were compiled without support for it. + pub const UNSUPPORTED_ENGINE: i32 = 2; + + /// We were asked to sync an engine which is not open (e.g. Weak::upgrade + /// returns None). + pub const ENGINE_NOT_OPEN: i32 = 3; +} + +fn get_code(err: &Error) -> ErrorCode { + match err.kind() { + ErrorKind::UnknownEngine(e) => { + log::error!("Unknown engine: {}", e); + ErrorCode::new(error_codes::UNSUPPORTED_ENGINE) + } + ErrorKind::UnsupportedFeature(f) => { + log::error!("Unsupported feature: {}", f); + ErrorCode::new(error_codes::UNSUPPORTED_ENGINE) + } + ErrorKind::ConnectionClosed(e) => { + log::error!("Connection closed: {}", e); + ErrorCode::new(error_codes::ENGINE_NOT_OPEN) + } + err => { + log::error!("Unexpected error: {}", err); + ErrorCode::new(error_codes::UNEXPECTED) + } + } +} + +impl From for ExternError { + fn from(e: Error) -> ExternError { + ExternError::new_error(get_code(&e), e.to_string()) + } +} + +ffi_support::implement_into_ffi_by_protobuf!(crate::msg_types::SyncResult); +ffi_support::implement_into_ffi_by_protobuf!(crate::msg_types::SyncParams); diff --git a/components/sync_manager/src/lib.rs b/components/sync_manager/src/lib.rs new file mode 100644 index 000000000..1ef5381ee --- /dev/null +++ b/components/sync_manager/src/lib.rs @@ -0,0 +1,53 @@ +/* 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)] + +pub mod error; +mod ffi; +mod manager; + +pub use error::{Error, ErrorKind, Result}; + +pub mod msg_types { + include!(concat!(env!("OUT_DIR"), "/msg_types.rs")); +} + +#[cfg(feature = "logins")] +use logins::PasswordEngine; +use manager::SyncManager; +#[cfg(feature = "places")] +use places::PlacesApi; +#[cfg(any(feature = "places", feature = "logins"))] +use std::sync::Arc; +use std::sync::Mutex; + +lazy_static::lazy_static! { + static ref MANAGER: Mutex = Mutex::new(SyncManager::new()); +} + +#[cfg(feature = "places")] +pub fn set_places(places: Arc) { + let mut manager = MANAGER.lock().unwrap(); + manager.set_places(places); +} + +#[cfg(feature = "logins")] +pub fn set_logins(places: Arc>) { + let mut manager = MANAGER.lock().unwrap(); + manager.set_logins(places); +} + +pub fn disconnect() { + let mut manager = MANAGER.lock().unwrap(); + manager.disconnect(); +} + +pub fn sync(params: msg_types::SyncParams) -> Result { + let mut manager = MANAGER.lock().unwrap(); + // TODO: translate the protobuf message into something nicer to work with in + // Rust. + manager.sync(params) +} diff --git a/components/sync_manager/src/manager.rs b/components/sync_manager/src/manager.rs new file mode 100644 index 000000000..75a8cf601 --- /dev/null +++ b/components/sync_manager/src/manager.rs @@ -0,0 +1,71 @@ +/* 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/. */ + +#[cfg(feature = "places")] +use places::PlacesApi; +// use sql_support::SqlInterruptHandle; +use crate::error::*; +use crate::msg_types::{SyncParams, SyncResult}; +#[cfg(feature = "logins")] +use logins::PasswordEngine; +#[cfg(feature = "logins")] +use std::sync::Mutex; +#[cfg(any(feature = "places", feature = "logins"))] +use std::sync::{Arc, Weak}; + +pub struct SyncManager { + #[cfg(feature = "places")] + places: Weak, + #[cfg(feature = "logins")] + logins: Weak>, +} + +impl SyncManager { + pub fn new() -> Self { + Self { + #[cfg(feature = "places")] + places: Weak::new(), + #[cfg(feature = "logins")] + logins: Weak::new(), + } + } + + #[cfg(feature = "places")] + pub fn set_places(&mut self, places: Arc) { + self.places = Arc::downgrade(&places); + } + + #[cfg(feature = "logins")] + pub fn set_logins(&mut self, logins: Arc>) { + self.logins = Arc::downgrade(&logins); + } + + pub fn disconnect(&mut self) { + unimplemented!(); + } + + pub fn sync(&mut self, params: SyncParams) -> Result { + check_engine_list(¶ms.engines_to_sync)?; + unimplemented!(); + } +} + +fn check_engine_list(list: &[String]) -> Result<()> { + for e in list { + if e == "bookmarks" || e == "history" { + if cfg!(not(feature = "places")) { + return Err(ErrorKind::UnsupportedFeature(e.to_string()).into()); + } + continue; + } + if e == "passwords" { + if cfg!(not(feature = "logins")) { + return Err(ErrorKind::UnsupportedFeature(e.to_string()).into()); + } + continue; + } + return Err(ErrorKind::UnknownEngine(e.to_string()).into()); + } + Ok(()) +} diff --git a/components/sync_manager/src/manager_msg_types.proto b/components/sync_manager/src/manager_msg_types.proto new file mode 100644 index 000000000..6a9e1e09a --- /dev/null +++ b/components/sync_manager/src/manager_msg_types.proto @@ -0,0 +1,56 @@ +syntax = "proto2"; + +// Note: this file name must be unique due to how the iOS megazord works :( + +package msg_types; + +option java_package = "mozilla.appservices.syncmanager"; +option java_outer_classname = "MsgTypes"; + +enum SyncReason { + SCHEDULED = 1; + USER = 2; + PRE_SLEEP = 3; + STARTUP = 4; + ENABLED_CHANGE = 5; +} + +message SyncParams { + repeated string engines_to_sync = 1; + required bool sync_all_engines = 2; + + required SyncReason reason = 3; + + map engines_to_change_state = 4; + + optional string persisted_state = 5; + + // These conceptually are a nested type, but exposing them as such would add + // needless complexity to the FFI. + required string acct_key_id = 6; + required string acct_access_token = 7; + required string acct_tokenserver_url = 8; + required string acct_sync_key = 9; +} + +enum ServiceStatus { + OK = 1; + NETWORK_ERROR = 2; + SERVICE_ERROR = 3; + AUTH_ERROR = 4; + BACKED_OFF = 5; + OTHER_ERROR = 6; +} + +message SyncResult { + required ServiceStatus status = 1; + map results = 2; // empty string used for 'no error' + + repeated string declined = 3; + // false if we didn't manage to check declined. + required bool have_declined = 4; + + optional int64 next_sync_allowed_at = 5; + required string persisted_state = 6; + optional string telemetry_json = 7; +} diff --git a/megazords/fenix/Cargo.toml b/megazords/fenix/Cargo.toml index 6fa529617..56c2c6c29 100644 --- a/megazords/fenix/Cargo.toml +++ b/megazords/fenix/Cargo.toml @@ -15,3 +15,4 @@ places-ffi = { path = "../../components/places/ffi" } push-ffi = { path = "../../components/push/ffi" } rc_log_ffi = { path = "../../components/rc_log" } viaduct = { path = "../../components/viaduct", default-features = false } +sync_manager_ffi = { path = "../../components/sync_manager/ffi", features = ["places"] } diff --git a/megazords/fenix/src/lib.rs b/megazords/fenix/src/lib.rs index 4232613b8..9d4f9dca7 100644 --- a/megazords/fenix/src/lib.rs +++ b/megazords/fenix/src/lib.rs @@ -10,4 +10,5 @@ pub use logins_ffi; pub use places_ffi; pub use push_ffi; pub use rc_log_ffi; +pub use sync_manager_ffi; pub use viaduct; diff --git a/megazords/full/Cargo.toml b/megazords/full/Cargo.toml index c666f1420..b037fe634 100644 --- a/megazords/full/Cargo.toml +++ b/megazords/full/Cargo.toml @@ -15,4 +15,5 @@ places-ffi = { path = "../../components/places/ffi" } push-ffi = { path = "../../components/push/ffi" } rc_log_ffi = { path = "../../components/rc_log" } viaduct = { path = "../../components/viaduct", default_features = false } +sync_manager_ffi = { path = "../../components/sync_manager/ffi", features = ["logins", "places"] } lazy_static = "1.4.0" diff --git a/megazords/full/src/lib.rs b/megazords/full/src/lib.rs index a39517824..3943d4202 100644 --- a/megazords/full/src/lib.rs +++ b/megazords/full/src/lib.rs @@ -13,6 +13,7 @@ pub use logins_ffi; pub use places_ffi; pub use push_ffi; pub use rc_log_ffi; +pub use sync_manager_ffi; pub use viaduct; /// In order to support the use case of consumers who don't know about megazords diff --git a/megazords/lockbox/Cargo.toml b/megazords/lockbox/Cargo.toml index 27f8a0fce..7e6f91010 100644 --- a/megazords/lockbox/Cargo.toml +++ b/megazords/lockbox/Cargo.toml @@ -13,3 +13,4 @@ fxaclient_ffi = { path = "../../components/fxa-client/ffi" } logins_ffi = { path = "../../components/logins/ffi" } rc_log_ffi = { path = "../../components/rc_log" } viaduct = { path = "../../components/viaduct", default-features = false } +sync_manager_ffi = { path = "../../components/sync_manager/ffi", features = ["logins"] } diff --git a/megazords/lockbox/src/lib.rs b/megazords/lockbox/src/lib.rs index 51afae50b..7f267c96e 100644 --- a/megazords/lockbox/src/lib.rs +++ b/megazords/lockbox/src/lib.rs @@ -8,4 +8,5 @@ pub use fxaclient_ffi; pub use logins_ffi; pub use rc_log_ffi; +pub use sync_manager_ffi; pub use viaduct;