1use std::path::PathBuf;
26
27use anyhow::{format_err, Context};
28use lazy_static::lazy_static;
29use std::borrow::Cow;
30use std::sync::{Arc, Mutex, Weak};
31use std::{env, fs, io};
32use thiserror::Error;
33use {rust_icu_common as icu, rust_icu_ucal as ucal, rust_icu_udata as udata};
34
35lazy_static! {
36 static ref REFCOUNT: Mutex<Weak<PathBuf>> = Mutex::new(Weak::new());
38}
39
40const ICU_DATA_PATH_DEFAULT: &str = "/pkg/data";
45
46const MIN_TZ_REVISION_ID_LENGTH: usize = 5;
48const MAX_TZ_REVISION_ID_LENGTH: usize = 15;
50
51#[derive(Error, Debug)]
54pub enum Error {
55 #[error("[icu_data]: {}", _0)]
56 Fail(anyhow::Error),
57 #[error("[icu_data]: generic error: {}, details: {:?}", _0, _1)]
59 Status(zx::Status, Option<Cow<'static, str>>),
60 #[error("[icu_data]: IO error: {}", _0)]
62 IO(io::Error),
63 #[error("[icu_data]: ICU error: {}", _0)]
65 ICU(icu::Error),
66}
67impl From<zx::Status> for Error {
68 fn from(status: zx::Status) -> Self {
69 Error::Status(status, None)
70 }
71}
72impl From<io::Error> for Error {
73 fn from(err: io::Error) -> Self {
74 Error::IO(err)
75 }
76}
77impl From<anyhow::Error> for Error {
78 fn from(err: anyhow::Error) -> Self {
79 Error::Fail(err)
80 }
81}
82impl From<icu::Error> for Error {
83 fn from(err: icu::Error) -> Self {
84 Error::ICU(err)
85 }
86}
87
88#[derive(Debug, Clone)]
93pub struct Loader {
94 _refs: Arc<PathBuf>,
97}
98unsafe impl Sync for Loader {}
100
101impl Loader {
102 pub fn new() -> Result<Self, Error> {
108 Self::new_with_optional_tz_resources(None, None)
109 }
110
111 pub fn new_with_tz_resource_path(tzdata_dir_path: &str) -> Result<Self, Error> {
115 Self::new_with_optional_tz_resources(Some(tzdata_dir_path), None)
116 }
117
118 pub fn new_with_tz_resources_and_validation(
123 tzdata_dir_path: &str,
124 tz_revision_file_path: &str,
125 ) -> Result<Self, Error> {
126 Self::new_with_optional_tz_resources(Some(tzdata_dir_path), Some(tz_revision_file_path))
127 }
128
129 fn new_with_optional_tz_resources(
131 tzdata_dir_path: Option<&str>,
132 tz_revision_file_path: Option<&str>,
133 ) -> Result<Self, Error> {
134 let mut l = REFCOUNT.lock().expect("refcount lock acquired");
137 match l.upgrade() {
138 Some(_refs) => Ok(Loader { _refs }),
139 None => {
140 if let Some(p) = tzdata_dir_path {
142 let for_path = fs::File::open(p)
143 .map_err(|e| Error::Fail(format_err!("io error: {}", e)))
144 .with_context(|| format!("error while opening: {:?}", &tzdata_dir_path))?;
145 let meta = for_path
146 .metadata()
147 .with_context(|| format!("while getting metadata for: {:?}", &p))?;
148 if !meta.is_dir() {
149 return Err(Error::Fail(format_err!("not a directory: {}", p)));
150 }
151 env::set_var("ICU_TIMEZONE_FILES_DIR", p);
157 }
158
159 let path = PathBuf::from(ICU_DATA_PATH_DEFAULT);
163 udata::set_data_directory(&path);
164 let _refs = Arc::new(path);
165 Self::validate_revision(tz_revision_file_path)?;
166 (*l) = Arc::downgrade(&_refs);
167 Ok(Loader { _refs })
168 }
169 }
170 }
171
172 fn validate_revision(tz_revision_file_path: Option<&str>) -> Result<(), Error> {
173 match tz_revision_file_path {
174 None => Ok(()),
175 Some(tz_revision_file_path) => {
176 let expected_revision_id = std::fs::read_to_string(tz_revision_file_path)
177 .with_context(|| {
178 format!("could not read file: {:?}", &tz_revision_file_path)
179 })?;
180 if !(MIN_TZ_REVISION_ID_LENGTH..=MAX_TZ_REVISION_ID_LENGTH)
181 .contains(&expected_revision_id.len())
182 {
183 return Err(Error::Status(
184 zx::Status::IO_DATA_INTEGRITY,
185 Some(
186 format!(
187 "invalid revision ID in {}: {}",
188 tz_revision_file_path, expected_revision_id
189 )
190 .into(),
191 ),
192 ));
193 }
194
195 let actual_revision_id = ucal::get_tz_data_version().with_context(|| {
196 format!("while getting data version from: {:?}", &tz_revision_file_path)
197 })?;
198 if expected_revision_id != actual_revision_id {
199 return Err(Error::Status(
200 zx::Status::IO_DATA_INTEGRITY,
201 Some(
202 format!(
203 "expected revision ID {} but got {}",
204 expected_revision_id, actual_revision_id
205 )
206 .into(),
207 ),
208 ));
209 }
210
211 Ok(())
212 }
213 }
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220 use assert_matches::assert_matches;
221 use rust_icu_uenum as uenum;
222
223 #[test]
225 fn initialization() {
226 let _loader = Loader::new().expect("loader is constructed with success");
227 let _loader2 = Loader::new().expect("loader is just fine with a second initialization");
228 let tz: String = uenum::open_time_zones().unwrap().take(1).map(|e| e.unwrap()).collect();
229 assert_eq!(tz, "ACT");
230 }
232
233 #[test]
234 fn you_can_also_clone_loaders() {
235 let _loader = Loader::new().expect("loader is constructed with success");
236 let _loader2 = Loader::new().expect("loader is just fine with a second initialization");
237 let _loader3 = _loader2.clone();
238 let tz: String = uenum::open_time_zones().unwrap().take(1).map(|e| e.unwrap()).collect();
239 assert_eq!(tz, "ACT");
240 }
241
242 #[test]
243 fn two_initializations_in_a_row() {
244 {
245 let _loader = Loader::new().expect("loader is constructed with success");
246 let tz: String =
247 uenum::open_time_zones().unwrap().take(1).map(|e| e.unwrap()).collect();
248 assert_eq!(tz, "ACT");
249 }
250 {
251 let _loader2 = Loader::new().expect("loader is just fine with a second initialization");
252 let tz: String =
253 uenum::open_time_zones().unwrap().take(1).map(|e| e.unwrap()).collect();
254 assert_eq!(tz, "ACT");
255 }
256 }
257 #[test]
260 fn test_tz_res_loading_without_validation() -> Result<(), Error> {
261 let _loader = Loader::new().expect("loader is constructed with success");
262 let tz: String = uenum::open_time_zones()?.take(1).map(|e| e.unwrap()).collect();
263 assert_eq!(tz, "ACT");
264 Ok(())
265 }
266
267 #[test]
268 fn test_tz_res_loading_with_validation_valid() -> Result<(), Error> {
269 let _loader = Loader::new_with_tz_resources_and_validation(
270 "/pkg/data/tzdata/icu/44/le",
271 "/pkg/data/tzdata/revision.txt",
272 )
273 .expect("loader is constructed successfully");
274 let tz: String = uenum::open_time_zones()?.take(1).map(|e| e.unwrap()).collect();
275 assert_eq!(tz, "ACT");
276 Ok(())
277 }
278
279 #[test]
280 fn test_tz_res_loading_with_validation_invalid() -> Result<(), Error> {
281 let result = Loader::new_with_tz_resources_and_validation(
282 "/pkg/data/tzdata/icu/44/le",
283 "/pkg/data/test_inconsistent_revision.txt",
284 );
285 let err = result.unwrap_err();
286 assert_matches!(err, Error::Status(zx::Status::IO_DATA_INTEGRITY, Some(_)));
287 Ok(())
288 }
289}