Bug 1672317 - [l10nfilesource] part3: Introduce L10nFileSource backed by l10nregistry-rs. r=nika,emilio

Depends on D102102

Differential Revision: https://phabricator.services.mozilla.com/D103002
This commit is contained in:
Zibi Braniecki 2021-07-29 21:30:25 +00:00
parent e0ee86ae33
commit f23494c7e7
21 changed files with 1056 additions and 177 deletions

9
Cargo.lock generated
View File

@ -2656,10 +2656,19 @@ dependencies = [
name = "l10nregistry-ffi"
version = "0.1.0"
dependencies = [
"async-trait",
"cstr",
"fluent",
"futures-channel",
"l10nregistry 0.2.0 (git+https://github.com/zbraniecki/l10nregistry-rs?rev=92d8fbfbbbdffa2047ce01a935a389eb11031f69)",
"libc",
"log",
"moz_task",
"nserror",
"nsstring",
"thin-vec",
"unic-langid",
"xpcom",
]
[[package]]

View File

@ -440,6 +440,11 @@ DOMInterfaces = {
'concrete': False,
},
'L10nFileSource': {
'headerFile': 'mozilla/intl/FileSource.h',
'nativeType': 'mozilla::intl::L10nFileSource',
},
'LegacyMozTCPSocket': {
'headerFile': 'TCPSocket.h',
'wrapperCache': False,

View File

@ -0,0 +1,89 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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/. */
dictionary L10nFileSourceMockFile {
required UTF8String path;
required UTF8String source;
};
enum L10nFileSourceHasFileStatus {
"present",
"missing",
"unknown"
};
/**
* The interface represents a single source location for
* the registry.
*
* Examples of sources are:
* * Toolkit omni.ja Localization Directory
* * Browser omni.ja Localization Directory
* * Language Pack Toolkit Localization Directory
* * Language Pack Browser Localization Directory
*/
[ChromeOnly,
Exposed=Window]
interface L10nFileSource {
/**
* Optional argument `index` can be used to provide a list
* of files available in the source.
*
* This is particularly useful for custom file sources which
* provide a small number of known override files allowing the
* registry to avoid trying I/O on the source for all
* files not available in the source.
*/
[Throws]
constructor(UTF8String name, sequence<UTF8String> locales, UTF8String prePath, optional sequence<UTF8String> index);
/**
* Tests may want to introduce custom file sources and
* register them in a custom `L10nRegistry` to mock
* behavior of using localization with test file sources.
*
* # Example:
*
* ```
* let fs = [
* {
* "path": "/localization/en-US/test.ftl",
* "source": "key = Hello World!",
* }
* ];
* let mockSource = L10nFileSource.createMock("mock", ["en-US"], "/localization/{locale}/", fs);
*
* let reg = new L10nRegistry();
* reg.registerSources([mockSource]);
*
* let loc = new Localization(["test.ftl"], reg);
* let value = await loc.formatValue("key");
* assert(value, "Hello World!");
* ```
*/
[Throws]
static L10nFileSource createMock(UTF8String name, sequence<UTF8String> locales, UTF8String prePath, sequence<L10nFileSourceMockFile> fs);
readonly attribute UTF8String name;
[Pure, Cached, Frozen]
readonly attribute sequence<UTF8String> locales;
/**
* `prePath` is the root path used together with a relative path to construct the full path used to retrieve a file
* out of a file source.
*
* The `prePath` will usually contain a placeholder `{locale}` which gets replaced with a given locale.
*/
readonly attribute UTF8String prePath;
[Pure, Cached, Frozen]
readonly attribute sequence<UTF8String>? index;
[Throws]
L10nFileSourceHasFileStatus hasFile(UTF8String locale, UTF8String path);
[Throws]
Promise<FluentResource?> fetchFile(UTF8String locale, UTF8String path);
[Throws]
FluentResource? fetchFileSync(UTF8String locale, UTF8String path);
};

View File

@ -56,6 +56,7 @@ WEBIDL_FILES = [
"JSProcessActor.webidl",
"JSWindowActor.webidl",
"L10nOverlays.webidl",
"L10nRegistry.webidl",
"MatchGlob.webidl",
"MatchPattern.webidl",
"MediaController.webidl",

203
intl/l10n/FileSource.cpp Normal file
View File

@ -0,0 +1,203 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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 "FileSource.h"
#include "mozilla/dom/Promise.h"
using namespace mozilla::dom;
namespace mozilla::intl {
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(L10nFileSource, mGlobal)
NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(L10nFileSource, AddRef)
NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(L10nFileSource, Release)
L10nFileSource::L10nFileSource(RefPtr<const ffi::FileSource> aRaw,
nsIGlobalObject* aGlobal)
: mGlobal(aGlobal), mRaw(std::move(aRaw)) {}
/* static */
already_AddRefed<L10nFileSource> L10nFileSource::Create(
const nsACString& aName, const nsTArray<nsCString>& aLocales,
const nsACString& aPrePath, ErrorResult& aRv) {
ffi::L10nFileSourceStatus status;
RefPtr<const ffi::FileSource> raw(dont_AddRef(
ffi::l10nfilesource_new(&aName, &aLocales, &aPrePath, &status)));
if (PopulateError(aRv, status)) {
return nullptr;
}
return MakeAndAddRef<L10nFileSource>(std::move(raw));
}
/* static */
already_AddRefed<L10nFileSource> L10nFileSource::Constructor(
const GlobalObject& aGlobal, const nsACString& aName,
const nsTArray<nsCString>& aLocales, const nsACString& aPrePath,
const Optional<Sequence<nsCString>>& aIndex, ErrorResult& aRv) {
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
ffi::L10nFileSourceStatus status;
RefPtr<const ffi::FileSource> raw;
if (aIndex.WasPassed()) {
raw = dont_AddRef(ffi::l10nfilesource_new_with_index(
&aName, &aLocales, &aPrePath, aIndex.Value().Elements(),
aIndex.Value().Length(), &status));
} else {
raw = dont_AddRef(
ffi::l10nfilesource_new(&aName, &aLocales, &aPrePath, &status));
}
if (PopulateError(aRv, status)) {
return nullptr;
}
return MakeAndAddRef<L10nFileSource>(std::move(raw), global);
}
/* static */
already_AddRefed<L10nFileSource> L10nFileSource::CreateMock(
const GlobalObject& aGlobal, const nsACString& aName,
const nsTArray<nsCString>& aLocales, const nsACString& aPrePath,
const nsTArray<L10nFileSourceMockFile>& aFS, ErrorResult& aRv) {
nsTArray<ffi::L10nFileSourceMockFile> fs(aFS.Length());
for (const auto& file : aFS) {
auto f = fs.AppendElement();
f->path = file.mPath;
f->source = file.mSource;
}
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
ffi::L10nFileSourceStatus status;
RefPtr<const ffi::FileSource> raw(dont_AddRef(ffi::l10nfilesource_new_mock(
&aName, &aLocales, &aPrePath, &fs, &status)));
if (PopulateError(aRv, status)) {
return nullptr;
}
return MakeAndAddRef<L10nFileSource>(std::move(raw), global);
}
JSObject* L10nFileSource::WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) {
return L10nFileSource_Binding::Wrap(aCx, this, aGivenProto);
}
void L10nFileSource::GetName(nsCString& aRetVal) {
ffi::l10nfilesource_get_name(mRaw.get(), &aRetVal);
}
void L10nFileSource::GetLocales(nsTArray<nsCString>& aRetVal) {
ffi::l10nfilesource_get_locales(mRaw.get(), &aRetVal);
}
void L10nFileSource::GetPrePath(nsCString& aRetVal) {
ffi::l10nfilesource_get_prepath(mRaw.get(), &aRetVal);
}
void L10nFileSource::GetIndex(Nullable<nsTArray<nsCString>>& aRetVal) {
bool hasIndex =
ffi::l10nfilesource_get_index(mRaw.get(), &aRetVal.SetValue());
if (!hasIndex) {
aRetVal.SetNull();
}
}
L10nFileSourceHasFileStatus L10nFileSource::HasFile(const nsACString& aLocale,
const nsACString& aPath,
ErrorResult& aRv) {
ffi::L10nFileSourceStatus status;
bool isPresent = false;
bool hasValue = ffi::l10nfilesource_has_file(mRaw.get(), &aLocale, &aPath,
&status, &isPresent);
if (!PopulateError(aRv, status) && hasValue) {
if (isPresent) {
return L10nFileSourceHasFileStatus::Present;
}
return L10nFileSourceHasFileStatus::Missing;
}
return L10nFileSourceHasFileStatus::Unknown;
}
already_AddRefed<FluentResource> L10nFileSource::FetchFileSync(
const nsACString& aLocale, const nsACString& aPath, ErrorResult& aRv) {
ffi::L10nFileSourceStatus status;
RefPtr<const ffi::FluentResource> raw =
dont_AddRef(ffi::l10nfilesource_fetch_file_sync(mRaw.get(), &aLocale,
&aPath, &status));
if (!PopulateError(aRv, status) && raw) {
return MakeAndAddRef<FluentResource>(mGlobal, raw);
}
return nullptr;
}
already_AddRefed<Promise> L10nFileSource::FetchFile(const nsACString& aLocale,
const nsACString& aPath,
ErrorResult& aRv) {
RefPtr<Promise> promise = Promise::Create(mGlobal, aRv);
if (aRv.Failed()) {
return nullptr;
}
ffi::L10nFileSourceStatus status;
ffi::l10nfilesource_fetch_file(
mRaw.get(), &aLocale, &aPath, promise,
[](const Promise* aPromise, const ffi::FluentResource* aRes) {
Promise* promise = const_cast<Promise*>(aPromise);
if (aRes) {
nsIGlobalObject* global = promise->GetGlobalObject();
RefPtr<FluentResource> res = new FluentResource(global, aRes);
promise->MaybeResolve(res);
} else {
promise->MaybeResolve(JS::NullHandleValue);
}
},
&status);
if (PopulateError(aRv, status)) {
return nullptr;
}
return promise.forget();
}
/* static */
bool L10nFileSource::PopulateError(ErrorResult& aError,
ffi::L10nFileSourceStatus& aStatus) {
switch (aStatus) {
case ffi::L10nFileSourceStatus::InvalidLocaleCode:
aError.ThrowTypeError("Invalid locale code");
return true;
case ffi::L10nFileSourceStatus::EmptyName:
aError.ThrowTypeError("Name cannot be empty.");
return true;
case ffi::L10nFileSourceStatus::EmptyPrePath:
aError.ThrowTypeError("prePath cannot be empty.");
return true;
case ffi::L10nFileSourceStatus::EmptyResId:
aError.ThrowTypeError("resId cannot be empty.");
return true;
case ffi::L10nFileSourceStatus::None:
return false;
}
MOZ_ASSERT_UNREACHABLE("Unknown status");
return false;
}
} // namespace mozilla::intl

72
intl/l10n/FileSource.h Normal file
View File

@ -0,0 +1,72 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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/. */
#ifndef mozilla_intl_l10n_FileSource_h
#define mozilla_intl_l10n_FileSource_h
#include "nsWrapperCache.h"
#include "mozilla/dom/BindingDeclarations.h"
#include "mozilla/dom/L10nRegistryBinding.h"
#include "mozilla/dom/FluentBinding.h"
#include "mozilla/intl/RegistryBindings.h"
class nsIGlobalObject;
namespace mozilla::intl {
class L10nFileSource : public nsWrapperCache {
public:
NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(L10nFileSource)
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(L10nFileSource)
explicit L10nFileSource(RefPtr<const ffi::FileSource> aRaw,
nsIGlobalObject* aGlobal = nullptr);
static already_AddRefed<L10nFileSource> Create(
const nsACString& aName, const nsTArray<nsCString>& aLocales,
const nsACString& aPrePath, ErrorResult& aRv);
static already_AddRefed<L10nFileSource> Constructor(
const dom::GlobalObject& aGlobal, const nsACString& aName,
const nsTArray<nsCString>& aLocales, const nsACString& aPrePath,
const dom::Optional<dom::Sequence<nsCString>>& aIndex, ErrorResult& aRv);
static already_AddRefed<L10nFileSource> CreateMock(
const dom::GlobalObject& aGlobal, const nsACString& aName,
const nsTArray<nsCString>& aLocales, const nsACString& aPrePath,
const nsTArray<dom::L10nFileSourceMockFile>& aFS, ErrorResult& aRv);
nsIGlobalObject* GetParentObject() const { return mGlobal; }
JSObject* WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) override;
void GetName(nsCString& aRetVal);
void GetLocales(nsTArray<nsCString>& aRetVal);
void GetPrePath(nsCString& aRetVal);
void GetIndex(dom::Nullable<nsTArray<nsCString>>& aRetVal);
dom::L10nFileSourceHasFileStatus HasFile(const nsACString& aLocale,
const nsACString& aPath,
ErrorResult& aRv);
already_AddRefed<dom::Promise> FetchFile(const nsACString& aLocale,
const nsACString& aPath,
ErrorResult& aRv);
already_AddRefed<FluentResource> FetchFileSync(const nsACString& aLocale,
const nsACString& aPath,
ErrorResult& aRv);
protected:
virtual ~L10nFileSource() = default;
nsCOMPtr<nsIGlobalObject> mGlobal;
const RefPtr<const ffi::FileSource> mRaw;
static bool PopulateError(ErrorResult& aError,
ffi::L10nFileSourceStatus& aStatus);
};
} // namespace mozilla::intl
#endif

View File

@ -17,10 +17,13 @@ NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(FluentResource, mParent)
NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(FluentResource, AddRef)
NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(FluentResource, Release)
FluentResource::FluentResource(nsISupports* aParent,
const ffi::FluentResource* aRaw)
: mParent(aParent), mRaw(std::move(aRaw)), mHasErrors(false) {}
FluentResource::FluentResource(nsISupports* aParent, const nsACString& aSource)
: mParent(aParent),
mRaw(dont_AddRef(ffi::fluent_resource_new(&aSource, &mHasErrors))) {
MOZ_COUNT_CTOR(FluentResource);
: mParent(aParent), mHasErrors(false) {
mRaw = dont_AddRef(ffi::fluent_resource_new(&aSource, &mHasErrors));
}
already_AddRefed<FluentResource> FluentResource::Constructor(
@ -41,7 +44,5 @@ JSObject* FluentResource::WrapObject(JSContext* aCx,
return FluentResource_Binding::Wrap(aCx, this, aGivenProto);
}
FluentResource::~FluentResource() { MOZ_COUNT_DTOR(FluentResource); };
} // namespace intl
} // namespace mozilla

View File

@ -21,7 +21,8 @@ class FluentResource : public nsWrapperCache {
NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(FluentResource)
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(FluentResource)
explicit FluentResource(nsISupports* aParent, const nsACString& aSource);
FluentResource(nsISupports* aParent, const ffi::FluentResource* aRaw);
FluentResource(nsISupports* aParent, const nsACString& aSource);
static already_AddRefed<FluentResource> Constructor(
const dom::GlobalObject& aGlobal, const nsACString& aSource);
@ -33,10 +34,10 @@ class FluentResource : public nsWrapperCache {
const ffi::FluentResource* Raw() const { return mRaw; }
protected:
virtual ~FluentResource();
virtual ~FluentResource() = default;
nsCOMPtr<nsISupports> mParent;
const RefPtr<const ffi::FluentResource> mRaw;
RefPtr<const ffi::FluentResource> mRaw;
bool mHasErrors;
};

View File

@ -13,63 +13,18 @@
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<char*>(const_cast<uint8_t*>(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) {
nsIStreamLoaderObserver* aObserver) {
nsCOMPtr<nsIURI> uri;
nsresult rv = NS_NewURI(getter_AddRefs(uri), aPath);
NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_TRUE(uri, NS_ERROR_INVALID_ARG);
RefPtr<ResourceLoader> listener =
MakeRefPtr<ResourceLoader>(aCallback, aClosure);
// TODO: What is the lifetime requirement for loader?
RefPtr<nsIStreamLoader> loader;
rv = NS_NewStreamLoader(
getter_AddRefs(loader), uri, listener,
getter_AddRefs(loader), uri, aObserver,
nsContentUtils::GetSystemPrincipal(),
nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
nsIContentPolicy::TYPE_OTHER);
@ -77,7 +32,8 @@ class L10nRegistry {
return rv;
}
static nsresult LoadSync(const nsACString& aPath, nsACString& aRetVal) {
static nsresult LoadSync(const nsACString& aPath, void** aData,
uint64_t* aSize) {
nsCOMPtr<nsIURI> uri;
nsresult rv = NS_NewURI(getter_AddRefs(uri), aPath);
@ -87,7 +43,9 @@ class L10nRegistry {
auto result = URLPreloader::ReadURI(uri);
if (result.isOk()) {
aRetVal = result.unwrap();
auto uri = result.unwrap();
*aData = ToNewCString(uri);
*aSize = uri.Length();
return NS_OK;
}
@ -104,14 +62,14 @@ class L10nRegistry {
nullptr, /* aPerformanceStorage */
nullptr, /* aLoadGroup */
nullptr, /* aCallbacks */
nsIRequest::LOAD_BACKGROUND);
nsIRequest::LOAD_NORMAL);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIInputStream> input;
rv = channel->Open(getter_AddRefs(input));
NS_ENSURE_SUCCESS(rv, NS_ERROR_INVALID_ARG);
return NS_ReadInputStreamToString(input, aRetVal, -1);
return NS_ReadInputStreamToBuffer(input, aData, -1, aSize);
}
};
@ -120,16 +78,22 @@ class L10nRegistry {
extern "C" {
nsresult L10nRegistryLoad(const nsACString* aPath,
mozilla::intl::ResourceLoader::Callback aCallback,
void* aClosure) {
if (!aPath || !aCallback) {
const nsIStreamLoaderObserver* aObserver) {
if (!aPath || !aObserver) {
return NS_ERROR_INVALID_ARG;
}
return mozilla::intl::L10nRegistry::Load(*aPath, aCallback, aClosure);
return mozilla::intl::L10nRegistry::Load(
*aPath, const_cast<nsIStreamLoaderObserver*>(aObserver));
}
nsresult L10nRegistryLoadSync(const nsACString* aPath, nsACString* aRetVal) {
return mozilla::intl::L10nRegistry::LoadSync(*aPath, *aRetVal);
}
nsresult L10nRegistryLoadSync(const nsACString* aPath, void** aData,
uint64_t* aSize) {
if (!aPath || !aData || !aSize) {
return NS_ERROR_INVALID_ARG;
}
return mozilla::intl::L10nRegistry::LoadSync(*aPath, aData, aSize);
}
} // extern "C"

View File

@ -0,0 +1,26 @@
/* 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/. */
#ifndef mozilla_intl_l10n_RegistryBindings_h
#define mozilla_intl_l10n_RegistryBindings_h
#include "mozilla/intl/l10nregistry_ffi_generated.h"
#include "mozilla/RefPtr.h"
namespace mozilla {
template <>
struct RefPtrTraits<intl::ffi::FileSource> {
static void AddRef(const intl::ffi::FileSource* aPtr) {
intl::ffi::l10nfilesource_addref(aPtr);
}
static void Release(const intl::ffi::FileSource* aPtr) {
intl::ffi::l10nfilesource_release(aPtr);
}
};
} // namespace mozilla
#endif

View File

@ -5,13 +5,16 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
EXPORTS.mozilla.intl += [
"FileSource.h",
"FluentBindings.h",
"FluentBundle.h",
"FluentResource.h",
"Localization.h",
"RegistryBindings.h",
]
UNIFIED_SOURCES += [
"FileSource.cpp",
"FluentBundle.cpp",
"FluentResource.cpp",
"L10nRegistry.cpp",
@ -45,9 +48,13 @@ USE_LIBS += ["intlcomponents"]
if CONFIG["COMPILE_ENVIRONMENT"]:
CbindgenHeader("fluent_ffi_generated.h", inputs=["/intl/l10n/rust/fluent-ffi"])
CbindgenHeader(
"l10nregistry_ffi_generated.h", inputs=["/intl/l10n/rust/l10nregistry-ffi"]
)
EXPORTS.mozilla.intl += [
"!fluent_ffi_generated.h",
"!l10nregistry_ffi_generated.h",
]
XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell.ini"]

View File

@ -16,7 +16,6 @@ namespaces = ["mozilla", "intl", "ffi"]
[parse]
parse_deps = true
include = ["fluent", "fluent-bundle", "intl-memoizer"]
exclude = ["fxhash"]
[enum]
derive_helper_methods = true

View File

@ -4,8 +4,10 @@
pub use fluent::FluentResource;
use nsstring::nsACString;
use std::mem;
use std::rc::Rc;
use std::{
mem::{self, ManuallyDrop},
rc::Rc,
};
#[no_mangle]
pub extern "C" fn fluent_resource_new(
@ -27,12 +29,11 @@ pub extern "C" fn fluent_resource_new(
#[no_mangle]
pub unsafe extern "C" fn fluent_resource_addref(res: &FluentResource) {
let raw = Rc::from_raw(res);
let raw = ManuallyDrop::new(Rc::from_raw(res));
mem::forget(Rc::clone(&raw));
mem::forget(raw);
}
#[no_mangle]
pub unsafe extern "C" fn fluent_resource_release(res: &FluentResource) {
pub unsafe extern "C" fn fluent_resource_release(res: *const FluentResource) {
let _ = Rc::from_raw(res);
}

View File

@ -2,7 +2,7 @@
* 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 l10nregistry_ffi::load::{load_async, load_sync};
use moz_task;
use std::{
sync::atomic::{AtomicBool, Ordering::Relaxed},

View File

@ -7,5 +7,14 @@ edition = "2018"
[dependencies]
futures-channel = "0.3"
libc = "0.2"
cstr = "0.2"
log = "0.4"
nserror = { path = "../../../../xpcom/rust/nserror" }
nsstring = { path = "../../../../xpcom/rust/nsstring" }
l10nregistry = { git = "https://github.com/zbraniecki/l10nregistry-rs", rev = "92d8fbfbbbdffa2047ce01a935a389eb11031f69" }
fluent = { version = "0.15.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" }

View File

@ -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", "l10nregistry"]
[enum]
derive_helper_methods = true
[export.rename]
"ThinVec" = "nsTArray"
"Promise" = "dom::Promise"

View File

@ -0,0 +1,88 @@
/* 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 l10nregistry::{env::ErrorReporter, errors::L10nRegistryError};
use log::warn;
use nserror::{nsresult, NS_ERROR_NOT_AVAILABLE};
use nsstring::nsString;
use std::{
ffi::CStr,
fmt::{self, Write},
};
use xpcom::interfaces;
#[derive(Clone)]
pub struct GeckoEnvironment;
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 { path, loc, error } => log_simple_console_error(
&error.to_string(),
cstr!("l10n"),
false,
true,
Some(nsString::from(&path)),
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(),
cstr!("l10n"),
false,
true,
None,
(0, 0),
interfaces::nsIScriptError::warningFlag as u32,
),
};
if let Err(err) = result {
warn!("Error while reporting an error: {}", err);
}
}
}
}
fn log_simple_console_error(
error: &impl fmt::Display,
category: &CStr,
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)?;
unsafe {
script_error
.Init(
&*error_str,
&*path.unwrap_or(nsString::new()), /* aSourceName */
&*nsString::new(), /* aSourceLine */
pos.0, /* aLineNumber */
pos.1, /* aColNumber */
error_flags,
category.as_ptr(),
from_private_window,
from_chrome_context,
)
.to_result()?;
console_service.LogMessage(&**script_error).to_result()?;
}
Ok(())
}

View File

@ -0,0 +1,53 @@
/* 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;
use std::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()))
}
#[async_trait::async_trait(?Send)]
impl FileFetcher for GeckoFileFetcher {
fn fetch_sync(&self, path: &str) -> io::Result<String> {
crate::load::load_sync(path).and_then(try_string_from_box_u8)
}
async fn fetch(&self, path: &str) -> io::Result<String> {
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, path: &str) -> io::Result<String> {
for (p, source) in &self.fs {
if p == path {
return Ok(source.clone());
}
}
Err(io::Error::new(io::ErrorKind::NotFound, "File not found"))
}
async fn fetch(&self, path: &str) -> io::Result<String> {
self.fetch_sync(path)
}
}

View File

@ -2,107 +2,7 @@
* 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<extern "C" fn(*mut c_void, *mut nsACString, nsresult)>,
closure: *mut c_void,
) -> io::Result<()> {
extern "C" {
fn L10nRegistryLoad(
aPath: *const nsACString,
callback: Option<extern "C" fn(*mut c_void, *mut nsACString, nsresult)>,
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<dyn FnOnce(io::Result<&mut nsACString>)>,
}
impl CallbackClosure {
fn new<CB>(cb: CB) -> *mut Self
where
CB: FnOnce(io::Result<&mut nsACString>) + 'static,
{
let boxed = Box::new(Self {
cb: Box::new(cb) as Box<dyn FnOnce(io::Result<&mut nsACString>)>,
});
Box::into_raw(boxed)
}
fn call(self: Box<Self>, 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<nsCString> {
let (sender, receiver) = channel::<io::Result<nsCString>>();
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<nsCString> {
let mut result = nsCString::new();
ffi::load_sync(&*path.adapt(), &mut result)?;
Ok(result)
}
mod env;
mod fetcher;
pub mod load;
mod source;

View File

@ -0,0 +1,115 @@
/* 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))
}
}
#[derive(xpcom)]
#[xpimplements(nsIStreamLoaderObserver)]
#[refcnt = "nonatomic"]
struct InitStreamLoaderObserver {
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,
))
}
}

View File

@ -0,0 +1,310 @@
/* 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, ResourceStatus};
use nsstring::{nsACString, nsCString};
use thin_vec::ThinVec;
use unic_langid::LanguageIdentifier;
use std::{mem, rc::Rc};
use xpcom::RefPtr;
#[repr(C)]
pub enum L10nFileSourceStatus {
None,
EmptyName,
EmptyPrePath,
EmptyResId,
InvalidLocaleCode,
}
#[no_mangle]
pub extern "C" fn l10nfilesource_new(
name: &nsACString,
locales: &ThinVec<nsCString>,
pre_path: &nsACString,
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| 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(),
locales,
pre_path.to_string(),
Default::default(),
GeckoFileFetcher,
);
source.set_reporter(GeckoEnvironment);
*status = L10nFileSourceStatus::None;
Rc::into_raw(Rc::new(source))
}
#[no_mangle]
pub unsafe extern "C" fn l10nfilesource_new_with_index(
name: &nsACString,
locales: &ThinVec<nsCString>,
pre_path: &nsACString,
index_elements: *const nsCString,
index_length: usize,
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| 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(),
locales,
pre_path.to_string(),
Default::default(),
GeckoFileFetcher,
index,
);
source.set_reporter(GeckoEnvironment);
*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,
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| 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(), locales, pre_path.to_string(), Default::default(), fetcher);
source.set_reporter(GeckoEnvironment);
*status = L10nFileSourceStatus::None;
Rc::into_raw(Rc::new(source))
}
#[no_mangle]
pub unsafe extern "C" fn l10nfilesource_addref(source: &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_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()) {
*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;
if let Some(res) = source.fetch_file_sync(&locale, &path.to_utf8(), false) {
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();
match source.fetch_file(&locale, &path) {
ResourceStatus::Missing => callback(promise, None),
ResourceStatus::Loaded(res) => callback(promise, Some(&res)),
res @ ResourceStatus::Loading(_) => {
let strong_promise = RefPtr::new(promise);
moz_task::spawn_current_thread(async move {
callback(&strong_promise, res.await.as_ref().map(|r| &**r));
})
.expect("Failed to spawn future");
}
}
}