diff --git a/Cargo.lock b/Cargo.lock index fb848540a5a..2a467fa9343 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -436,6 +436,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + [[package]] name = "core_maths" version = "0.1.0" @@ -749,6 +755,16 @@ dependencies = [ "void", ] +[[package]] +name = "env_preferences" +version = "1.5.0" +dependencies = [ + "core-foundation-sys", + "icu_locale", + "libc", + "windows", +] + [[package]] name = "erased-serde" version = "0.3.31" @@ -3092,6 +3108,59 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "windows" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132" +dependencies = [ + "windows-core", + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-core" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-implement" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "windows-interface" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.5", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index de4036a3714..1fe118a2fcd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,7 @@ members = [ "utils/pattern", "utils/preferences", "utils/resb", + "utils/env_preferences", "utils/tinystr", "utils/tzif", "utils/writeable", diff --git a/utils/env_preferences/Cargo.toml b/utils/env_preferences/Cargo.toml new file mode 100644 index 00000000000..0c3398dcef2 --- /dev/null +++ b/utils/env_preferences/Cargo.toml @@ -0,0 +1,35 @@ +# This file is part of ICU4X. For terms of use, please see the file +# called LICENSE at the top level of the ICU4X source tree +# (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +[package] +name = "env_preferences" +version.workspace = true +rust-version.workspace = true +authors.workspace = true +edition.workspace = true +repository.workspace = true +homepage.workspace = true +license.workspace = true +categories.workspace = true +include.workspace = true + +[dependencies] +core-foundation-sys = "0.8.6" +libc = "0.2.155" + +[dependencies.windows] +version = "0.56.0" +features = [ + "System", + "Foundation", + "System_UserProfile", + "Foundation_Collections", + "Globalization", + "Globalization_DateTimeFormatting", + "Win32", + "Win32_Globalization" +] + +[dev-dependencies] +icu_locale = { path = "../../components/locale" } diff --git a/utils/env_preferences/LICENSE b/utils/env_preferences/LICENSE new file mode 100644 index 00000000000..c9be6012c53 --- /dev/null +++ b/utils/env_preferences/LICENSE @@ -0,0 +1,46 @@ +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. diff --git a/utils/env_preferences/README.md b/utils/env_preferences/README.md new file mode 100644 index 00000000000..e381aa47e9f --- /dev/null +++ b/utils/env_preferences/README.md @@ -0,0 +1,5 @@ + + +Retrieval of system locales and preferences. + + diff --git a/utils/env_preferences/src/apple.rs b/utils/env_preferences/src/apple.rs new file mode 100644 index 00000000000..38304ac53de --- /dev/null +++ b/utils/env_preferences/src/apple.rs @@ -0,0 +1,164 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +use core_foundation_sys::{ + array::{CFArrayGetCount, CFArrayGetValueAtIndex}, + base::{CFIndex, CFRelease, CFRetain}, + calendar::{CFCalendarCopyCurrent, CFCalendarCopyLocale, CFCalendarGetIdentifier}, + locale::{CFLocaleCopyPreferredLanguages, CFLocaleGetIdentifier}, + string::{ + kCFStringEncodingUTF8, CFStringGetCString, CFStringGetCStringPtr, CFStringGetLength, + CFStringRef, + }, + timezone, +}; +use libc::c_char; +use std::ffi::{CStr, CString}; + +use crate::RetrievalError; + +/// Helps to get string, it tries to get the string directly from the pointer itself, in case it is unable to retrieve +/// the string (c_str_ptr is NULL) a buffer is created of size `length + 1` and we perform manual allocations to get +/// the string +fn get_string(ptr: CFStringRef) -> Result { + // SAFETY: The call to `CFStringGetCStringPtr` because the reference of string we are accessing is not `NULL` + // Returns pointer in O(1) without any memory allocation. This can return NULL so we are handling it by directly + // copying it using `CFStringGetCString` + let c_str_ptr: *const c_char = unsafe { CFStringGetCStringPtr(ptr, kCFStringEncodingUTF8) }; + + if !c_str_ptr.is_null() { + // SAFETY: A valid `NULL` terminator is present which is a requirement of `from_ptr` + let lang_rust_str = unsafe { CStr::from_ptr(c_str_ptr) }.to_str()?; + Ok(lang_rust_str.to_string()) + } else { + // `c_str_ptr` is null, i.e. `CFStringGetCStringPtr` couldn't give desired output, trying with + // manual allocations + // SAFETY: It returns length of the string, from above conditional statement we ensure + // that the `lang_ptr` is not NULL thus making it safe to call + let length = unsafe { CFStringGetLength(ptr) as usize }; + + let mut c_str_buf: Vec = vec![0; length + 1]; + + // SAFETY: Safety is ensured by following points + // 1. `lang_ptr` is not NULL, checked through conditional statement + // 2. `c_str_buf` is large enough and in scope after this call + unsafe { + CFStringGetCString( + ptr, + c_str_buf.as_mut_ptr() as *mut c_char, + c_str_buf.len() as CFIndex, + kCFStringEncodingUTF8, + ); + } + + let c_string = CString::from_vec_with_nul(c_str_buf)?; + c_string + .into_string() + .map_err(|e| RetrievalError::ConversionError(e.utf8_error())) + } +} + +pub fn get_locales() -> Result, RetrievalError> { + let mut languages: Vec = Vec::new(); + + // SAFETY: The call to `CFLocaleCopyPreferredLanguages` returns an immutable reference to `CFArray` which is owned by us + // https://developer.apple.com/documentation/corefoundation/cfarrayref. It is ensured that `locale_carr_ref` is not mutated + // Immutablility ensures that nothing is overriden during it's scope + let locale_carr_ref = unsafe { CFLocaleCopyPreferredLanguages() }; + + if !locale_carr_ref.is_null() { + // SAFETY: The call to `CFArrayGetCount` is only made when is `locale_carr_ref` is not `NULL` + let count = unsafe { CFArrayGetCount(locale_carr_ref as _) }; + + for i in 0..count { + // SAFETY: The call to `CFArrayGetValueAtIndex` is safe because we are iterating from 0 to count (`CFArrayGetCount`) which + // in itself will always be greater than 0 and less than count ensuring we will not get "out of bounds" error + let lang_ptr = unsafe { CFArrayGetValueAtIndex(locale_carr_ref, i) }; + + if !lang_ptr.is_null() { + let locale_str = get_string(lang_ptr as CFStringRef)?; + languages.push(locale_str); + } else { + return Err(RetrievalError::NullLocale); + } + } + } else { + // No need to release memory for `locale_carr_ref` since it is NULL + return Err(RetrievalError::NullLocale); + } + // Release for memory + unsafe { + CFRelease(locale_carr_ref as _); + } + + Ok(languages) +} + +pub fn get_system_calendars() -> Result, RetrievalError> { + let mut calendars = Vec::new(); + let calendar_locale_str: String; + let mut calendar_identifier_str = String::new(); + + // SAFETY: The call to `CFCalendarCopyCurrent` returns a calendar object owned by us + // This calendar object is used extract locale and type of calendar (identifier) + let calendar = unsafe { CFCalendarCopyCurrent() }; + + if !calendar.is_null() { + // SAFETY: Retaining the calendar object when not `NULL` + // It is released when all actions are completed + unsafe { CFRetain(calendar as _) }; + + // SAFETY: Retrieves `CFLocale` object for the calendar, the `if` statement ensures we don't + // pass in a `NULL` references + let locale = unsafe { CFCalendarCopyLocale(calendar as _) }; + + // SAFETY: Retrieves `CFString` (identifier) for the calendar, the `if` statement ensures + // we don't pass in a `NULL` references + let identifier = unsafe { CFCalendarGetIdentifier(calendar as _) }; + + if !locale.is_null() { + // SAFETY: Retain the locale object, released when we extracted the string + unsafe { CFRetain(locale as _) }; + + // SAFETY: Retrieves `CFString` (identifier) for the calendar, the `if` statement ensures + // we don't pass in a `NULL` reference + let locale_identifier = unsafe { CFLocaleGetIdentifier(locale) }; + calendar_locale_str = get_string(locale_identifier as CFStringRef)?; + + // SAFETY: Releases the locale object which was retained + unsafe { CFRelease(locale as _) }; + } else { + return Err(RetrievalError::NullLocale); + } + + if !identifier.is_null() { + calendar_identifier_str = get_string(identifier as CFStringRef)?; + } + // SAFETY: Release the calendar when done to avoid memory leaks + unsafe { CFRelease(calendar as _) }; + + calendars.push((calendar_locale_str, calendar_identifier_str)); + } else { + return Err(RetrievalError::NullCalendar); + } + + Ok(calendars) +} + +pub fn get_system_timezone() -> Result { + // SAFETY: Returns the time zone currently used by the system + // Returns an immutable reference to TimeZone object owned by us + let timezone = unsafe { timezone::CFTimeZoneCopySystem() }; + + if !timezone.is_null() { + // SAFETY: Extracts name of time zone from the TimeZone object, reference to timezone + // is guaranteed to be not NULL + let cf_string = unsafe { timezone::CFTimeZoneGetName(timezone) }; + + if !cf_string.is_null() { + return Ok(get_string(cf_string)?); + } + } + Err(RetrievalError::NullTimeZone) +} diff --git a/utils/env_preferences/src/error.rs b/utils/env_preferences/src/error.rs new file mode 100644 index 00000000000..370865239ca --- /dev/null +++ b/utils/env_preferences/src/error.rs @@ -0,0 +1,51 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +use std::{ffi::FromVecWithNulError, str::Utf8Error}; + +#[derive(Debug)] +pub enum RetrievalError { + /// Error converting into `&CStr` to `&str` + ConversionError(Utf8Error), + + /// Error creating a `CString` from a buffer with a null terminator + FromVecWithNulError(FromVecWithNulError), + + /// Unable to retrieve the calendar + NullCalendar, + + /// Unable to retrieve the locale + NullLocale, + + /// Unable to retrieve TimeZone + NullTimeZone, + + /// UnknownCategory when retrieving locale for linux + UnknownCategory, + + /// Error handling for windows system + #[cfg(target_os = "windows")] + Windows(windows::core::Error), + + Other(String), +} + +#[cfg(target_os = "windows")] +impl From for RetrievalError { + fn from(input: windows::core::Error) -> Self { + Self::Windows(input) + } +} + +impl From for RetrievalError { + fn from(input: Utf8Error) -> Self { + Self::ConversionError(input) + } +} + +impl From for RetrievalError { + fn from(input: FromVecWithNulError) -> Self { + Self::FromVecWithNulError(input) + } +} diff --git a/utils/env_preferences/src/lib.rs b/utils/env_preferences/src/lib.rs new file mode 100644 index 00000000000..5d0fa1f97a5 --- /dev/null +++ b/utils/env_preferences/src/lib.rs @@ -0,0 +1,25 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +//! Retrieval of system locales and preferences. + +mod error; +pub use error::RetrievalError; + +#[cfg(target_os = "linux")] +mod linux; +#[cfg(target_os = "linux")] +pub use linux::*; +#[cfg(target_os = "macos")] +mod apple; +#[cfg(target_os = "macos")] +pub use apple::*; +#[cfg(target_os = "windows")] +mod windows; +#[cfg(target_os = "windows")] +pub use windows::*; +#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] +compile_error!( + "Unsupported target OS. Supported operating systems are Apple, Linux & Windows as of now" +); diff --git a/utils/env_preferences/src/linux.rs b/utils/env_preferences/src/linux.rs new file mode 100644 index 00000000000..af19f42be2e --- /dev/null +++ b/utils/env_preferences/src/linux.rs @@ -0,0 +1,97 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +use std::{collections::HashMap, ffi::CStr, ptr}; + +use libc::{setlocale, LC_ALL, LC_TIME}; +use std::str::FromStr; + +use crate::RetrievalError; + +#[derive(Hash, Eq, PartialEq, Debug)] +pub enum LocaleCategory { + Character, + Number, + Time, + Collate, + Monetary, + Messages, + Paper, + Name, + Address, + Telephone, + Measurement, + Identification, + All, +} + +impl FromStr for LocaleCategory { + type Err = RetrievalError; + + fn from_str(s: &str) -> Result { + match s { + "LC_CTYPE" => Ok(Self::Character), + "LC_NUMERIC" => Ok(Self::Number), + "LC_TIME" => Ok(Self::Time), + "LC_COLLATE" => Ok(Self::Collate), + "LC_MONETARY" => Ok(Self::Monetary), + "LC_MESSAGES" => Ok(Self::Messages), + "LC_PAPER" => Ok(Self::Paper), + "LC_NAME" => Ok(Self::Name), + "LC_ADDRESS" => Ok(Self::Address), + "LC_TELEPHONE" => Ok(Self::Telephone), + "LC_MEASUREMENT" => Ok(Self::Measurement), + "LC_IDENTIFICATION" => Ok(Self::Identification), + "LC_ALL" => Ok(Self::All), + _ => Err(RetrievalError::UnknownCategory), + } + } +} + +pub fn get_locales() -> Result, RetrievalError> { + let mut locale_map = HashMap::new(); + + // SAFETY: Safety is ensured because we pass a `NULL` pointer and retrieve the locale there is + // no subsequent calls for `setlocale` which could change the locale of this particular thread + let locales_ptr = unsafe { setlocale(LC_ALL, ptr::null()) }; + + if locales_ptr.is_null() { + return Err(RetrievalError::NullLocale); + } + + // SAFETY: A valid `NULL` terminator is present which is a requirement of `from_ptr` + let locales_str = unsafe { CStr::from_ptr(locales_ptr) }.to_str()?; + let locale_pairs = locales_str.split(';'); + for locale_pair in locale_pairs { + let mut parts = locale_pair.split('='); + if let Some(value) = parts.next() { + if let Some(key) = parts.next() { + if let Ok(category) = LocaleCategory::from_str(value) { + locale_map.insert(category, key.to_string()); + } + } else { + // Handle case where only a single locale + locale_map.insert(LocaleCategory::All, value.to_string()); + } + } + } + Ok(locale_map) +} + +/// This only returns the calendar locale,`gnome-calendar` is the default calendar in linux +/// The locale returned is for `Gregorian` calendar +/// Related issue: `` +pub fn get_system_calendars() -> Result { + // SAFETY: Safety is ensured because we pass a `NULL` pointer and retrieve the locale there is + // no subsequent calls for `setlocale` which could change the locale of this particular thread + let locale_ptr = unsafe { setlocale(LC_TIME, ptr::null()) }; + + if !locale_ptr.is_null() { + // SAFETY: A valid `NULL` terminator is present which is a requirement of `from_ptr` + let rust_str = unsafe { CStr::from_ptr(locale_ptr) }.to_str()?; + let calendar_locale = rust_str.to_string(); + return Ok(calendar_locale); + } + Err(RetrievalError::NullLocale) +} diff --git a/utils/env_preferences/src/windows.rs b/utils/env_preferences/src/windows.rs new file mode 100644 index 00000000000..87a13a00239 --- /dev/null +++ b/utils/env_preferences/src/windows.rs @@ -0,0 +1,39 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +use windows::{Globalization, System::UserProfile::GlobalizationPreferences}; + +use crate::RetrievalError; + +pub fn get_locales() -> Result, RetrievalError> { + let mut locale_vec_str: Vec = Vec::new(); + let locale = GlobalizationPreferences::Languages()?; + + for i in 0..locale.Size()? { + let hstring = locale.GetAt(i)?; + let string = hstring.to_string_lossy(); + locale_vec_str.push(string); + } + Ok(locale_vec_str) +} + +pub fn get_system_calendars() -> Result, RetrievalError> { + let calendar = Globalization::Calendar::new()?; + let system_calendar = Globalization::Calendar::GetCalendarSystem(&calendar)?; + let calendar_type: String = system_calendar.to_string(); + let locale_list: Vec = get_locales()?; + + let result: Vec<(String, String)> = locale_list + .into_iter() + .map(|locale| (locale, calendar_type.clone())) + .collect(); + + Ok(result) +} + +pub fn get_system_timezone() -> Result { + let calendar = Globalization::Calendar::new()?; + let timezone = calendar.GetTimeZone()?; + Ok(timezone.to_string_lossy()) +} diff --git a/utils/env_preferences/tests/test.rs b/utils/env_preferences/tests/test.rs new file mode 100644 index 00000000000..10afaa524b6 --- /dev/null +++ b/utils/env_preferences/tests/test.rs @@ -0,0 +1,162 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +#[cfg(target_os = "linux")] +#[cfg(test)] +mod linux_tests { + use env_preferences::{get_locales, get_system_calendars, LocaleCategory, RetrievalError}; + use icu_locale::Locale; + use libc::setlocale; + + // Testing fetching of locale, as `get_locales` fetches the locales for category + // `LC_ALL`. For this category this should return non empty + #[test] + fn test_get_locales() { + let locale_res = get_locales().unwrap(); + assert!( + !locale_res.is_empty(), + "Empty hashmap for locales retrieved" + ); + for locale in locale_res.into_values() { + assert!(locale.is_ascii(), "Invalid form of locale retrieved") + } + } + + #[test] + fn test_converting_locales() { + let locale_res: std::collections::HashMap = get_locales().unwrap(); + for locale in locale_res.into_values() { + let parts: Vec<&str> = locale.split('.').collect(); + + // Skipping "C" and those ending with "UTF-8", as they cannot be converted + // into the locale + if !parts.iter().any(|&part| part == "C") + && (parts.len() > 1 && parts[parts.len() - 1] != "UTF-8") + { + let mut locale_converted: Locale = locale.parse().unwrap(); + locale_converted.extensions.unicode.clear(); + assert_eq!(locale_converted, locale.parse().unwrap()); + } + } + } + + // This test contains unsafe code, the idea is to manually set a locale for `LC_TIME`, + // compare the result from `get_locales` and `get_system_calendar` they must be equal + #[test] + fn test_calendar() { + // Using "C" locale since it is the default, using any other locale like `en_IN` or `en_US` + // may work on some system and may not others depending on the availability + let test_calendar_locale = "C"; + let locale_cstr = + std::ffi::CString::new(test_calendar_locale).expect("CString::new failed"); + + // SAFETY: This call is safe because any subsequent call to `setlocale` we pass a `NULL` locale + // to retrieve locale which does not sets the locale. The test locale `locale_cstr` is a CString + // nul terminated string for which we have the ownership + let tr = unsafe { setlocale(libc::LC_TIME, locale_cstr.as_ptr()) }; + + if tr.is_null() { + panic!("{:?}", RetrievalError::NullLocale); + } + + let calendar_locale = get_system_calendars().unwrap(); + assert_eq!(test_calendar_locale.to_string(), calendar_locale); + } +} + +#[cfg(target_os = "macos")] +#[cfg(test)] +mod macos_test { + use env_preferences::{get_locales, get_system_calendars, get_system_timezone}; + use icu_locale::Locale; + + #[test] + fn test_get_locales() { + let locales_res = get_locales(); + match locales_res { + Ok(locales) => { + for locale in locales { + assert!(!locale.is_empty(), "Empty locale retrieved"); + assert!(locale.is_ascii(), "Invalid form of locale retrieved"); + } + } + Err(e) => { + panic!("{:?}", e) + } + } + } + + #[test] + fn test_converting_locales() { + let locales = get_locales().unwrap(); + for locale in locales { + let _loc: Locale = locale.parse().unwrap(); + } + } + + #[test] + fn test_calendar() { + let calendar_res = get_system_calendars().unwrap(); + for calendar in calendar_res { + assert!(!calendar.0.is_empty(), "Couldn't retreive calendar locale"); + assert!(calendar.0.is_ascii(), "Calendar locale form is not valid"); + assert!(!calendar.1.is_empty(), "Couldn't retreive calendar"); + assert!( + calendar.1.is_ascii(), + "Calendar identifier form is not valid" + ); + } + } + + #[test] + fn test_time_zone() { + let time_zone = get_system_timezone().unwrap(); + assert!(!time_zone.is_empty(), "Couldn't retreive time_zone"); + } +} + +#[cfg(target_os = "windows")] +#[cfg(test)] +mod windows_test { + use env_preferences::{get_locales, get_system_calendars, get_system_timezone}; + use icu_locale::Locale; + + #[test] + fn test_get_locales() { + let locales = get_locales().unwrap(); + for locale in locales { + assert!(!locale.is_empty(), "Empty locale retrieved"); + assert!(locale.is_ascii(), "Invalid form of locale retrieved"); + } + } + + #[test] + fn test_converting_locales() { + let locales = get_locales().unwrap(); + for locale in locales { + let _converted_locale: Locale = locale.parse().unwrap(); + } + } + + #[test] + fn test_calendar() { + let calendars = get_system_calendars().unwrap(); + for calendar in calendars { + assert!(!calendar.0.is_empty(), "Calendar locale is empty"); + assert!(calendar.0.is_ascii(), "Calendar locale form is not valid"); + assert!(!calendar.1.is_empty(), "Calendar identifier is empty"); + assert!( + calendar.1.is_ascii(), + "Calendar identifier form is not valid" + ); + } + } + + #[test] + fn test_time_zone() { + let time_zone = get_system_timezone().unwrap(); + assert!(!time_zone.is_empty(), "Couldn't retreive time_zone"); + assert!(time_zone.is_ascii(), "Invalid TimeZone format"); + } +}