/* 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 = { 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; pub type GeckoFluentBundleIterator = GenerateBundlesSync; trait GeckoReportError { fn report_error(self) -> Result; } impl GeckoReportError for Result { 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, pre_path: nsCString, index: ThinVec, } fn get_l10n_registry_category_entries() -> Vec { crate::xpcom_utils::get_category_entries(&nsCString::from("l10n-registry")).unwrap_or_default() } fn get_packaged_locales() -> Vec { 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>) -> Rc { 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) { 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 { 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, ) { 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, ) { 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, ) { 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, ) { 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, _> = 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, // Ownership is transferred here. callback: unsafe extern "C" fn(&xpcom::Promise, *mut FluentBundleRc), } pub struct GeckoFluentBundleAsyncIteratorWrapper(UnboundedSender); #[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, _> = 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::(); 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(); }