diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /intl/l10n/rust/l10nregistry-ffi | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'intl/l10n/rust/l10nregistry-ffi')
-rw-r--r-- | intl/l10n/rust/l10nregistry-ffi/Cargo.toml | 24 | ||||
-rw-r--r-- | intl/l10n/rust/l10nregistry-ffi/cbindgen.toml | 26 | ||||
-rw-r--r-- | intl/l10n/rust/l10nregistry-ffi/src/env.rs | 132 | ||||
-rw-r--r-- | intl/l10n/rust/l10nregistry-ffi/src/fetcher.rs | 70 | ||||
-rw-r--r-- | intl/l10n/rust/l10nregistry-ffi/src/lib.rs | 10 | ||||
-rw-r--r-- | intl/l10n/rust/l10nregistry-ffi/src/load.rs | 113 | ||||
-rw-r--r-- | intl/l10n/rust/l10nregistry-ffi/src/registry.rs | 519 | ||||
-rw-r--r-- | intl/l10n/rust/l10nregistry-ffi/src/source.rs | 359 | ||||
-rw-r--r-- | intl/l10n/rust/l10nregistry-ffi/src/xpcom_utils.rs | 129 |
9 files changed, 1382 insertions, 0 deletions
diff --git a/intl/l10n/rust/l10nregistry-ffi/Cargo.toml b/intl/l10n/rust/l10nregistry-ffi/Cargo.toml new file mode 100644 index 0000000000..8d5d575074 --- /dev/null +++ b/intl/l10n/rust/l10nregistry-ffi/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "l10nregistry-ffi" +version = "0.1.0" +authors = ["The Mozilla Project Developers"] +edition = "2018" +license = "MPL-2.0" + +[dependencies] +futures-channel = "0.3" +futures = "0.3" +libc = "0.2" +cstr = "0.2" +log = "0.4" +nserror = { path = "../../../../xpcom/rust/nserror" } +nsstring = { path = "../../../../xpcom/rust/nsstring" } +l10nregistry = { path = "../l10nregistry-rs" } +fluent = { version = "0.16.0", features = ["fluent-pseudo"] } +unic-langid = "0.9" +thin-vec = { version = "0.2.1", features = ["gecko-ffi"] } +async-trait = "0.1" +moz_task = { path = "../../../../xpcom/rust/moz_task" } +xpcom = { path = "../../../../xpcom/rust/xpcom" } +fluent-ffi = { path = "../fluent-ffi" } +fluent-fallback = "0.7.0" diff --git a/intl/l10n/rust/l10nregistry-ffi/cbindgen.toml b/intl/l10n/rust/l10nregistry-ffi/cbindgen.toml new file mode 100644 index 0000000000..9e7127eeaa --- /dev/null +++ b/intl/l10n/rust/l10nregistry-ffi/cbindgen.toml @@ -0,0 +1,26 @@ +header = """/* 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/. */""" +autogen_warning = """/* DO NOT MODIFY THIS MANUALLY! This file was generated using cbindgen. See RunCbindgen.py */ +#ifndef mozilla_intl_l10n_RegistryBindings_h +#error "Don't include this file directly, instead include RegistryBindings.h" +#endif +""" +include_version = true +braces = "SameLine" +line_length = 100 +tab_width = 2 +language = "C++" +namespaces = ["mozilla", "intl", "ffi"] +includes = ["nsIStreamLoader.h"] + +[parse] +parse_deps = true +include = ["fluent-bundle", "fluent-fallback", "l10nregistry"] + +[enum] +derive_helper_methods = true + +[export.rename] +"ThinVec" = "nsTArray" +"Promise" = "dom::Promise" diff --git a/intl/l10n/rust/l10nregistry-ffi/src/env.rs b/intl/l10n/rust/l10nregistry-ffi/src/env.rs new file mode 100644 index 0000000000..7a77af2176 --- /dev/null +++ b/intl/l10n/rust/l10nregistry-ffi/src/env.rs @@ -0,0 +1,132 @@ +/* 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::xpcom_utils::get_app_locales; +use cstr::cstr; +use fluent_fallback::env::LocalesProvider; +use l10nregistry::{ + env::ErrorReporter, + errors::{L10nRegistryError, L10nRegistrySetupError}, +}; +use log::warn; +use nserror::{nsresult, NS_ERROR_NOT_AVAILABLE}; +use nsstring::{nsCStr, nsString}; +use std::fmt::{self, Write}; +use unic_langid::LanguageIdentifier; +use xpcom::interfaces; + +#[derive(Clone)] +pub struct GeckoEnvironment { + custom_locales: Option<Vec<LanguageIdentifier>>, +} + +impl GeckoEnvironment { + pub fn new(custom_locales: Option<Vec<LanguageIdentifier>>) -> Self { + Self { custom_locales } + } + + pub fn report_l10nregistry_setup_error(error: &L10nRegistrySetupError) { + warn!("L10nRegistry setup error: {}", error); + let result = log_simple_console_error( + &error.to_string(), + false, + true, + None, + (0, 0), + interfaces::nsIScriptError::errorFlag as u32, + ); + if let Err(err) = result { + warn!("Error while reporting an error: {}", err); + } + } +} + +impl ErrorReporter for GeckoEnvironment { + fn report_errors(&self, errors: Vec<L10nRegistryError>) { + for error in errors { + warn!("L10nRegistry error: {}", error); + let result = match error { + L10nRegistryError::FluentError { + resource_id, + loc, + error, + } => log_simple_console_error( + &error.to_string(), + false, + true, + Some(nsString::from(&resource_id.value)), + loc.map_or((0, 0), |(l, c)| (l as u32, c as u32)), + interfaces::nsIScriptError::errorFlag as u32, + ), + L10nRegistryError::MissingResource { .. } => log_simple_console_error( + &error.to_string(), + false, + true, + None, + (0, 0), + interfaces::nsIScriptError::warningFlag as u32, + ), + }; + if let Err(err) = result { + warn!("Error while reporting an error: {}", err); + } + } + } +} + +impl LocalesProvider for GeckoEnvironment { + type Iter = std::vec::IntoIter<unic_langid::LanguageIdentifier>; + fn locales(&self) -> Self::Iter { + if let Some(custom_locales) = &self.custom_locales { + custom_locales.clone().into_iter() + } else { + let result = get_app_locales() + .expect("Failed to retrieve app locales") + .into_iter() + .map(|s| LanguageIdentifier::from_bytes(&s).expect("Failed to parse a locale")) + .collect::<Vec<_>>(); + result.into_iter() + } + } +} + +fn log_simple_console_error( + error: &impl fmt::Display, + from_private_window: bool, + from_chrome_context: bool, + path: Option<nsString>, + pos: (u32, u32), + error_flags: u32, +) -> Result<(), nsresult> { + // Format whatever error argument into a wide string with `Display`. + let mut error_str = nsString::new(); + write!(&mut error_str, "{}", error).expect("nsString has an infallible Write impl"); + + // Get the relevant services, and create the script error object. + let console_service = + xpcom::get_service::<interfaces::nsIConsoleService>(cstr!("@mozilla.org/consoleservice;1")) + .ok_or(NS_ERROR_NOT_AVAILABLE)?; + let script_error = + xpcom::create_instance::<interfaces::nsIScriptError>(cstr!("@mozilla.org/scripterror;1")) + .ok_or(NS_ERROR_NOT_AVAILABLE)?; + let category = nsCStr::from("l10n"); + unsafe { + script_error + .Init( + &*error_str, + &*path.unwrap_or(nsString::new()), /* aSourceName */ + &*nsString::new(), /* aSourceLine */ + pos.0, /* aLineNumber */ + pos.1, /* aColNumber */ + error_flags, + &*category, + from_private_window, + from_chrome_context, + ) + .to_result()?; + + console_service.LogMessage(&**script_error).to_result()?; + } + Ok(()) +} diff --git a/intl/l10n/rust/l10nregistry-ffi/src/fetcher.rs b/intl/l10n/rust/l10nregistry-ffi/src/fetcher.rs new file mode 100644 index 0000000000..aba8a81470 --- /dev/null +++ b/intl/l10n/rust/l10nregistry-ffi/src/fetcher.rs @@ -0,0 +1,70 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use l10nregistry::source::{FileFetcher, ResourceId}; + +use std::{borrow::Cow, io}; + +pub struct GeckoFileFetcher; + +fn try_string_from_box_u8(input: Box<[u8]>) -> io::Result<String> { + String::from_utf8(input.into()) + .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err.utf8_error())) +} + +// For historical reasons we maintain a locale in Firefox with a codename `ja-JP-mac`. +// This string is an invalid BCP-47 language tag, so we don't store it in Gecko, which uses +// valid BCP-47 tags only, but rather keep that quirk local to Gecko L10nRegistry file fetcher. +// +// Here, if we encounter `ja-JP-macos` (valid BCP-47), we swap it for `ja-JP-mac`. +// +// See bug 1726586 for details, and source::get_locale_from_gecko. +fn get_path_for_gecko<'s>(input: &'s str) -> Cow<'s, str> { + if input.contains("ja-JP-macos") { + input.replace("ja-JP-macos", "ja-JP-mac").into() + } else { + input.into() + } +} + +#[async_trait::async_trait(?Send)] +impl FileFetcher for GeckoFileFetcher { + fn fetch_sync(&self, resource_id: &ResourceId) -> io::Result<String> { + let path = get_path_for_gecko(&resource_id.value); + crate::load::load_sync(path).and_then(try_string_from_box_u8) + } + + async fn fetch(&self, resource_id: &ResourceId) -> io::Result<String> { + let path = get_path_for_gecko(&resource_id.value); + crate::load::load_async(path) + .await + .and_then(try_string_from_box_u8) + } +} + +pub struct MockFileFetcher { + fs: Vec<(String, String)>, +} + +impl MockFileFetcher { + pub fn new(fs: Vec<(String, String)>) -> Self { + Self { fs } + } +} + +#[async_trait::async_trait(?Send)] +impl FileFetcher for MockFileFetcher { + fn fetch_sync(&self, resource_id: &ResourceId) -> io::Result<String> { + for (p, source) in &self.fs { + if p == &resource_id.value { + return Ok(source.clone()); + } + } + Err(io::Error::new(io::ErrorKind::NotFound, "File not found")) + } + + async fn fetch(&self, resource_id: &ResourceId) -> io::Result<String> { + self.fetch_sync(resource_id) + } +} diff --git a/intl/l10n/rust/l10nregistry-ffi/src/lib.rs b/intl/l10n/rust/l10nregistry-ffi/src/lib.rs new file mode 100644 index 0000000000..843860abf9 --- /dev/null +++ b/intl/l10n/rust/l10nregistry-ffi/src/lib.rs @@ -0,0 +1,10 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +pub mod env; +mod fetcher; +pub mod load; +pub mod registry; +mod source; +mod xpcom_utils; diff --git a/intl/l10n/rust/l10nregistry-ffi/src/load.rs b/intl/l10n/rust/l10nregistry-ffi/src/load.rs new file mode 100644 index 0000000000..04a041246f --- /dev/null +++ b/intl/l10n/rust/l10nregistry-ffi/src/load.rs @@ -0,0 +1,113 @@ +/* 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 futures_channel::oneshot; +use nserror::{nsresult, NS_OK, NS_SUCCESS_ADOPTED_DATA}; +use nsstring::{nsACString, nsCStringLike}; +use std::{ + cell::Cell, + ffi::c_void, + io::{self, Error, ErrorKind}, + ptr, +}; +use xpcom::{ + interfaces::{nsIStreamLoader, nsIStreamLoaderObserver, nsISupports}, + xpcom, +}; + +unsafe fn boxed_slice_from_raw(ptr: *mut u8, len: usize) -> Box<[u8]> { + if ptr.is_null() { + // It is undefined behaviour to create a `Box<[u8]>` with a null pointer, + // so avoid that case. + assert_eq!(len, 0); + Box::new([]) + } else { + Box::from_raw(ptr::slice_from_raw_parts_mut(ptr, len)) + } +} + +#[xpcom(implement(nsIStreamLoaderObserver), nonatomic)] +struct StreamLoaderObserver { + sender: Cell<Option<oneshot::Sender<Result<Box<[u8]>, nsresult>>>>, +} + +impl StreamLoaderObserver { + #[allow(non_snake_case)] + unsafe fn OnStreamComplete( + &self, + _loader: *const nsIStreamLoader, + _ctxt: *const nsISupports, + status: nsresult, + result_length: u32, + result: *const u8, + ) -> nsresult { + let sender = match self.sender.take() { + Some(sender) => sender, + None => return NS_OK, + }; + + if status.failed() { + sender.send(Err(status)).expect("Failed to send data"); + return NS_OK; + } + + // safety: take ownership of the data passed in. This is OK because we + // have configured Rust and C++ to use the same allocator, and our + // caller won't free the `result` pointer if we return + // NS_SUCCESS_ADOPTED_DATA. + sender + .send(Ok(boxed_slice_from_raw( + result as *mut u8, + result_length as usize, + ))) + .expect("Failed to send data"); + NS_SUCCESS_ADOPTED_DATA + } +} + +extern "C" { + fn L10nRegistryLoad( + path: *const nsACString, + observer: *const nsIStreamLoaderObserver, + ) -> nsresult; + + fn L10nRegistryLoadSync( + aPath: *const nsACString, + aData: *mut *mut c_void, + aSize: *mut u64, + ) -> nsresult; +} + +pub async fn load_async(path: impl nsCStringLike) -> io::Result<Box<[u8]>> { + let (sender, receiver) = oneshot::channel::<Result<Box<[u8]>, nsresult>>(); + let observer = StreamLoaderObserver::allocate(InitStreamLoaderObserver { + sender: Cell::new(Some(sender)), + }); + unsafe { + L10nRegistryLoad(&*path.adapt(), observer.coerce()) + .to_result() + .map_err(|err| Error::new(ErrorKind::Other, err))?; + } + receiver + .await + .expect("Failed to receive from observer.") + .map_err(|err| Error::new(ErrorKind::Other, err)) +} + +pub fn load_sync(path: impl nsCStringLike) -> io::Result<Box<[u8]>> { + let mut data_ptr: *mut c_void = ptr::null_mut(); + let mut data_length: u64 = 0; + unsafe { + L10nRegistryLoadSync(&*path.adapt(), &mut data_ptr, &mut data_length) + .to_result() + .map_err(|err| Error::new(ErrorKind::Other, err))?; + + // The call succeeded, meaning `data_ptr` and `size` have been filled in with owning pointers to actual data payloads (or null). + // If we get a null, return a successful read of the empty file. + Ok(boxed_slice_from_raw( + data_ptr as *mut u8, + data_length as usize, + )) + } +} diff --git a/intl/l10n/rust/l10nregistry-ffi/src/registry.rs b/intl/l10n/rust/l10nregistry-ffi/src/registry.rs new file mode 100644 index 0000000000..846f12273c --- /dev/null +++ b/intl/l10n/rust/l10nregistry-ffi/src/registry.rs @@ -0,0 +1,519 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +use fluent_ffi::{adapt_bundle_for_gecko, FluentBundleRc}; +use nsstring::{nsACString, nsCString}; +use std::mem; +use std::rc::Rc; +use thin_vec::ThinVec; + +use crate::{env::GeckoEnvironment, fetcher::GeckoFileFetcher, xpcom_utils::is_parent_process}; +use fluent_fallback::{generator::BundleGenerator, types::ResourceType}; +use futures_channel::mpsc::{unbounded, UnboundedSender}; +pub use l10nregistry::{ + errors::L10nRegistrySetupError, + registry::{BundleAdapter, GenerateBundles, GenerateBundlesSync, L10nRegistry}, + source::{FileSource, ResourceId, ToResourceId}, +}; +use unic_langid::LanguageIdentifier; +use xpcom::RefPtr; + +#[derive(Clone)] +pub struct GeckoBundleAdapter { + use_isolating: bool, +} + +impl Default for GeckoBundleAdapter { + fn default() -> Self { + Self { + use_isolating: true, + } + } +} + +impl BundleAdapter for GeckoBundleAdapter { + fn adapt_bundle(&self, bundle: &mut l10nregistry::fluent::FluentBundle) { + bundle.set_use_isolating(self.use_isolating); + adapt_bundle_for_gecko(bundle, None); + } +} + +thread_local!(static L10N_REGISTRY: Rc<GeckoL10nRegistry> = { + let sources = if is_parent_process() { + let packaged_locales = get_packaged_locales(); + let entries = get_l10n_registry_category_entries(); + + Some(entries + .into_iter() + .map(|entry| { + FileSource::new( + entry.entry.to_string(), + Some("app".to_string()), + packaged_locales.clone(), + entry.value.to_string(), + Default::default(), + GeckoFileFetcher, + ) + }) + .collect()) + + } else { + None + }; + + create_l10n_registry(sources) +}); + +pub type GeckoL10nRegistry = L10nRegistry<GeckoEnvironment, GeckoBundleAdapter>; +pub type GeckoFluentBundleIterator = GenerateBundlesSync<GeckoEnvironment, GeckoBundleAdapter>; + +trait GeckoReportError<V, E> { + fn report_error(self) -> Result<V, E>; +} + +impl<V> GeckoReportError<V, L10nRegistrySetupError> for Result<V, L10nRegistrySetupError> { + fn report_error(self) -> Self { + if let Err(ref err) = self { + GeckoEnvironment::report_l10nregistry_setup_error(err); + } + self + } +} + +#[derive(Debug)] +#[repr(C)] +pub struct L10nFileSourceDescriptor { + name: nsCString, + metasource: nsCString, + locales: ThinVec<nsCString>, + pre_path: nsCString, + index: ThinVec<nsCString>, +} + +fn get_l10n_registry_category_entries() -> Vec<crate::xpcom_utils::CategoryEntry> { + crate::xpcom_utils::get_category_entries(&nsCString::from("l10n-registry")).unwrap_or_default() +} + +fn get_packaged_locales() -> Vec<LanguageIdentifier> { + crate::xpcom_utils::get_packaged_locales() + .map(|locales| { + locales + .into_iter() + .map(|s| s.to_utf8().parse().expect("Failed to parse locale.")) + .collect() + }) + .unwrap_or_default() +} + +fn create_l10n_registry(sources: Option<Vec<FileSource>>) -> Rc<GeckoL10nRegistry> { + let env = GeckoEnvironment::new(None); + let mut reg = L10nRegistry::with_provider(env); + + reg.set_bundle_adapter(GeckoBundleAdapter::default()) + .expect("Failed to set bundle adaptation closure."); + + if let Some(sources) = sources { + reg.register_sources(sources) + .expect("Failed to register sources."); + } + Rc::new(reg) +} + +pub fn set_l10n_registry(new_sources: &ThinVec<L10nFileSourceDescriptor>) { + L10N_REGISTRY.with(|reg| { + let new_source_names: Vec<_> = new_sources + .iter() + .map(|d| d.name.to_utf8().to_string()) + .collect(); + let old_sources = reg.get_source_names().unwrap(); + + let mut sources_to_be_removed = vec![]; + for name in &old_sources { + if !new_source_names.contains(&name) { + sources_to_be_removed.push(name); + } + } + reg.remove_sources(sources_to_be_removed).unwrap(); + + let mut add_sources = vec![]; + for desc in new_sources { + if !old_sources.contains(&desc.name.to_string()) { + add_sources.push(FileSource::new( + desc.name.to_string(), + Some(desc.metasource.to_string()), + desc.locales + .iter() + .map(|s| s.to_utf8().parse().unwrap()) + .collect(), + desc.pre_path.to_string(), + Default::default(), + GeckoFileFetcher, + )); + } + } + reg.register_sources(add_sources).unwrap(); + }); +} + +pub fn get_l10n_registry() -> Rc<GeckoL10nRegistry> { + L10N_REGISTRY.with(|reg| reg.clone()) +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub enum GeckoResourceType { + Optional, + Required, +} + +#[repr(C)] +pub struct GeckoResourceId { + value: nsCString, + resource_type: GeckoResourceType, +} + +impl From<&GeckoResourceId> for ResourceId { + fn from(resource_id: &GeckoResourceId) -> Self { + resource_id + .value + .to_string() + .to_resource_id(match resource_id.resource_type { + GeckoResourceType::Optional => ResourceType::Optional, + GeckoResourceType::Required => ResourceType::Required, + }) + } +} + +#[repr(C)] +pub enum L10nRegistryStatus { + None, + EmptyName, + InvalidLocaleCode, +} + +#[no_mangle] +pub extern "C" fn l10nregistry_new(use_isolating: bool) -> *const GeckoL10nRegistry { + let env = GeckoEnvironment::new(None); + let mut reg = L10nRegistry::with_provider(env); + let _ = reg + .set_bundle_adapter(GeckoBundleAdapter { use_isolating }) + .report_error(); + Rc::into_raw(Rc::new(reg)) +} + +#[no_mangle] +pub extern "C" fn l10nregistry_instance_get() -> *const GeckoL10nRegistry { + let reg = get_l10n_registry(); + Rc::into_raw(reg) +} + +#[no_mangle] +pub unsafe extern "C" fn l10nregistry_get_parent_process_sources( + sources: &mut ThinVec<L10nFileSourceDescriptor>, +) { + debug_assert!( + is_parent_process(), + "This should be called only in parent process." + ); + + // If at the point when the first content process is being initialized, the parent + // process `L10nRegistryService` has not been initialized yet, this will trigger it. + // + // This is architecturally imperfect, but acceptable for simplicity reasons because + // `L10nRegistry` instance is cheap and mainly servers as a store of state. + let reg = get_l10n_registry(); + for name in reg.get_source_names().unwrap() { + let source = reg.file_source_by_name(&name).unwrap().unwrap(); + let descriptor = L10nFileSourceDescriptor { + name: source.name.as_str().into(), + metasource: source.metasource.as_str().into(), + locales: source + .locales() + .iter() + .map(|l| l.to_string().into()) + .collect(), + pre_path: source.pre_path.as_str().into(), + index: source + .get_index() + .map(|index| index.into_iter().map(|s| s.into()).collect()) + .unwrap_or_default(), + }; + sources.push(descriptor); + } +} + +#[no_mangle] +pub unsafe extern "C" fn l10nregistry_register_parent_process_sources( + sources: &ThinVec<L10nFileSourceDescriptor>, +) { + debug_assert!( + !is_parent_process(), + "This should be called only in content process." + ); + set_l10n_registry(sources); +} + +#[no_mangle] +pub unsafe extern "C" fn l10nregistry_addref(reg: *const GeckoL10nRegistry) { + let raw = Rc::from_raw(reg); + mem::forget(Rc::clone(&raw)); + mem::forget(raw); +} + +#[no_mangle] +pub unsafe extern "C" fn l10nregistry_release(reg: *const GeckoL10nRegistry) { + let _ = Rc::from_raw(reg); +} + +#[no_mangle] +pub extern "C" fn l10nregistry_get_available_locales( + reg: &GeckoL10nRegistry, + result: &mut ThinVec<nsCString>, +) { + if let Ok(locales) = reg.get_available_locales().report_error() { + result.extend(locales.into_iter().map(|locale| locale.to_string().into())); + } +} + +fn broadcast_settings_if_parent(reg: &GeckoL10nRegistry) { + if !is_parent_process() { + return; + } + + L10N_REGISTRY.with(|reg_service| { + if std::ptr::eq(Rc::as_ptr(reg_service), reg) { + let locales = reg + .get_available_locales() + .unwrap() + .iter() + .map(|loc| loc.to_string().into()) + .collect(); + + unsafe { + crate::xpcom_utils::set_available_locales(&locales); + L10nRegistrySendUpdateL10nFileSources(); + } + } + }); +} + +#[no_mangle] +pub extern "C" fn l10nregistry_register_sources( + reg: &GeckoL10nRegistry, + sources: &ThinVec<&FileSource>, +) { + let _ = reg + .register_sources(sources.iter().map(|&s| s.clone()).collect()) + .report_error(); + + broadcast_settings_if_parent(reg); +} + +#[no_mangle] +pub extern "C" fn l10nregistry_update_sources( + reg: &GeckoL10nRegistry, + sources: &mut ThinVec<&FileSource>, +) { + let _ = reg + .update_sources(sources.iter().map(|&s| s.clone()).collect()) + .report_error(); + broadcast_settings_if_parent(reg); +} + +#[no_mangle] +pub unsafe extern "C" fn l10nregistry_remove_sources( + reg: &GeckoL10nRegistry, + sources_elements: *const nsCString, + sources_length: usize, +) { + if sources_elements.is_null() { + return; + } + + let sources = std::slice::from_raw_parts(sources_elements, sources_length); + let _ = reg.remove_sources(sources.to_vec()).report_error(); + broadcast_settings_if_parent(reg); +} + +#[no_mangle] +pub extern "C" fn l10nregistry_has_source( + reg: &GeckoL10nRegistry, + name: &nsACString, + status: &mut L10nRegistryStatus, +) -> bool { + if name.is_empty() { + *status = L10nRegistryStatus::EmptyName; + return false; + } + *status = L10nRegistryStatus::None; + reg.has_source(&name.to_utf8()) + .report_error() + .unwrap_or(false) +} + +#[no_mangle] +pub extern "C" fn l10nregistry_get_source( + reg: &GeckoL10nRegistry, + name: &nsACString, + status: &mut L10nRegistryStatus, +) -> *mut FileSource { + if name.is_empty() { + *status = L10nRegistryStatus::EmptyName; + return std::ptr::null_mut(); + } + + *status = L10nRegistryStatus::None; + + if let Ok(Some(source)) = reg.file_source_by_name(&name.to_utf8()).report_error() { + Box::into_raw(Box::new(source)) + } else { + std::ptr::null_mut() + } +} + +#[no_mangle] +pub extern "C" fn l10nregistry_clear_sources(reg: &GeckoL10nRegistry) { + let _ = reg.clear_sources().report_error(); + + broadcast_settings_if_parent(reg); +} + +#[no_mangle] +pub extern "C" fn l10nregistry_get_source_names( + reg: &GeckoL10nRegistry, + result: &mut ThinVec<nsCString>, +) { + if let Ok(names) = reg.get_source_names().report_error() { + result.extend(names.into_iter().map(|name| nsCString::from(name))); + } +} + +#[no_mangle] +pub unsafe extern "C" fn l10nregistry_generate_bundles_sync( + reg: &GeckoL10nRegistry, + locales_elements: *const nsCString, + locales_length: usize, + res_ids_elements: *const GeckoResourceId, + res_ids_length: usize, + status: &mut L10nRegistryStatus, +) -> *mut GeckoFluentBundleIterator { + let locales = std::slice::from_raw_parts(locales_elements, locales_length); + let res_ids = std::slice::from_raw_parts(res_ids_elements, res_ids_length) + .into_iter() + .map(ResourceId::from) + .collect(); + let locales: Result<Vec<LanguageIdentifier>, _> = + locales.into_iter().map(|s| s.to_utf8().parse()).collect(); + + match locales { + Ok(locales) => { + *status = L10nRegistryStatus::None; + let iter = reg.bundles_iter(locales.into_iter(), res_ids); + Box::into_raw(Box::new(iter)) + } + Err(_) => { + *status = L10nRegistryStatus::InvalidLocaleCode; + std::ptr::null_mut() + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn fluent_bundle_iterator_destroy(iter: *mut GeckoFluentBundleIterator) { + let _ = Box::from_raw(iter); +} + +#[no_mangle] +pub extern "C" fn fluent_bundle_iterator_next( + iter: &mut GeckoFluentBundleIterator, +) -> *mut FluentBundleRc { + if let Some(Ok(result)) = iter.next() { + Box::into_raw(Box::new(result)) + } else { + std::ptr::null_mut() + } +} + +pub struct NextRequest { + promise: RefPtr<xpcom::Promise>, + // Ownership is transferred here. + callback: unsafe extern "C" fn(&xpcom::Promise, *mut FluentBundleRc), +} + +pub struct GeckoFluentBundleAsyncIteratorWrapper(UnboundedSender<NextRequest>); + +#[no_mangle] +pub unsafe extern "C" fn l10nregistry_generate_bundles( + reg: &GeckoL10nRegistry, + locales_elements: *const nsCString, + locales_length: usize, + res_ids_elements: *const GeckoResourceId, + res_ids_length: usize, + status: &mut L10nRegistryStatus, +) -> *mut GeckoFluentBundleAsyncIteratorWrapper { + let locales = std::slice::from_raw_parts(locales_elements, locales_length); + let res_ids = std::slice::from_raw_parts(res_ids_elements, res_ids_length) + .into_iter() + .map(ResourceId::from) + .collect(); + let locales: Result<Vec<LanguageIdentifier>, _> = + locales.into_iter().map(|s| s.to_utf8().parse()).collect(); + + match locales { + Ok(locales) => { + *status = L10nRegistryStatus::None; + let mut iter = reg.bundles_stream(locales.into_iter(), res_ids); + + // Immediately spawn the task which will handle the async calls, and use an `UnboundedSender` + // to send callbacks for specific `next()` calls to it. + let (sender, mut receiver) = unbounded::<NextRequest>(); + moz_task::spawn_local("l10nregistry_generate_bundles", async move { + use futures::StreamExt; + while let Some(req) = receiver.next().await { + let result = match iter.next().await { + Some(Ok(result)) => Box::into_raw(Box::new(result)), + _ => std::ptr::null_mut(), + }; + (req.callback)(&req.promise, result); + } + }) + .detach(); + let iter = GeckoFluentBundleAsyncIteratorWrapper(sender); + Box::into_raw(Box::new(iter)) + } + Err(_) => { + *status = L10nRegistryStatus::InvalidLocaleCode; + std::ptr::null_mut() + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn fluent_bundle_async_iterator_destroy( + iter: *mut GeckoFluentBundleAsyncIteratorWrapper, +) { + let _ = Box::from_raw(iter); +} + +#[no_mangle] +pub extern "C" fn fluent_bundle_async_iterator_next( + iter: &GeckoFluentBundleAsyncIteratorWrapper, + promise: &xpcom::Promise, + callback: extern "C" fn(&xpcom::Promise, *mut FluentBundleRc), +) { + if iter + .0 + .unbounded_send(NextRequest { + promise: RefPtr::new(promise), + callback, + }) + .is_err() + { + callback(promise, std::ptr::null_mut()); + } +} + +extern "C" { + pub fn L10nRegistrySendUpdateL10nFileSources(); +} diff --git a/intl/l10n/rust/l10nregistry-ffi/src/source.rs b/intl/l10n/rust/l10nregistry-ffi/src/source.rs new file mode 100644 index 0000000000..7003d8da97 --- /dev/null +++ b/intl/l10n/rust/l10nregistry-ffi/src/source.rs @@ -0,0 +1,359 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +use super::fetcher::{GeckoFileFetcher, MockFileFetcher}; +use crate::env::GeckoEnvironment; + +use fluent::FluentResource; +use l10nregistry::source::{FileSource, FileSourceOptions, ResourceOption, ResourceStatus, RcResource}; + +use nsstring::{nsACString, nsCString}; +use thin_vec::ThinVec; +use unic_langid::LanguageIdentifier; + +use std::{borrow::Cow, mem, rc::Rc}; +use xpcom::RefPtr; + +#[repr(C)] +pub enum L10nFileSourceStatus { + None, + EmptyName, + EmptyPrePath, + EmptyResId, + InvalidLocaleCode, +} + +// For historical reasons we maintain a locale in Firefox with a codename `ja-JP-mac`. +// This string is an invalid BCP-47 language tag, so we don't store it in Gecko, which uses +// valid BCP-47 tags only, but rather keep that quirk local to Gecko L10nRegistry file fetcher. +// +// Here, if we encounter `ja-JP-mac` (invalid BCP-47), we swap it for a valid equivalent: `ja-JP-macos`. +// +// See bug 1726586 for details and fetcher::get_locale_for_gecko. +fn get_locale_from_gecko<'s>(input: Cow<'s, str>) -> Cow<'s, str> { + if input == "ja-JP-mac" { + "ja-JP-macos".into() + } else { + input + } +} + +#[no_mangle] +pub extern "C" fn l10nfilesource_new( + name: &nsACString, + metasource: &nsACString, + locales: &ThinVec<nsCString>, + pre_path: &nsACString, + allow_override: bool, + status: &mut L10nFileSourceStatus, +) -> *const FileSource { + if name.is_empty() { + *status = L10nFileSourceStatus::EmptyName; + return std::ptr::null(); + } + if pre_path.is_empty() { + *status = L10nFileSourceStatus::EmptyPrePath; + return std::ptr::null(); + } + + let locales: Result<Vec<LanguageIdentifier>, _> = locales + .iter() + .map(|l| get_locale_from_gecko(l.to_utf8()).parse()) + .collect(); + + let locales = match locales { + Ok(locales) => locales, + Err(..) => { + *status = L10nFileSourceStatus::InvalidLocaleCode; + return std::ptr::null(); + } + }; + + let mut source = FileSource::new( + name.to_string(), + Some(metasource.to_string()), + locales, + pre_path.to_string(), + FileSourceOptions { allow_override }, + GeckoFileFetcher, + ); + source.set_reporter(GeckoEnvironment::new(None)); + + *status = L10nFileSourceStatus::None; + Rc::into_raw(Rc::new(source)) +} + +#[no_mangle] +pub unsafe extern "C" fn l10nfilesource_new_with_index( + name: &nsACString, + metasource: &nsACString, + locales: &ThinVec<nsCString>, + pre_path: &nsACString, + index_elements: *const nsCString, + index_length: usize, + allow_override: bool, + status: &mut L10nFileSourceStatus, +) -> *const FileSource { + if name.is_empty() { + *status = L10nFileSourceStatus::EmptyName; + return std::ptr::null(); + } + if pre_path.is_empty() { + *status = L10nFileSourceStatus::EmptyPrePath; + return std::ptr::null(); + } + + let locales: Result<Vec<LanguageIdentifier>, _> = locales + .iter() + .map(|l| get_locale_from_gecko(l.to_utf8()).parse()) + .collect(); + + let index = if index_length > 0 { + assert!(!index_elements.is_null()); + std::slice::from_raw_parts(index_elements, index_length) + } else { + &[] + } + .into_iter() + .map(|s| s.to_string()) + .collect(); + + let locales = match locales { + Ok(locales) => locales, + Err(..) => { + *status = L10nFileSourceStatus::InvalidLocaleCode; + return std::ptr::null(); + } + }; + + let mut source = FileSource::new_with_index( + name.to_string(), + Some(metasource.to_string()), + locales, + pre_path.to_string(), + FileSourceOptions { allow_override }, + GeckoFileFetcher, + index, + ); + source.set_reporter(GeckoEnvironment::new(None)); + + *status = L10nFileSourceStatus::None; + Rc::into_raw(Rc::new(source)) +} + +#[repr(C)] +pub struct L10nFileSourceMockFile { + path: nsCString, + source: nsCString, +} + +#[no_mangle] +pub extern "C" fn l10nfilesource_new_mock( + name: &nsACString, + metasource: &nsACString, + locales: &ThinVec<nsCString>, + pre_path: &nsACString, + fs: &ThinVec<L10nFileSourceMockFile>, + status: &mut L10nFileSourceStatus, +) -> *const FileSource { + if name.is_empty() { + *status = L10nFileSourceStatus::EmptyName; + return std::ptr::null(); + } + if pre_path.is_empty() { + *status = L10nFileSourceStatus::EmptyPrePath; + return std::ptr::null(); + } + + let locales: Result<Vec<LanguageIdentifier>, _> = locales + .iter() + .map(|l| get_locale_from_gecko(l.to_utf8()).parse()) + .collect(); + + let locales = match locales { + Ok(locales) => locales, + Err(..) => { + *status = L10nFileSourceStatus::InvalidLocaleCode; + return std::ptr::null(); + } + }; + + let fs = fs + .iter() + .map(|mock| (mock.path.to_string(), mock.source.to_string())) + .collect(); + let fetcher = MockFileFetcher::new(fs); + let mut source = FileSource::new( + name.to_string(), + Some(metasource.to_string()), + locales, + pre_path.to_string(), + Default::default(), + fetcher, + ); + source.set_reporter(GeckoEnvironment::new(None)); + + *status = L10nFileSourceStatus::None; + Rc::into_raw(Rc::new(source)) +} + +#[no_mangle] +pub unsafe extern "C" fn l10nfilesource_addref(source: *const FileSource) { + let raw = Rc::from_raw(source); + mem::forget(Rc::clone(&raw)); + mem::forget(raw); +} + +#[no_mangle] +pub unsafe extern "C" fn l10nfilesource_release(source: *const FileSource) { + let _ = Rc::from_raw(source); +} + +#[no_mangle] +pub extern "C" fn l10nfilesource_get_name(source: &FileSource, ret_val: &mut nsACString) { + ret_val.assign(&source.name); +} + +#[no_mangle] +pub extern "C" fn l10nfilesource_get_metasource(source: &FileSource, ret_val: &mut nsACString) { + ret_val.assign(&source.metasource); +} + +#[no_mangle] +pub extern "C" fn l10nfilesource_get_locales( + source: &FileSource, + ret_val: &mut ThinVec<nsCString>, +) { + for locale in source.locales() { + ret_val.push(locale.to_string().into()); + } +} + +#[no_mangle] +pub extern "C" fn l10nfilesource_get_prepath(source: &FileSource, ret_val: &mut nsACString) { + ret_val.assign(&source.pre_path); +} + +#[no_mangle] +pub extern "C" fn l10nfilesource_get_index( + source: &FileSource, + ret_val: &mut ThinVec<nsCString>, +) -> bool { + if let Some(index) = source.get_index() { + for entry in index { + ret_val.push(entry.to_string().into()); + } + true + } else { + false + } +} + +#[no_mangle] +pub extern "C" fn l10nfilesource_has_file( + source: &FileSource, + locale: &nsACString, + path: &nsACString, + status: &mut L10nFileSourceStatus, + present: &mut bool, +) -> bool { + if path.is_empty() { + *status = L10nFileSourceStatus::EmptyResId; + return false; + } + + let locale = match locale.to_utf8().parse() { + Ok(locale) => locale, + Err(..) => { + *status = L10nFileSourceStatus::InvalidLocaleCode; + return false; + } + }; + + *status = L10nFileSourceStatus::None; + // To work around Option<bool> we return bool for the option, + // and the `present` argument is the value of it. + if let Some(val) = source.has_file(&locale, &path.to_utf8().into()) { + *present = val; + true + } else { + false + } +} + +#[no_mangle] +pub extern "C" fn l10nfilesource_fetch_file_sync( + source: &FileSource, + locale: &nsACString, + path: &nsACString, + status: &mut L10nFileSourceStatus, +) -> *const FluentResource { + if path.is_empty() { + *status = L10nFileSourceStatus::EmptyResId; + return std::ptr::null(); + } + + let locale = match locale.to_utf8().parse() { + Ok(locale) => locale, + Err(..) => { + *status = L10nFileSourceStatus::InvalidLocaleCode; + return std::ptr::null(); + } + }; + + *status = L10nFileSourceStatus::None; + //XXX: Bug 1723191 - if we encounter a request for sync load while async load is in progress + // we will discard the async load and force the sync load instead. + // There may be a better option but we haven't had time to explore it. + if let ResourceOption::Some(res) = + source.fetch_file_sync(&locale, &path.to_utf8().into(), /* overload */ true) + { + Rc::into_raw(res) + } else { + std::ptr::null() + } +} + +#[no_mangle] +pub unsafe extern "C" fn l10nfilesource_fetch_file( + source: &FileSource, + locale: &nsACString, + path: &nsACString, + promise: &xpcom::Promise, + callback: extern "C" fn(&xpcom::Promise, Option<&FluentResource>), + status: &mut L10nFileSourceStatus, +) { + if path.is_empty() { + *status = L10nFileSourceStatus::EmptyResId; + return; + } + + let locale = match locale.to_utf8().parse() { + Ok(locale) => locale, + Err(..) => { + *status = L10nFileSourceStatus::InvalidLocaleCode; + return; + } + }; + + *status = L10nFileSourceStatus::None; + + let path = path.to_utf8().into(); + + match source.fetch_file(&locale, &path) { + ResourceStatus::MissingOptional => callback(promise, None), + ResourceStatus::MissingRequired => callback(promise, None), + ResourceStatus::Loaded(res) => callback(promise, Some(&res)), + res @ ResourceStatus::Loading(_) => { + let strong_promise = RefPtr::new(promise); + moz_task::spawn_local("l10nfilesource_fetch_file", async move { + callback( + &strong_promise, + Option::<RcResource>::from(res.await).as_ref().map(|r| &**r), + ); + }) + .detach(); + } + } +} diff --git a/intl/l10n/rust/l10nregistry-ffi/src/xpcom_utils.rs b/intl/l10n/rust/l10nregistry-ffi/src/xpcom_utils.rs new file mode 100644 index 0000000000..7c83088075 --- /dev/null +++ b/intl/l10n/rust/l10nregistry-ffi/src/xpcom_utils.rs @@ -0,0 +1,129 @@ +/* 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 cstr::cstr; +use nsstring::{nsACString, nsCString}; +use std::marker::PhantomData; +use thin_vec::ThinVec; +use xpcom::{ + get_service, getter_addrefs, + interfaces::{ + mozILocaleService, nsICategoryEntry, nsICategoryManager, nsISimpleEnumerator, nsIXULRuntime, + }, + RefPtr, XpCom, +}; + +pub struct IterSimpleEnumerator<T> { + enumerator: RefPtr<nsISimpleEnumerator>, + phantom: PhantomData<T>, +} + +impl<T: XpCom> IterSimpleEnumerator<T> { + /// Convert a `nsISimpleEnumerator` into a rust `Iterator` type. + pub fn new(enumerator: RefPtr<nsISimpleEnumerator>) -> Self { + IterSimpleEnumerator { + enumerator, + phantom: PhantomData, + } + } +} + +impl<T: XpCom + 'static> Iterator for IterSimpleEnumerator<T> { + type Item = RefPtr<T>; + + fn next(&mut self) -> Option<Self::Item> { + let mut more = false; + unsafe { + self.enumerator + .HasMoreElements(&mut more) + .to_result() + .ok()? + } + if !more { + return None; + } + + let element = getter_addrefs(|p| unsafe { self.enumerator.GetNext(p) }).ok()?; + element.query_interface::<T>() + } +} + +fn process_type() -> u32 { + if let Ok(appinfo) = xpcom::components::XULRuntime::service::<nsIXULRuntime>() { + let mut process_type = nsIXULRuntime::PROCESS_TYPE_DEFAULT; + if unsafe { appinfo.GetProcessType(&mut process_type).succeeded() } { + return process_type; + } + } + nsIXULRuntime::PROCESS_TYPE_DEFAULT +} + +pub fn is_parent_process() -> bool { + process_type() == nsIXULRuntime::PROCESS_TYPE_DEFAULT +} + +pub fn get_packaged_locales() -> Option<ThinVec<nsCString>> { + let locale_service = + get_service::<mozILocaleService>(cstr!("@mozilla.org/intl/localeservice;1"))?; + let mut locales = ThinVec::new(); + unsafe { + locale_service + .GetPackagedLocales(&mut locales) + .to_result() + .ok()?; + } + Some(locales) +} + +pub fn get_app_locales() -> Option<ThinVec<nsCString>> { + let locale_service = + get_service::<mozILocaleService>(cstr!("@mozilla.org/intl/localeservice;1"))?; + let mut locales = ThinVec::new(); + unsafe { + locale_service + .GetAppLocalesAsBCP47(&mut locales) + .to_result() + .ok()?; + } + Some(locales) +} + +pub fn set_available_locales(locales: &ThinVec<nsCString>) { + let locale_service = + get_service::<mozILocaleService>(cstr!("@mozilla.org/intl/localeservice;1")) + .expect("Failed to get a service."); + unsafe { + locale_service + .SetAvailableLocales(locales) + .to_result() + .expect("Failed to set locales."); + } +} + +pub struct CategoryEntry { + pub entry: nsCString, + pub value: nsCString, +} + +pub fn get_category_entries(category: &nsACString) -> Option<Vec<CategoryEntry>> { + let category_manager = + get_service::<nsICategoryManager>(cstr!("@mozilla.org/categorymanager;1"))?; + + let enumerator = + getter_addrefs(|p| unsafe { category_manager.EnumerateCategory(category, p) }).ok()?; + + Some( + IterSimpleEnumerator::<nsICategoryEntry>::new(enumerator) + .map(|ientry| { + let mut entry = nsCString::new(); + let mut value = nsCString::new(); + unsafe { + let _ = ientry.GetEntry(&mut *entry); + let _ = ientry.GetValue(&mut *value); + } + CategoryEntry { entry, value } + }) + .collect(), + ) +} |