Initial implementation of the Kotlin Sync Manager API

This commit is contained in:
Thom Chiovoloni 2019-07-22 19:54:10 -07:00
Родитель ca6a6a90b7
Коммит 9f6fcedc85
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 31F01AEBD799934A
31 изменённых файлов: 1115 добавлений и 18 удалений

Просмотреть файл

@ -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

34
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"

Просмотреть файл

@ -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",

Просмотреть файл

@ -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() {

Просмотреть файл

@ -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<PasswordEngine> = ConcurrentHandleMap::new();
// TODO: this is basically a RwLock<HandleMap<Mutex<Arc<Mutex<...>>>>.
// but could just be a `RwLock<HandleMap<Arc<Mutex<...>>>>`.
// Find a way to express this cleanly in ffi_support?
pub static ref ENGINES: ConcurrentHandleMap<Arc<Mutex<PasswordEngine>>> = 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<String>, 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<url::Url> {
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<String> {
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)
});
}

Просмотреть файл

@ -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)

Просмотреть файл

@ -30,7 +30,7 @@ fn parse_url(url: &str) -> places::Result<url::Url> {
}
lazy_static::lazy_static! {
static ref APIS: ConcurrentHandleMap<Arc<PlacesApi>> = ConcurrentHandleMap::new();
pub static ref APIS: ConcurrentHandleMap<Arc<PlacesApi>> = ConcurrentHandleMap::new();
static ref CONNECTIONS: ConcurrentHandleMap<PlacesDb> = ConcurrentHandleMap::new();
}

Просмотреть файл

@ -0,0 +1,22 @@
[package]
name = "sync_manager"
version = "0.1.0"
authors = ["application-services <application-services@mozilla.com>"]
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"

Просмотреть файл

@ -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()

21
components/sync_manager/android/proguard-rules.pro поставляемый Normal file
Просмотреть файл

@ -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

Просмотреть файл

@ -0,0 +1,2 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.mozilla.appservices.syncmanager" />

Просмотреть файл

@ -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)

Просмотреть файл

@ -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

Просмотреть файл

@ -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")
}
}

Просмотреть файл

@ -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 <U> rustCall(callback: (RustError.ByReference) -> U): U {
val e = RustError.ByReference()
val ret: U = callback(e)
if (e.isFailure()) {
throw e.intoException()
} else {
return ret
}
}

Просмотреть файл

@ -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<String>?,
/**
* 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<String, Boolean>,
/**
* 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()
}
}

Просмотреть файл

@ -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<String, String>,
/**
* The list of engines which synced without any errors.
*/
val successful: List<String>,
/**
* 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<String>?,
/**
* 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
)
}
}
}

Просмотреть файл

@ -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();
}

Просмотреть файл

@ -0,0 +1,19 @@
[package]
name = "sync_manager_ffi"
version = "0.1.0"
authors = ["application-services <application-services@mozilla.com>"]
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"

Просмотреть файл

@ -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);

Просмотреть файл

@ -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),
}
}

Просмотреть файл

@ -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<Error> 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);

Просмотреть файл

@ -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<SyncManager> = Mutex::new(SyncManager::new());
}
#[cfg(feature = "places")]
pub fn set_places(places: Arc<PlacesApi>) {
let mut manager = MANAGER.lock().unwrap();
manager.set_places(places);
}
#[cfg(feature = "logins")]
pub fn set_logins(places: Arc<Mutex<PasswordEngine>>) {
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<msg_types::SyncResult> {
let mut manager = MANAGER.lock().unwrap();
// TODO: translate the protobuf message into something nicer to work with in
// Rust.
manager.sync(params)
}

Просмотреть файл

@ -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<PlacesApi>,
#[cfg(feature = "logins")]
logins: Weak<Mutex<PasswordEngine>>,
}
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<PlacesApi>) {
self.places = Arc::downgrade(&places);
}
#[cfg(feature = "logins")]
pub fn set_logins(&mut self, logins: Arc<Mutex<PasswordEngine>>) {
self.logins = Arc::downgrade(&logins);
}
pub fn disconnect(&mut self) {
unimplemented!();
}
pub fn sync(&mut self, params: SyncParams) -> Result<SyncResult> {
check_engine_list(&params.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(())
}

Просмотреть файл

@ -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<string, bool> 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<string, string> 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;
}

Просмотреть файл

@ -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"] }

Просмотреть файл

@ -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;

Просмотреть файл

@ -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"

Просмотреть файл

@ -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

Просмотреть файл

@ -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"] }

Просмотреть файл

@ -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;