blob: 1dbad5ebe9442d45c0aa66b98005d9d4e9488a1d [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! Initializers for ICU data files.
//!
//! Use the library by instantiating a `Loader` and keeping a reference to it for as long as you
//! need access to timezone data. You can do this in your program as many times as needed, and the
//! loader will make sure that the data is loaded before it is first used, and that it is unloaded
//! once no more loaders are live.
//!
//! It is also possible to clone a loader in case you need to pass it along to ensure that timezone
//! data is available.
//!
//! Example use:
//!
//! ```
//! fn basic() {
//! let _loader = Loader::new().expect("loader is constructed with success");
//! let _loader2 = Loader::new().expect("second initialization is a no-operation");
//! let _loader3 = _loader2.clone(); // It is OK to clone a loader and keep it around.
//! }
//! ```
use {
anyhow::format_err,
fuchsia_zircon as zx,
lazy_static::lazy_static,
rust_icu_common as icu, rust_icu_ucal as ucal, rust_icu_udata as udata,
std::{
borrow::Cow,
convert::TryFrom,
env, fs, io,
sync::{Arc, Mutex, Weak},
},
thiserror::Error,
};
lazy_static! {
// The storage for the loaded ICU data. At most one may be loaded at any given time.
static ref REFCOUNT: Mutex<Weak<udata::UDataMemory>> = Mutex::new(Weak::new());
}
// The default location at which to find the ICU data.
const ICU_DATA_PATH_DEFAULT: &str = "/pkg/data/icudtl.dat";
/// Minimum expected length of a time zone revision ID (e.g. "2019c").
const MIN_TZ_REVISION_ID_LENGTH: usize = 5;
/// Maximum expected length of a time zone revision ID.
const MAX_TZ_REVISION_ID_LENGTH: usize = 15;
/// Error type returned by `icu_udata`. The individual enum values encode
/// classes of possible errors returned.
#[derive(Error, Debug)]
pub enum Error {
#[error("[icu_data]: {}", _0)]
Fail(anyhow::Error),
/// The operation failed due to an underlying Zircon error.
#[error("[icu_data]: generic error: {}, details: {:?}", _0, _1)]
Status(zx::Status, Option<Cow<'static, str>>),
/// The operation failed due to an IO error.
#[error("[icu_data]: IO error: {}", _0)]
IO(io::Error),
/// The operation failed due to an ICU library error.
#[error("[icu_data]: ICU error: {}", _0)]
ICU(icu::Error),
}
impl From<zx::Status> for Error {
fn from(status: zx::Status) -> Self {
Error::Status(status, None)
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Self {
Error::IO(err)
}
}
impl From<anyhow::Error> for Error {
fn from(err: anyhow::Error) -> Self {
Error::Fail(err)
}
}
impl From<icu::Error> for Error {
fn from(err: icu::Error) -> Self {
Error::ICU(err)
}
}
/// Manages the lifecycle of the loaded ICU data.
///
/// `Loader` can be created using `Loader::new` and can be cloned. For as long as any Loader
/// remains in scope, the ICU data will not be unloaded.
#[derive(Debug, Clone)]
pub struct Loader {
// TODO(fxbug.dev/84729)
#[allow(unused)]
refs: Arc<udata::UDataMemory>,
// TODO(fxbug.dev/84729)
#[allow(unused)]
vmo_size_bytes: usize,
// TODO(fxbug.dev/84729)
#[allow(unused)]
file_size_bytes: usize,
// TODO(fxbug.dev/84729)
#[allow(unused)]
icu_tzdata_dir_path: Option<String>,
// TODO(fxbug.dev/84729)
#[allow(unused)]
icu_data_path: String,
}
// Loader is OK to be sent to threads.
unsafe impl Sync for Loader {}
impl Loader {
/// Initializes the ICU dynamic timezone data, based on the default resource directory.
///
/// The caller should create a `Loader` very early on in the lifetime of the program, and keep
/// instances of `Loader` alive until the ICU data is needed. You can make as many `Loader`
/// objects as you need. The data will be unloaded only after the last of them leaves scope.
pub fn new() -> Result<Self, Error> {
Self::new_with_optional_tz_resources(None, None)
}
/// Initializes ICU data, loading time zone resources from the supplied `path`.
///
/// See documentation for `new` for calling constraints.
pub fn new_with_tz_resource_path(tzdata_dir_path: &str) -> Result<Self, Error> {
Self::new_with_optional_tz_resources(Some(tzdata_dir_path), None)
}
/// Initializes ICU data, loading time zone resources from the supplied `path` and validating
/// the time zone revision ID against the ID contained in the file at `revision_file_path`.
///
/// See documentation for `new` for calling constraints.
pub fn new_with_tz_resources_and_validation(
tzdata_dir_path: &str,
tz_revision_file_path: &str,
) -> Result<Self, Error> {
Self::new_with_optional_tz_resources(Some(tzdata_dir_path), Some(tz_revision_file_path))
}
// Ensures that all calls to create a `Loader` go through the same code path.
fn new_with_optional_tz_resources(
tzdata_dir_path: Option<&str>,
tz_revision_file_path: Option<&str>,
) -> Result<Self, Error> {
// The lock contention should not be an issue. Only a few calls (single digits) to this
// function are expected. So we take a write lock immmediately.
let mut l = REFCOUNT.lock().expect("refcount lock acquired");
match l.upgrade() {
Some(refs) => Ok(Loader {
refs,
vmo_size_bytes: 0,
file_size_bytes: 0,
icu_tzdata_dir_path: None,
icu_data_path: "".to_string(),
}),
None => {
// Load up the TZ files directory.
if let Some(p) = tzdata_dir_path {
let for_path = fs::File::open(p)?;
let meta = for_path.metadata()?;
if !meta.is_dir() {
return Err(Error::Fail(format_err!("not a directory: {}", p)));
}
// This is the default API used to configure the ICU library, so we are
// just using it here. Even though it is not a preferred way to configure
// Fuchsia programs.
// Further, we want to load the same ICU data for all programs that need this
// file.
env::set_var("ICU_TIMEZONE_FILES_DIR", p);
}
// Read ICU data file from the filesystem.
let file = fs::File::open(ICU_DATA_PATH_DEFAULT)?;
let file_size_bytes = file.metadata()?.len() as usize;
let vmo = fdio::get_vmo_copy_from_file(&file)?;
let vmo_size_bytes = vmo.get_size()? as usize;
let mut buf: Vec<u8> = vec![0; file_size_bytes];
vmo.read(&mut buf, 0)?;
let refs = Arc::new(udata::UDataMemory::try_from(buf)?);
Self::validate_revision(tz_revision_file_path)?;
(*l) = Arc::downgrade(&refs);
Ok(Loader {
refs,
vmo_size_bytes,
file_size_bytes,
icu_tzdata_dir_path: tzdata_dir_path.map(|p| p.to_string()),
icu_data_path: ICU_DATA_PATH_DEFAULT.to_string(),
})
}
}
}
fn validate_revision(tz_revision_file_path: Option<&str>) -> Result<(), Error> {
match tz_revision_file_path {
None => Ok(()),
Some(tz_revision_file_path) => {
let expected_revision_id = std::fs::read_to_string(tz_revision_file_path)?;
if !(MIN_TZ_REVISION_ID_LENGTH..=MAX_TZ_REVISION_ID_LENGTH)
.contains(&expected_revision_id.len())
{
return Err(Error::Status(
zx::Status::IO_DATA_INTEGRITY,
Some(
format!(
"invalid revision ID in {}: {}",
tz_revision_file_path, expected_revision_id
)
.into(),
),
));
}
let actual_revision_id = ucal::get_tz_data_version()?;
if expected_revision_id != actual_revision_id {
return Err(Error::Status(
zx::Status::IO_DATA_INTEGRITY,
Some(
format!(
"expected revision ID {} but got {}",
expected_revision_id, actual_revision_id
)
.into(),
),
));
}
Ok(())
}
}
}
}
#[cfg(test)]
mod tests {
use {super::*, assert_matches::assert_matches, rust_icu_uenum as uenum};
// [START loader_example]
#[test]
fn initialization() {
let _loader = Loader::new().expect("loader is constructed with success");
let _loader2 = Loader::new().expect("loader is just fine with a second initialization");
let tz: String = uenum::open_time_zones().unwrap().take(1).map(|e| e.unwrap()).collect();
assert_eq!(tz, "ACT");
// The library will be cleaned up after the last of the loaders goes out of scope.
}
#[test]
fn you_can_also_clone_loaders() {
let _loader = Loader::new().expect("loader is constructed with success");
let _loader2 = Loader::new().expect("loader is just fine with a second initialization");
let _loader3 = _loader2.clone();
let tz: String = uenum::open_time_zones().unwrap().take(1).map(|e| e.unwrap()).collect();
assert_eq!(tz, "ACT");
}
#[test]
fn two_initializations_in_a_row() {
{
let _loader = Loader::new().expect("loader is constructed with success");
let tz: String =
uenum::open_time_zones().unwrap().take(1).map(|e| e.unwrap()).collect();
assert_eq!(tz, "ACT");
}
{
let _loader2 = Loader::new().expect("loader is just fine with a second initialization");
let tz: String =
uenum::open_time_zones().unwrap().take(1).map(|e| e.unwrap()).collect();
assert_eq!(tz, "ACT");
}
}
// [END loader_example]
#[test]
fn test_tz_res_loading_without_validation() -> Result<(), Error> {
let _loader = Loader::new().expect("loader is constructed with success");
let tz: String = uenum::open_time_zones()?.take(1).map(|e| e.unwrap()).collect();
assert_eq!(tz, "ACT");
Ok(())
}
#[test]
fn test_tz_res_loading_with_validation_valid() -> Result<(), Error> {
let _loader = Loader::new_with_tz_resources_and_validation(
"/config/data/tzdata/icu/44/le",
"/config/data/tzdata/revision.txt",
)
.expect("loader is constructed successfully");
let tz: String = uenum::open_time_zones()?.take(1).map(|e| e.unwrap()).collect();
assert_eq!(tz, "ACT");
Ok(())
}
#[test]
fn test_tz_res_loading_with_validation_invalid() -> Result<(), Error> {
let result = Loader::new_with_tz_resources_and_validation(
"/config/data/tzdata/icu/44/le",
"/pkg/data/test_inconsistent_revision.txt",
);
let err = result.unwrap_err();
assert_matches!(err, Error::Status(zx::Status::IO_DATA_INTEGRITY, Some(_)));
Ok(())
}
}