diff --git a/Cargo.lock b/Cargo.lock index 60b2ad45d436..39eaca994cf3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1950,6 +1950,7 @@ dependencies = [ "fog-gtest", "gecko-fuzz-targets", "gkrust-shared", + "l10nregistry-ffi-gtest", "moz_task-gtest", "mozglue-static", "mp4parse-gtest", @@ -1989,6 +1990,7 @@ dependencies = [ "http_sfv", "jsrust_shared", "kvstore", + "l10nregistry-ffi", "lmdb-rkv-sys", "log", "mapped_hyph", @@ -2606,6 +2608,24 @@ dependencies = [ "xpcom", ] +[[package]] +name = "l10nregistry-ffi" +version = "0.1.0" +dependencies = [ + "futures-channel", + "libc", + "nserror", + "nsstring", +] + +[[package]] +name = "l10nregistry-ffi-gtest" +version = "0.1.0" +dependencies = [ + "l10nregistry-ffi", + "moz_task", +] + [[package]] name = "lazy_static" version = "1.4.0" diff --git a/intl/l10n/L10nRegistry.cpp b/intl/l10n/L10nRegistry.cpp new file mode 100644 index 000000000000..d3818d87a012 --- /dev/null +++ b/intl/l10n/L10nRegistry.cpp @@ -0,0 +1,134 @@ +/* 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/. */ + +#include "mozilla/RefPtr.h" +#include "mozilla/URLPreloader.h" +#include "nsIChannel.h" +#include "nsILoadInfo.h" +#include "nsIStreamLoader.h" +#include "nsString.h" + +namespace mozilla { +namespace intl { + +class ResourceLoader final : public nsIStreamLoaderObserver { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSISTREAMLOADEROBSERVER + + typedef nsresult (*Callback)(void* aClosure, nsACString* aString, + nsresult aSuccess); + + ResourceLoader(Callback aCallback, void* aClosure) + : mCallback(aCallback), mClosure(aClosure) {} + + protected: + ~ResourceLoader() = default; + + private: + Callback mCallback; + void* mClosure; +}; + +NS_IMPL_ISUPPORTS(ResourceLoader, nsIStreamLoaderObserver) + +// nsIStreamLoaderObserver +NS_IMETHODIMP +ResourceLoader::OnStreamComplete(nsIStreamLoader* aLoader, + nsISupports* aContext, nsresult aStatus, + uint32_t aStringLen, const uint8_t* aString) { + MOZ_ASSERT(NS_IsMainThread()); + + if (NS_FAILED(aStatus)) { + mCallback(mClosure, nullptr, aStatus); + return NS_OK; + } + + nsCString data; + data.Adopt(reinterpret_cast(const_cast(aString)), + aStringLen); + mCallback(mClosure, &data, NS_OK); + + return NS_SUCCESS_ADOPTED_DATA; +} + +class L10nRegistry { + public: + static nsresult Load(const nsACString& aPath, + ResourceLoader::Callback aCallback, void* aClosure) { + nsCOMPtr uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), aPath); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(uri, NS_ERROR_INVALID_ARG); + + RefPtr listener = + MakeRefPtr(aCallback, aClosure); + + // TODO: What is the lifetime requirement for loader? + RefPtr loader; + rv = NS_NewStreamLoader( + getter_AddRefs(loader), uri, listener, + nsContentUtils::GetSystemPrincipal(), + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_OTHER); + + return rv; + } + + static nsresult LoadSync(const nsACString& aPath, nsACString& aRetVal) { + nsCOMPtr uri; + + nsresult rv = NS_NewURI(getter_AddRefs(uri), aPath); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ENSURE_TRUE(uri, NS_ERROR_INVALID_ARG); + + auto result = URLPreloader::ReadURI(uri); + if (result.isOk()) { + aRetVal = result.unwrap(); + return NS_OK; + } + + auto err = result.unwrapErr(); + if (err != NS_ERROR_INVALID_ARG && err != NS_ERROR_NOT_INITIALIZED) { + return err; + } + + nsCOMPtr channel; + rv = NS_NewChannel( + getter_AddRefs(channel), uri, nsContentUtils::GetSystemPrincipal(), + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_OTHER, nullptr, /* nsICookieJarSettings */ + nullptr, /* aPerformanceStorage */ + nullptr, /* aLoadGroup */ + nullptr, /* aCallbacks */ + nsIRequest::LOAD_BACKGROUND); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr input; + rv = channel->Open(getter_AddRefs(input)); + NS_ENSURE_SUCCESS(rv, NS_ERROR_INVALID_ARG); + + return NS_ReadInputStreamToString(input, aRetVal, -1); + } +}; + +} // namespace intl +} // namespace mozilla + +extern "C" { +nsresult L10nRegistryLoad(const nsACString* aPath, + mozilla::intl::ResourceLoader::Callback aCallback, + void* aClosure) { + if (!aPath || !aCallback) { + return NS_ERROR_INVALID_ARG; + } + + return mozilla::intl::L10nRegistry::Load(*aPath, aCallback, aClosure); +} + +nsresult L10nRegistryLoadSync(const nsACString* aPath, nsACString* aRetVal) { + return mozilla::intl::L10nRegistry::LoadSync(*aPath, *aRetVal); +} +} diff --git a/intl/l10n/moz.build b/intl/l10n/moz.build index 87740c574008..da8f1cfc1663 100644 --- a/intl/l10n/moz.build +++ b/intl/l10n/moz.build @@ -14,6 +14,7 @@ EXPORTS.mozilla.intl += [ UNIFIED_SOURCES += [ 'FluentBundle.cpp', 'FluentResource.cpp', + 'L10nRegistry.cpp', 'Localization.cpp', ] @@ -26,6 +27,10 @@ TESTING_JS_MODULES += [ 'FluentSyntax.jsm', ] +TEST_DIRS += [ + 'rust/gtest', +] + XPIDL_SOURCES += [ 'mozILocalization.idl', ] diff --git a/intl/l10n/rust/gtest/Cargo.toml b/intl/l10n/rust/gtest/Cargo.toml new file mode 100644 index 000000000000..f53d7413d7ba --- /dev/null +++ b/intl/l10n/rust/gtest/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "l10nregistry-ffi-gtest" +version = "0.1.0" +authors = ["nobody@mozilla.org"] +license = "MPL-2.0" +description = "Tests for rust bindings to l10nRegistry" +edition = "2018" + +[dependencies] +l10nregistry-ffi = { path = "../l10nregistry-ffi" } +moz_task = { path = "../../../../xpcom/rust/moz_task" } + +[lib] +path = "test.rs" diff --git a/intl/l10n/rust/gtest/Test.cpp b/intl/l10n/rust/gtest/Test.cpp new file mode 100644 index 000000000000..98e7a8b5c680 --- /dev/null +++ b/intl/l10n/rust/gtest/Test.cpp @@ -0,0 +1,23 @@ +/* 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/. */ + +#include "gtest/gtest.h" + +extern "C" void Rust_L10NLoadAsync(bool* aItWorked); + +TEST(RustL10N, LoadAsync) +{ + bool itWorked = false; + Rust_L10NLoadAsync(&itWorked); + EXPECT_TRUE(itWorked); +} + +extern "C" void Rust_L10NLoadSync(bool* aItWorked); + +TEST(RustL10N, LoadSync) +{ + bool itWorked = false; + Rust_L10NLoadSync(&itWorked); + EXPECT_TRUE(itWorked); +} diff --git a/intl/l10n/rust/gtest/moz.build b/intl/l10n/rust/gtest/moz.build new file mode 100644 index 000000000000..a71b43e3f173 --- /dev/null +++ b/intl/l10n/rust/gtest/moz.build @@ -0,0 +1,11 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +UNIFIED_SOURCES += [ + 'Test.cpp', +] + +FINAL_LIBRARY = 'xul-gtest' diff --git a/intl/l10n/rust/gtest/test.rs b/intl/l10n/rust/gtest/test.rs new file mode 100644 index 000000000000..072cd932f8b4 --- /dev/null +++ b/intl/l10n/rust/gtest/test.rs @@ -0,0 +1,55 @@ +/* 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 l10nregistry_ffi::{load_async, load_sync}; +use moz_task; +use std::{ + sync::atomic::{AtomicBool, Ordering::Relaxed}, + sync::Arc, +}; + +#[no_mangle] +pub extern "C" fn Rust_L10NLoadAsync(it_worked: *mut bool) { + let done = Arc::new(AtomicBool::new(false)); + let done2 = done.clone(); + + moz_task::spawn_current_thread(async move { + match load_async("resource://gre/localization/en-US/toolkit/about/aboutAbout.ftl").await { + Ok(res) => { + assert_eq!(res.len(), 460); + assert!(res.starts_with( + b"# This Source Code Form is subject to the terms of the Mozilla Public" + )); + unsafe { + *it_worked = true; + } + } + Err(err) => println!("{:?}", err), + } + + done.store(true, Relaxed); + }) + .unwrap(); + + unsafe { + moz_task::gtest_only::spin_event_loop_until(move || done2.load(Relaxed)).unwrap(); + *it_worked = true; + } +} + +#[no_mangle] +pub extern "C" fn Rust_L10NLoadSync(it_worked: *mut bool) { + match load_sync("resource://gre/localization/en-US/toolkit/about/aboutAbout.ftl") { + Ok(res) => { + assert_eq!(res.len(), 460); + assert!(res.starts_with( + b"# This Source Code Form is subject to the terms of the Mozilla Public" + )); + unsafe { + *it_worked = true; + } + } + Err(err) => println!("{:?}", err), + } +} diff --git a/intl/l10n/rust/l10nregistry-ffi/Cargo.toml b/intl/l10n/rust/l10nregistry-ffi/Cargo.toml new file mode 100644 index 000000000000..0e2d25977ccd --- /dev/null +++ b/intl/l10n/rust/l10nregistry-ffi/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "l10nregistry-ffi" +version = "0.1.0" +authors = ["nobody@mozilla.org"] +edition = "2018" + +[dependencies] +futures-channel = "0.3" +libc = "0.2" +nserror = { path = "../../../../xpcom/rust/nserror" } +nsstring = { path = "../../../../xpcom/rust/nsstring" } 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 000000000000..25f2ec4fe548 --- /dev/null +++ b/intl/l10n/rust/l10nregistry-ffi/src/lib.rs @@ -0,0 +1,108 @@ +/* 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::channel; +use libc::c_void; +use nserror::nsresult; +use nsstring::{nsACString, nsCString, nsCStringLike}; +use std::io::{self, Error, ErrorKind}; + +trait ToIoResult { + fn to_io_result(self) -> io::Result<()>; +} + +impl ToIoResult for nsresult { + fn to_io_result(self) -> io::Result<()> { + if self.failed() { + return Err(Error::new(ErrorKind::Other, self)); + } else { + Ok(()) + } + } +} + +mod ffi { + use super::*; + + pub fn load( + path: &nsACString, + callback: Option, + closure: *mut c_void, + ) -> io::Result<()> { + extern "C" { + fn L10nRegistryLoad( + aPath: *const nsACString, + callback: Option, + closure: *mut c_void, + ) -> nsresult; + } + unsafe { L10nRegistryLoad(path, callback, closure) }.to_io_result() + } + + pub fn load_sync(path: &nsACString, result: &mut nsACString) -> io::Result<()> { + extern "C" { + fn L10nRegistryLoadSync(aPath: *const nsACString, aRetVal: *mut nsACString) -> nsresult; + } + unsafe { L10nRegistryLoadSync(path, result) }.to_io_result() + } + + /// Swap the memory to take ownership of the string data + #[allow(non_snake_case)] + pub fn take_nsACString(s: &mut nsACString) -> nsCString { + let mut result = nsCString::new(); + result.take_from(s); + result + } +} + +struct CallbackClosure { + cb: Box)>, +} + +impl CallbackClosure { + fn new(cb: CB) -> *mut Self + where + CB: FnOnce(io::Result<&mut nsACString>) + 'static, + { + let boxed = Box::new(Self { + cb: Box::new(cb) as Box)>, + }); + Box::into_raw(boxed) + } + + fn call(self: Box, s: io::Result<&mut nsACString>) { + (self.cb)(s) + } +} + +extern "C" fn load_async_cb(closure: *mut c_void, string: *mut nsACString, success: nsresult) { + let result = success.to_io_result().map(|_| unsafe { &mut *string }); + let closure = unsafe { + debug_assert_ne!(closure, std::ptr::null_mut()); + Box::from_raw(closure as *mut CallbackClosure) + }; + closure.call(result); +} + +pub async fn load_async(path: impl nsCStringLike) -> io::Result { + let (sender, receiver) = channel::>(); + + let closure = CallbackClosure::new(move |result| { + let result = result.map(ffi::take_nsACString); + sender.send(result).expect("Failed to send result"); + }); + + ffi::load(&*path.adapt(), Some(load_async_cb), closure as *mut c_void)?; + + let result = receiver + .await + .map_err(|_| Error::new(ErrorKind::Interrupted, "canceled"))?; + result +} + +pub fn load_sync(path: impl nsCStringLike) -> io::Result { + let mut result = nsCString::new(); + ffi::load_sync(&*path.adapt(), &mut result)?; + Ok(result) +} diff --git a/toolkit/library/gtest/rust/Cargo.toml b/toolkit/library/gtest/rust/Cargo.toml index 1dd6461b7f9a..e8e4116a9a0e 100644 --- a/toolkit/library/gtest/rust/Cargo.toml +++ b/toolkit/library/gtest/rust/Cargo.toml @@ -38,6 +38,7 @@ with_dbus = ["gkrust-shared/with_dbus"] [dependencies] bench-collections-gtest = { path = "../../../../xpcom/rust/gtest/bench-collections" } +l10nregistry-ffi-gtest = { path = "../../../../intl/l10n/rust/gtest" } moz_task-gtest = { path = "../../../../xpcom/rust/gtest/moz_task" } mp4parse-gtest = { path = "../../../../dom/media/gtest" } nsstring-gtest = { path = "../../../../xpcom/rust/gtest/nsstring" } diff --git a/toolkit/library/gtest/rust/lib.rs b/toolkit/library/gtest/rust/lib.rs index efdcdee1e21d..d9a50bc5583e 100644 --- a/toolkit/library/gtest/rust/lib.rs +++ b/toolkit/library/gtest/rust/lib.rs @@ -8,6 +8,7 @@ extern crate fog_gtest; #[cfg(feature = "libfuzzer")] extern crate gecko_fuzz_targets; extern crate gkrust_shared; +extern crate l10nregistry_ffi_gtest; extern crate moz_task_gtest; extern crate mp4parse_gtest; extern crate nsstring_gtest; diff --git a/toolkit/library/rust/shared/Cargo.toml b/toolkit/library/rust/shared/Cargo.toml index b77d9f2ba616..61fb9e925bb8 100644 --- a/toolkit/library/rust/shared/Cargo.toml +++ b/toolkit/library/rust/shared/Cargo.toml @@ -65,6 +65,7 @@ rusqlite = { version = "0.24.1", features = ["modern_sqlite", "in_gecko"] } fluent = { version = "0.13.1", features = ["fluent-pseudo"] } fluent-ffi = { path = "../../../../intl/l10n/rust/fluent-ffi" } firefox-accounts-bridge = { path = "../../../../services/fxaccounts/rust-bridge/firefox-accounts-bridge", optional=true } +l10nregistry-ffi = { path = "../../../../intl/l10n/rust/l10nregistry-ffi" } processtools = { path = "../../../components/processtools" } qcms = { path = "../../../../gfx/qcms" } diff --git a/toolkit/library/rust/shared/lib.rs b/toolkit/library/rust/shared/lib.rs index 8cd1911d644a..fd9ff0121966 100644 --- a/toolkit/library/rust/shared/lib.rs +++ b/toolkit/library/rust/shared/lib.rs @@ -34,6 +34,7 @@ extern crate gkrust_utils; extern crate http_sfv; extern crate jsrust_shared; extern crate kvstore; +extern crate l10nregistry_ffi; extern crate mapped_hyph; extern crate mozurl; extern crate mp4parse_capi;