summaryrefslogtreecommitdiffstats
path: root/intl/l10n/rust/l10nregistry-ffi/src
diff options
context:
space:
mode:
Diffstat (limited to 'intl/l10n/rust/l10nregistry-ffi/src')
-rw-r--r--intl/l10n/rust/l10nregistry-ffi/src/env.rs132
-rw-r--r--intl/l10n/rust/l10nregistry-ffi/src/fetcher.rs70
-rw-r--r--intl/l10n/rust/l10nregistry-ffi/src/lib.rs10
-rw-r--r--intl/l10n/rust/l10nregistry-ffi/src/load.rs113
-rw-r--r--intl/l10n/rust/l10nregistry-ffi/src/registry.rs519
-rw-r--r--intl/l10n/rust/l10nregistry-ffi/src/source.rs359
-rw-r--r--intl/l10n/rust/l10nregistry-ffi/src/xpcom_utils.rs129
7 files changed, 1332 insertions, 0 deletions
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(),
+ )
+}