/* 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::{ cell::{Ref, RefCell}, convert::TryInto, ffi::OsString, mem, path::PathBuf, str, sync::Arc, }; use golden_gate::{ApplyTask, BridgedEngine, FerryTask}; use moz_task::{self, DispatchOptions, TaskRunnable}; use nserror::{nsresult, NS_OK}; use nsstring::{nsACString, nsCString, nsString}; use thin_vec::ThinVec; use webext_storage::STORAGE_VERSION; use xpcom::{ interfaces::{ mozIBridgedSyncEngineApplyCallback, mozIBridgedSyncEngineCallback, mozIExtensionStorageCallback, mozIServicesLogSink, nsIFile, nsISerialEventTarget, }, RefPtr, }; use crate::error::{Error, Result}; use crate::punt::{Punt, PuntTask, TeardownTask}; use crate::store::{LazyStore, LazyStoreConfig}; fn path_from_nsifile(file: &nsIFile) -> Result { let mut raw_path = nsString::new(); // `nsIFile::GetPath` gives us a UTF-16-encoded version of its // native path, which we must turn back into a platform-native // string. We can't use `nsIFile::nativePath()` here because // it's marked as `nostdcall`, which Rust doesn't support. unsafe { file.GetPath(&mut *raw_path) }.to_result()?; let native_path = { // On Windows, we can create a native string directly from the // encoded path. #[cfg(windows)] { use std::os::windows::prelude::*; OsString::from_wide(&raw_path) } // On other platforms, we must first decode the raw path from // UTF-16, and then create our native string. #[cfg(not(windows))] OsString::from(String::from_utf16(&raw_path)?) }; Ok(native_path.into()) } /// An XPCOM component class for the Rust extension storage API. This class /// implements the interfaces needed for syncing and storage. /// /// This class can be created on any thread, but must not be shared between /// threads. In Rust terms, it's `Send`, but not `Sync`. #[xpcom( implement( mozIExtensionStorageArea, mozIConfigurableExtensionStorageArea, mozISyncedExtensionStorageArea, mozIInterruptible, mozIBridgedSyncEngine ), nonatomic )] pub struct StorageSyncArea { /// A background task queue, used to run all our storage operations on a /// thread pool. Using a serial event target here means that all operations /// will execute sequentially. queue: RefPtr, /// The store is lazily initialized on the task queue the first time it's /// used. store: RefCell>>, } /// `mozIExtensionStorageArea` implementation. impl StorageSyncArea { /// Creates a storage area and its task queue. pub fn new() -> Result> { let queue = moz_task::create_background_task_queue(cstr!("StorageSyncArea"))?; Ok(StorageSyncArea::allocate(InitStorageSyncArea { queue, store: RefCell::new(Some(Arc::default())), })) } /// Returns the store for this area, or an error if it's been torn down. fn store(&self) -> Result>> { let maybe_store = self.store.borrow(); if maybe_store.is_some() { Ok(Ref::map(maybe_store, |s| s.as_ref().unwrap())) } else { Err(Error::AlreadyTornDown) } } /// Dispatches a task for a storage operation to the task queue. fn dispatch(&self, punt: Punt, callback: &mozIExtensionStorageCallback) -> Result<()> { let name = punt.name(); let task = PuntTask::new(Arc::downgrade(&*self.store()?), punt, callback)?; let runnable = TaskRunnable::new(name, Box::new(task))?; // `may_block` schedules the runnable on a dedicated I/O pool. TaskRunnable::dispatch_with_options( runnable, self.queue.coerce(), DispatchOptions::new().may_block(true), )?; Ok(()) } xpcom_method!( configure => Configure( database_file: *const nsIFile, kinto_file: *const nsIFile ) ); /// Sets up the storage area. fn configure(&self, database_file: &nsIFile, kinto_file: &nsIFile) -> Result<()> { self.store()?.configure(LazyStoreConfig { path: path_from_nsifile(database_file)?, kinto_path: path_from_nsifile(kinto_file)?, })?; Ok(()) } xpcom_method!( set => Set( ext_id: *const ::nsstring::nsACString, json: *const ::nsstring::nsACString, callback: *const mozIExtensionStorageCallback ) ); /// Sets one or more key-value pairs. fn set( &self, ext_id: &nsACString, json: &nsACString, callback: &mozIExtensionStorageCallback, ) -> Result<()> { self.dispatch( Punt::Set { ext_id: str::from_utf8(ext_id)?.into(), value: serde_json::from_str(str::from_utf8(json)?)?, }, callback, )?; Ok(()) } xpcom_method!( get => Get( ext_id: *const ::nsstring::nsACString, json: *const ::nsstring::nsACString, callback: *const mozIExtensionStorageCallback ) ); /// Gets values for one or more keys. fn get( &self, ext_id: &nsACString, json: &nsACString, callback: &mozIExtensionStorageCallback, ) -> Result<()> { self.dispatch( Punt::Get { ext_id: str::from_utf8(ext_id)?.into(), keys: serde_json::from_str(str::from_utf8(json)?)?, }, callback, ) } xpcom_method!( remove => Remove( ext_id: *const ::nsstring::nsACString, json: *const ::nsstring::nsACString, callback: *const mozIExtensionStorageCallback ) ); /// Removes one or more keys and their values. fn remove( &self, ext_id: &nsACString, json: &nsACString, callback: &mozIExtensionStorageCallback, ) -> Result<()> { self.dispatch( Punt::Remove { ext_id: str::from_utf8(ext_id)?.into(), keys: serde_json::from_str(str::from_utf8(json)?)?, }, callback, ) } xpcom_method!( clear => Clear( ext_id: *const ::nsstring::nsACString, callback: *const mozIExtensionStorageCallback ) ); /// Removes all keys and values for the specified extension. fn clear(&self, ext_id: &nsACString, callback: &mozIExtensionStorageCallback) -> Result<()> { self.dispatch( Punt::Clear { ext_id: str::from_utf8(ext_id)?.into(), }, callback, ) } xpcom_method!( getBytesInUse => GetBytesInUse( ext_id: *const ::nsstring::nsACString, keys: *const ::nsstring::nsACString, callback: *const mozIExtensionStorageCallback ) ); /// Obtains the count of bytes in use for the specified key or for all keys. fn getBytesInUse( &self, ext_id: &nsACString, keys: &nsACString, callback: &mozIExtensionStorageCallback, ) -> Result<()> { self.dispatch( Punt::GetBytesInUse { ext_id: str::from_utf8(ext_id)?.into(), keys: serde_json::from_str(str::from_utf8(keys)?)?, }, callback, ) } xpcom_method!(teardown => Teardown(callback: *const mozIExtensionStorageCallback)); /// Tears down the storage area, closing the backing database connection. fn teardown(&self, callback: &mozIExtensionStorageCallback) -> Result<()> { // Each storage task holds a `Weak` reference to the store, which it // upgrades to an `Arc` (strong reference) when the task runs on the // background queue. The strong reference is dropped when the task // finishes. When we tear down the storage area, we relinquish our one // owned strong reference to the `TeardownTask`. Because we're using a // task queue, when the `TeardownTask` runs, it should have the only // strong reference to the store, since all other tasks that called // `Weak::upgrade` will have already finished. The `TeardownTask` can // then consume the `Arc` and destroy the store. let mut maybe_store = self.store.borrow_mut(); match mem::take(&mut *maybe_store) { Some(store) => { // Interrupt any currently-running statements. store.interrupt(); // If dispatching the runnable fails, we'll leak the store // without closing its database connection. teardown(&self.queue, store, callback)?; } None => return Err(Error::AlreadyTornDown), } Ok(()) } xpcom_method!(takeMigrationInfo => TakeMigrationInfo(callback: *const mozIExtensionStorageCallback)); /// Fetch-and-delete (e.g. `take`) information about the migration from the /// kinto-based extension-storage to the rust-based storage. fn takeMigrationInfo(&self, callback: &mozIExtensionStorageCallback) -> Result<()> { self.dispatch(Punt::TakeMigrationInfo, callback) } } fn teardown( queue: &nsISerialEventTarget, store: Arc, callback: &mozIExtensionStorageCallback, ) -> Result<()> { let task = TeardownTask::new(store, callback)?; let runnable = TaskRunnable::new(TeardownTask::name(), Box::new(task))?; TaskRunnable::dispatch_with_options( runnable, queue.coerce(), DispatchOptions::new().may_block(true), )?; Ok(()) } /// `mozISyncedExtensionStorageArea` implementation. impl StorageSyncArea { xpcom_method!( fetch_pending_sync_changes => FetchPendingSyncChanges(callback: *const mozIExtensionStorageCallback) ); fn fetch_pending_sync_changes(&self, callback: &mozIExtensionStorageCallback) -> Result<()> { self.dispatch(Punt::FetchPendingSyncChanges, callback) } } /// `mozIInterruptible` implementation. impl StorageSyncArea { xpcom_method!( interrupt => Interrupt() ); /// Interrupts any operations currently running on the background task /// queue. fn interrupt(&self) -> Result<()> { self.store()?.interrupt(); Ok(()) } } /// `mozIBridgedSyncEngine` implementation. impl StorageSyncArea { xpcom_method!(get_logger => GetLogger() -> *const mozIServicesLogSink); fn get_logger(&self) -> Result> { Err(NS_OK)? } xpcom_method!(set_logger => SetLogger(logger: *const mozIServicesLogSink)); fn set_logger(&self, _logger: Option<&mozIServicesLogSink>) -> Result<()> { Ok(()) } xpcom_method!(get_storage_version => GetStorageVersion() -> i32); fn get_storage_version(&self) -> Result { Ok(STORAGE_VERSION.try_into().unwrap()) } // It's possible that migration, or even merging, will result in records // too large for the server. We tolerate that (and hope that the addons do // too :) xpcom_method!(get_allow_skipped_record => GetAllowSkippedRecord() -> bool); fn get_allow_skipped_record(&self) -> Result { Ok(true) } xpcom_method!( get_last_sync => GetLastSync( callback: *const mozIBridgedSyncEngineCallback ) ); fn get_last_sync(&self, callback: &mozIBridgedSyncEngineCallback) -> Result<()> { Ok(FerryTask::for_last_sync(self.new_bridge()?, callback)?.dispatch(&self.queue)?) } xpcom_method!( set_last_sync => SetLastSync( last_sync_millis: i64, callback: *const mozIBridgedSyncEngineCallback ) ); fn set_last_sync( &self, last_sync_millis: i64, callback: &mozIBridgedSyncEngineCallback, ) -> Result<()> { Ok( FerryTask::for_set_last_sync(self.new_bridge()?, last_sync_millis, callback)? .dispatch(&self.queue)?, ) } xpcom_method!( get_sync_id => GetSyncId( callback: *const mozIBridgedSyncEngineCallback ) ); fn get_sync_id(&self, callback: &mozIBridgedSyncEngineCallback) -> Result<()> { Ok(FerryTask::for_sync_id(self.new_bridge()?, callback)?.dispatch(&self.queue)?) } xpcom_method!( reset_sync_id => ResetSyncId( callback: *const mozIBridgedSyncEngineCallback ) ); fn reset_sync_id(&self, callback: &mozIBridgedSyncEngineCallback) -> Result<()> { Ok(FerryTask::for_reset_sync_id(self.new_bridge()?, callback)?.dispatch(&self.queue)?) } xpcom_method!( ensure_current_sync_id => EnsureCurrentSyncId( new_sync_id: *const nsACString, callback: *const mozIBridgedSyncEngineCallback ) ); fn ensure_current_sync_id( &self, new_sync_id: &nsACString, callback: &mozIBridgedSyncEngineCallback, ) -> Result<()> { Ok( FerryTask::for_ensure_current_sync_id(self.new_bridge()?, new_sync_id, callback)? .dispatch(&self.queue)?, ) } xpcom_method!( sync_started => SyncStarted( callback: *const mozIBridgedSyncEngineCallback ) ); fn sync_started(&self, callback: &mozIBridgedSyncEngineCallback) -> Result<()> { Ok(FerryTask::for_sync_started(self.new_bridge()?, callback)?.dispatch(&self.queue)?) } xpcom_method!( store_incoming => StoreIncoming( incoming_envelopes_json: *const ThinVec<::nsstring::nsCString>, callback: *const mozIBridgedSyncEngineCallback ) ); fn store_incoming( &self, incoming_envelopes_json: Option<&ThinVec>, callback: &mozIBridgedSyncEngineCallback, ) -> Result<()> { Ok(FerryTask::for_store_incoming( self.new_bridge()?, incoming_envelopes_json.map(|v| v.as_slice()).unwrap_or(&[]), callback, )? .dispatch(&self.queue)?) } xpcom_method!(apply => Apply(callback: *const mozIBridgedSyncEngineApplyCallback)); fn apply(&self, callback: &mozIBridgedSyncEngineApplyCallback) -> Result<()> { Ok(ApplyTask::new(self.new_bridge()?, callback)?.dispatch(&self.queue)?) } xpcom_method!( set_uploaded => SetUploaded( server_modified_millis: i64, uploaded_ids: *const ThinVec<::nsstring::nsCString>, callback: *const mozIBridgedSyncEngineCallback ) ); fn set_uploaded( &self, server_modified_millis: i64, uploaded_ids: Option<&ThinVec>, callback: &mozIBridgedSyncEngineCallback, ) -> Result<()> { Ok(FerryTask::for_set_uploaded( self.new_bridge()?, server_modified_millis, uploaded_ids.map(|v| v.as_slice()).unwrap_or(&[]), callback, )? .dispatch(&self.queue)?) } xpcom_method!( sync_finished => SyncFinished( callback: *const mozIBridgedSyncEngineCallback ) ); fn sync_finished(&self, callback: &mozIBridgedSyncEngineCallback) -> Result<()> { Ok(FerryTask::for_sync_finished(self.new_bridge()?, callback)?.dispatch(&self.queue)?) } xpcom_method!( reset => Reset( callback: *const mozIBridgedSyncEngineCallback ) ); fn reset(&self, callback: &mozIBridgedSyncEngineCallback) -> Result<()> { Ok(FerryTask::for_reset(self.new_bridge()?, callback)?.dispatch(&self.queue)?) } xpcom_method!( wipe => Wipe( callback: *const mozIBridgedSyncEngineCallback ) ); fn wipe(&self, callback: &mozIBridgedSyncEngineCallback) -> Result<()> { Ok(FerryTask::for_wipe(self.new_bridge()?, callback)?.dispatch(&self.queue)?) } fn new_bridge(&self) -> Result> { Ok(Box::new(self.store()?.get()?.bridged_engine())) } }