use anyhow::{Context, Result};
use std::collections::BTreeMap;
use std::{ffi, fs, io, mem, str};
use tracing::error;
use {
intl_model as model, rust_icu_common as ucommon, rust_icu_sys as usys, rust_icu_uloc as uloc,
};
pub(crate) const ASSETS_DIR: &str = "/pkg/data/assets/locales";
#[repr(i8)]
#[derive(Debug, PartialEq)]
pub enum LookupStatus {
OK = 0,
Unavailable = 1,
ArgumentError = 2,
Internal = 111,
}
trait CApi {
fn string(&self, message_id: u64) -> Result<&ffi::CStr, LookupStatus>;
}
impl From<str::Utf8Error> for LookupStatus {
fn from(e: str::Utf8Error) -> Self {
error!("intl: utf-8: {:?}", e);
LookupStatus::Unavailable
}
}
impl From<anyhow::Error> for LookupStatus {
fn from(e: anyhow::Error) -> Self {
error!("intl: general: {:?}", e);
LookupStatus::Internal
}
}
impl From<ucommon::Error> for LookupStatus {
fn from(e: ucommon::Error) -> Self {
error!("intl: icu: {:?}", e);
LookupStatus::Internal
}
}
pub struct FakeLookup {
hello: ffi::CString,
hello_person: ffi::CString,
}
impl FakeLookup {
pub fn new() -> FakeLookup {
let hello =
ffi::CString::new("Hello world!").expect("CString from known value should never fail");
let hello_person = ffi::CString::new("Hello {person}!")
.expect("CString from known value should never fail");
FakeLookup { hello, hello_person }
}
}
impl CApi for FakeLookup {
fn string(&self, message_id: u64) -> Result<&ffi::CStr, LookupStatus> {
if message_id == 1 {
return Ok(self.hello_person.as_c_str());
}
match message_id % 2 == 0 {
true => Ok(self.hello.as_c_str()),
false => Err(LookupStatus::Unavailable),
}
}
}
#[allow(clippy::missing_safety_doc)] #[no_mangle]
pub unsafe extern "C" fn intl_lookup_new_fake_for_test(
len: libc::size_t,
array: *mut *const libc::c_char,
status: *mut i8,
) -> *const FakeLookup {
*status = LookupStatus::OK as i8;
let rsize = len as usize;
let input: Vec<*const libc::c_char> = Vec::from_raw_parts(array, rsize, rsize);
let input = mem::ManuallyDrop::new(input);
for raw in input.iter() {
let cstr = ffi::CStr::from_ptr(*raw).to_str().expect("not a valid UTF-8");
if cstr == "en-US" {
*status = LookupStatus::Unavailable as i8;
return std::ptr::null::<FakeLookup>();
}
}
Box::into_raw(Box::new(FakeLookup::new()))
}
#[allow(clippy::missing_safety_doc)] #[no_mangle]
pub unsafe extern "C" fn intl_lookup_delete_fake_for_test(this: *mut FakeLookup) {
generic_delete(this);
}
#[allow(clippy::missing_safety_doc)] #[no_mangle]
pub unsafe extern "C" fn intl_lookup_new(
len: libc::size_t,
array: *mut *const libc::c_char,
status: *mut i8,
) -> *const Lookup {
*status = LookupStatus::OK as i8;
let rsize = len as usize;
let input: Vec<*const libc::c_char> = Vec::from_raw_parts(array, rsize, rsize);
let input = mem::ManuallyDrop::new(input);
let mut locales = vec![];
for raw in input.iter() {
let cstr = ffi::CStr::from_ptr(*raw).to_str();
match cstr {
Err(e) => {
error!("intl::intl_lookup_new::c_str: {:?}", &e);
let ls: LookupStatus = e.into();
*status = ls as i8;
return std::ptr::null::<Lookup>();
}
Ok(s) => {
locales.push(s);
}
}
}
let data = icu_data::Loader::new().expect("icu data loaded");
let lookup_or = Lookup::new(data, &locales[..]);
match lookup_or {
Ok(lookup) => Box::into_raw(Box::new(lookup)),
Err(e) => {
error!("intl::intl_lookup_new: {:?}", &e);
let ls: LookupStatus = e.into();
*status = ls as i8;
std::ptr::null::<Lookup>()
}
}
}
#[allow(clippy::missing_safety_doc)] #[no_mangle]
pub unsafe extern "C" fn intl_lookup_delete(instance: *mut Lookup) {
generic_delete(instance);
}
#[allow(clippy::missing_safety_doc)] #[no_mangle]
pub unsafe extern "C" fn intl_lookup_string_fake_for_test(
this: *const FakeLookup,
id: u64,
status: *mut i8,
) -> *const libc::c_char {
generic_string(this, id, status)
}
unsafe fn generic_string<T: CApi>(this: *const T, id: u64, status: *mut i8) -> *const libc::c_char {
*status = LookupStatus::OK as i8;
match this.as_ref().unwrap().string(id) {
Err(e) => {
*status = e as i8;
std::ptr::null()
}
Ok(s) => s.as_ptr() as *const libc::c_char,
}
}
unsafe fn generic_delete<T>(instance: *mut T) {
let _ = Box::from_raw(instance);
}
#[allow(clippy::missing_safety_doc)] #[no_mangle]
pub unsafe extern "C" fn intl_lookup_string(
this: *const Lookup,
id: u64,
status: *mut i8,
) -> *const libc::c_char {
*status = LookupStatus::OK as i8;
match this.as_ref().unwrap().string(id) {
Err(e) => {
*status = e as i8;
std::ptr::null()
}
Ok(s) => s.as_ptr() as *const libc::c_char,
}
}
pub struct Catalog {
locale_to_message: BTreeMap<String, BTreeMap<u64, ffi::CString>>,
}
impl Catalog {
fn new() -> Catalog {
let locale_to_message = BTreeMap::new();
Catalog { locale_to_message }
}
fn add(&mut self, model: model::Model) -> Result<()> {
let locale_id = model.locale();
let mut messages: BTreeMap<u64, ffi::CString> = BTreeMap::new();
for (id, msg) in model.messages() {
let c_msg = ffi::CString::new(msg.clone())
.with_context(|| format!("interior NUL in {:?}", msg))?;
messages.insert(*id, c_msg);
}
self.locale_to_message.insert(locale_id.to_string(), messages);
Ok(())
}
fn get(&self, locale: &str, id: u64) -> Option<&ffi::CStr> {
self.locale_to_message
.get(locale)
.map(|messages| messages.get(&id))
.flatten()
.map(|cstring| cstring.as_c_str())
}
}
pub struct Lookup {
requested: Vec<uloc::ULoc>,
catalog: Catalog,
#[allow(dead_code)]
icu_data: icu_data::Loader,
}
impl Lookup {
pub fn new(icu_data: icu_data::Loader, requested: &[&str]) -> Result<Lookup> {
let supported_locales =
Lookup::get_available_locales().with_context(|| "while creating Lookup")?;
let catalog = Lookup::load_locales(&supported_locales[..])
.with_context(|| "while loading locales")?;
Lookup::new_internal(icu_data, requested, &supported_locales, catalog)
}
fn load_locales(supported: &[impl AsRef<str>]) -> Result<Catalog> {
let mut catalog = Catalog::new();
for locale in supported {
let mut locale_dir_path = std::path::PathBuf::from(ASSETS_DIR);
locale_dir_path.push(locale.as_ref());
let locale_dir = std::fs::read_dir(&locale_dir_path)
.with_context(|| format!("while reading {:?}", &locale_dir_path))?;
for entry in locale_dir {
let path = entry?.path();
let file = fs::File::open(&path)
.with_context(|| format!("while trying to open {:?}", &path))?;
let file = io::BufReader::new(file);
let model = model::Model::from_json_reader(file)
.with_context(|| format!("while reading {:?}", &path))?;
catalog.add(model)?;
}
}
Ok(catalog)
}
#[cfg(test)]
pub fn new_from_parts(
icu_data: icu_data::Loader,
requested: &[&str],
supported: &Vec<String>,
catalog: Catalog,
) -> Result<Lookup> {
Lookup::new_internal(icu_data, requested, supported, catalog)
}
fn new_internal(
icu_data: icu_data::Loader,
requested: &[&str],
supported: &Vec<String>,
catalog: Catalog,
) -> Result<Lookup> {
let mut supported_locales = supported
.iter()
.map(|s: &String| uloc::ULoc::try_from(s.as_str()))
.collect::<Result<Vec<_>, _>>()
.with_context(|| "while determining supported locales")?;
supported_locales.push(uloc::ULoc::try_from("und-und")?);
let supported_locales = supported_locales;
let mut requested_locales = vec![];
for locale in requested.iter() {
let (maybe_accepted_locale, accept_result) = uloc::accept_language(
vec![uloc::ULoc::try_from(*locale)
.with_context(|| format!("could not parse as locale: {:}", &locale))?],
supported_locales.clone(),
)?;
match accept_result {
usys::UAcceptResult::ULOC_ACCEPT_FAILED => {
}
_ => match maybe_accepted_locale {
None => {
return Err(anyhow::anyhow!(
"no matching locale found for: requested: {:?}, supported: {:?}",
&locale,
&supported_locales
));
}
Some(loc) => {
requested_locales.push(loc);
}
},
}
}
if requested_locales.is_empty() {
return Err(anyhow::anyhow!(
"no matching locale found for: requested: {:?}, supported: {:?}",
&requested,
&supported_locales
));
}
Ok(Lookup { requested: requested_locales, catalog, icu_data })
}
#[cfg(test)]
fn get_available_locales_for_test() -> Result<Vec<String>> {
Lookup::get_available_locales()
}
fn get_available_locales() -> Result<Vec<String>> {
let locale_dirs = std::fs::read_dir(ASSETS_DIR)
.with_context(|| format!("while reading {}", ASSETS_DIR))?;
let mut available_locales: Vec<String> = vec![];
for entry_or in locale_dirs {
let entry =
entry_or.with_context(|| format!("while reading entries in {}", ASSETS_DIR))?;
let name = entry.file_name().into_string().map_err(|os_string| {
anyhow::anyhow!("OS path not convertible to UTF-8: {:?}", os_string)
})?;
let entry_type = entry
.file_type()
.with_context(|| format!("while looking up file type for: {:?}", name))?;
if entry_type.is_dir() {
available_locales.push(name);
}
}
Ok(available_locales)
}
pub fn str(&self, id: u64) -> Result<&str, LookupStatus> {
Ok(self
.string(id)?
.to_str()
.with_context(|| format!("str(): while looking up id: {}", &id))?)
}
}
impl CApi for Lookup {
fn string(&self, id: u64) -> Result<&ffi::CStr, LookupStatus> {
for locale in self.requested.iter() {
if let Some(s) = self.catalog.get(&locale.to_language_tag(false)?, id) {
return Ok(s);
}
}
Err(LookupStatus::Unavailable)
}
}
#[cfg(test)]
mod tests {
use super::*;
use fidl_fuchsia_intl_test as ftest;
use std::collections::HashSet;
#[test]
fn lookup_en() -> Result<(), LookupStatus> {
let icu_data = icu_data::Loader::new().expect("icu data loaded");
let l = Lookup::new(icu_data, &vec!["en"])?;
assert_eq!("text_string", l.string(ftest::MessageIds::StringName as u64)?.to_str()?);
assert_eq!("text_string_2", l.string(ftest::MessageIds::StringName2 as u64)?.to_str()?);
Ok(())
}
#[test]
fn lookup_fr() -> Result<(), LookupStatus> {
let icu_data = icu_data::Loader::new().expect("icu data loaded");
let l = Lookup::new(icu_data, &vec!["fr"])?;
assert_eq!("le string", l.string(ftest::MessageIds::StringName as u64)?.to_str()?);
assert_eq!("le string 2", l.string(ftest::MessageIds::StringName2 as u64)?.to_str()?);
Ok(())
}
#[test]
fn lookup_es() -> Result<(), LookupStatus> {
let icu_data = icu_data::Loader::new().expect("icu data loaded");
let l = Lookup::new(icu_data, &vec!["es"])?;
assert_eq!("el stringo", l.string(ftest::MessageIds::StringName as u64)?.to_str()?);
assert_eq!("el stringo 2", l.string(ftest::MessageIds::StringName2 as u64)?.to_str()?);
Ok(())
}
#[test]
fn lookup_es_en() -> Result<(), LookupStatus> {
let icu_data = icu_data::Loader::new().expect("icu data loaded");
let l = Lookup::new(icu_data, &vec!["es", "en"])?;
assert_eq!("el stringo", l.string(ftest::MessageIds::StringName as u64)?.to_str()?);
assert_eq!("el stringo 2", l.string(ftest::MessageIds::StringName2 as u64)?.to_str()?);
Ok(())
}
#[test]
fn lookup_es_419_fallback() -> Result<(), LookupStatus> {
let icu_data = icu_data::Loader::new().expect("icu data loaded");
let l = Lookup::new(icu_data, &vec!["es-419-u-ca-gregorian"]).expect("locale exists");
assert_eq!("el stringo", l.string(ftest::MessageIds::StringName as u64)?.to_str()?);
assert_eq!("el stringo 2", l.string(ftest::MessageIds::StringName2 as u64)?.to_str()?);
Ok(())
}
#[test]
fn nonexistent_locale_rejected() -> Result<()> {
let icu_data = icu_data::Loader::new().expect("icu data loaded");
match Lookup::new(icu_data, &vec!["nonexistent-locale"]) {
Ok(_) => Err(anyhow::anyhow!("unexpectedly accepted nonexistent locale")),
Err(_) => Ok(()),
}
}
#[test]
fn locale_fallback_accounted_for() -> Result<()> {
let icu_data = icu_data::Loader::new().expect("icu data loaded");
Lookup::new(icu_data.clone(), &vec!["en"])?;
Lookup::new(icu_data.clone(), &vec!["fr"])?;
Lookup::new(icu_data.clone(), &vec!["es"])?;
Lookup::new(icu_data.clone(), &vec!["en-US"])?;
Lookup::new(icu_data.clone(), &vec!["es-ES"])?;
Lookup::new(icu_data.clone(), &vec!["es-419"])?;
Ok(())
}
#[test]
fn test_fake_lookup() -> Result<(), LookupStatus> {
let l = FakeLookup::new();
assert_eq!("Hello {person}!", l.string(1)?.to_str()?);
assert_eq!("Hello world!", l.string(10)?.to_str()?);
assert_eq!("Hello world!", l.string(12)?.to_str()?);
assert_eq!(LookupStatus::Unavailable, l.string(11).unwrap_err());
assert_eq!(LookupStatus::Unavailable, l.string(41).unwrap_err());
Ok(())
}
#[test]
fn test_real_lookup() -> Result<(), LookupStatus> {
let icu_data = icu_data::Loader::new().expect("icu data loaded");
let l = Lookup::new(icu_data, &vec!["es"])?;
assert_eq!("el stringo", l.str(ftest::MessageIds::StringName as u64)?);
Ok(())
}
#[test]
fn test_available_locales() -> Result<()> {
let expected: HashSet<String> = ["es", "en", "fr"].iter().map(|s| s.to_string()).collect();
assert_eq!(expected, Lookup::get_available_locales_for_test()?.into_iter().collect());
Ok(())
}
#[test]
fn ignore_unavailable_locales() {
let icu_data = icu_data::Loader::new().expect("icu data loaded");
let l = Lookup::new(icu_data, &vec!["sr", "es"]).expect("Lookup::new success");
assert_eq!(
"el stringo",
l.str(ftest::MessageIds::StringName as u64).expect("Lookup::str success")
);
}
#[test]
fn report_unavailable_locales_without_alternative() {
let icu_data = icu_data::Loader::new().expect("icu data loaded");
let l = Lookup::new(icu_data, &vec!["sr"]);
assert!(l.is_err());
}
}