1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
/*!
Provides ability to get/set the current process locale.

This module allows the client to fetch and set locales for different
`Category` values, or for all. This is a core capability for clients
to be able to set the current locale for their process. If you only
plan to set the locale, get settings, and then reset the locale you
may want to look at the `_for_locale` version of settings functions.

## Example

```
use locale_types::{Locale, LocaleString};
use locale_settings::locale::{Category, get_locale, set_locale};
use std::str::FromStr;

let old_locale = get_locale(&Category::Currency);

if old_locale.is_ok() {
    if set_locale(&Locale::String(LocaleString::from_str("en_US").unwrap()), &Category::Currency) {
        // do something with new locale...
        if !set_locale(&old_locale.unwrap(), &Category::Currency) {
            panic!("Could not re-set the old locale");
        }
    } else {
        panic!("Could not set the new locale");
    }
} else {
    panic!("Could not save the existing locale");
}
```
*/

use crate::ffi::*;
use locale_types::{Locale, LocaleError, LocaleResult};
use std::ffi::CStr;
use std::os::raw;
use std::ptr;
use std::str::FromStr;

// ------------------------------------------------------------------------------------------------
// Public Types
// ------------------------------------------------------------------------------------------------

/// The different categories for which locale information may be
/// set. This implies that entirely different locales may be then
///specified for each category.
#[derive(Debug)]
pub enum Category {
    /// Affects the manner in which characters are classified by
    /// functions such as `isdigit` and so forth.
    CharacterTypes,
    /// Affects the manner in which currency data is formatted.
    Currency,
    /// Affects the display of messages.
    Message,
    /// Affects the manner in which numeric data is formatted.
    Numeric,
    /// Affects the manner in which strings are collated/sorted.
    StringCollation,
    /// Affects the manner in which date/time data is formatted.
    Time,
}

impl Category {
    pub(crate) fn all_code() -> u32 {
        LC_ALL
    }

    pub(crate) fn to_os_code(&self) -> u32 {
        match self {
            Category::StringCollation => LC_COLLATE,
            Category::CharacterTypes => LC_CTYPE,
            Category::Currency => LC_MONETARY,
            Category::Numeric => LC_NUMERIC,
            Category::Time => LC_TIME,
            Category::Message => LC_MESSAGES,
        }
    }

    #[allow(dead_code)]
    pub(crate) fn to_os_mask(&self) -> u32 {
        match self {
            Category::StringCollation => LC_COLLATE_MASK,
            Category::CharacterTypes => LC_CTYPE_MASK,
            Category::Currency => LC_MONETARY_MASK,
            Category::Numeric => LC_NUMERIC_MASK,
            Category::Time => LC_TIME_MASK,
            Category::Message => LC_MESSAGES_MASK,
        }
    }
}

// ------------------------------------------------------------------------------------------------
// Public Functions
// ------------------------------------------------------------------------------------------------

const DEFAULT_LOCALE: &str = "";
const QUERY_LOCALE: *const raw::c_char = ptr::null();

/// Set all locale categories to `new_locale`.
pub fn set_locale_all(new_locale: &Locale) -> bool {
    set_locale_wrapper(Category::all_code() as i32, &new_locale.to_string())
}

/// Set all locale categories, based on the `LC_ALL` and `LANG` environment
/// variables,  to `new_locale`.
pub fn set_locale_all_from_env() -> bool {
    set_locale_wrapper(Category::all_code() as i32, DEFAULT_LOCALE)
}

/// Set the  locale to `new_locale` for the `for_category` category  to `new_locale`.
pub fn set_locale(new_locale: &Locale, for_category: &Category) -> bool {
    set_locale_wrapper(for_category.to_os_code() as i32, &new_locale.to_string())
}

/// Set the  locale for the `for_category` category, based on the value
/// of the `LC_{category}` environment variables,  to `new_locale`.
pub fn set_locale_from_env(for_category: &Category) -> bool {
    set_locale_wrapper(for_category.to_os_code() as i32, DEFAULT_LOCALE)
}

/// Get the locale for the `for_category` category only.
pub fn get_locale(for_category: &Category) -> LocaleResult<Locale> {
    let category = for_category.to_os_code() as i32;
    unsafe {
        let c_str: *mut raw::c_char = setlocale(category, QUERY_LOCALE);
        debug!("setlocale({}, null) returned {:#?}", category, c_str);
        if c_str == ptr::null_mut::<raw::c_char>() {
            Err(LocaleError::OSError)
        } else {
            let r_str = CStr::from_ptr(c_str).to_string_lossy().into_owned();
            Ok(Locale::from_str(&r_str).unwrap())
        }
    }
}

// ------------------------------------------------------------------------------------------------
// Private Functions
// ------------------------------------------------------------------------------------------------

fn set_locale_wrapper(category: i32, new_locale: &str) -> bool {
    // this is a nice wrapper around the FFI function, it only really
    // does type transformation, logging, and error handling.
    unsafe {
        let c_str: *mut raw::c_char = setlocale(category, new_locale.as_ptr() as *const i8);
        debug!(
            "setlocale({}, {:#?}) returned {:#?}",
            category, new_locale, c_str
        );
        return !(c_str == ptr::null_mut::<raw::c_char>());
    }
}

// ------------------------------------------------------------------------------------------------
// Unit Tests
// ------------------------------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use crate::locale::*;
    use locale_types::{Locale, LocaleString};
    use std::str::FromStr;

    // --------------------------------------------------------------------------------------------
    #[test]
    fn test_get_locale() {
        set_locale_all(&Locale::POSIX);
        for category in [
            Category::CharacterTypes,
            Category::Currency,
            Category::Message,
            Category::Numeric,
            Category::StringCollation,
            Category::Time,
        ]
        .iter()
        {
            let result = get_locale(category);
            assert!(result.is_ok());
            assert_eq!(result.unwrap(), Locale::POSIX);
        }
    }

    // --------------------------------------------------------------------------------------------
    #[test]
    fn test_set_locale_all() {
        set_locale_all(&Locale::POSIX);
        for category in [
            Category::CharacterTypes,
            Category::Currency,
            Category::Message,
            Category::Numeric,
            Category::StringCollation,
            Category::Time,
        ]
        .iter()
        {
            let result = get_locale(category);
            assert!(result.is_ok());
            assert_eq!(result.unwrap(), Locale::POSIX);
        }
    }

    #[ignore]
    #[test]
    fn test_set_locale_one() {
        // set everything
        set_locale_all(&Locale::POSIX);

        // re-set currency
        let locale = Locale::String(LocaleString::from_str("en_US.UTF-8").unwrap());
        let result = set_locale(&locale, &Category::Currency);
        assert_eq!(result, true);

        // check currency is set correctly
        let new_setting = get_locale(&Category::Currency);
        assert_eq!(new_setting.unwrap(), locale);

        // check everything else is left as-was
        for category in [
            Category::CharacterTypes,
            Category::Message,
            Category::Numeric,
            Category::StringCollation,
            Category::Time,
        ]
        .iter()
        {
            let result = get_locale(category);
            assert!(result.is_ok());
            assert_eq!(result.unwrap(), Locale::POSIX);
        }
    }
}