Skip to main content

apk/res/
config.rs

1//! `ResTable_config`: the device configuration a resource variant targets.
2//!
3//! [`Config`] preserves known configuration fields and offers typed accessors
4//! for common qualifiers. Its [`Display`](fmt::Display) and
5//! [`FromStr`] implementations use the resource directory syntax, in aapt2's
6//! qualifier order: `fr-rCA`, `b+zh+Hant+TW`, `night-xhdpi`, `anydpi-v26`.
7
8use std::fmt;
9use std::str::FromStr;
10
11use binrw::binrw;
12
13use super::chunk::read_padded;
14
15/// The serialized form of `ResTable_config` (64 bytes).
16#[binrw]
17#[brw(little)]
18#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
19pub(crate) struct RawConfig {
20    #[bw(calc = Self::SIZE as u32)]
21    #[br(temp)]
22    _size: u32,
23    mcc: u16,
24    mnc: u16,
25    language: [u8; 2],
26    country: [u8; 2],
27    orientation: u8,
28    touchscreen: u8,
29    density: u16,
30    keyboard: u8,
31    navigation: u8,
32    input_flags: u8,
33    grammatical_inflection: u8,
34    screen_width: u16,
35    screen_height: u16,
36    sdk_version: u16,
37    minor_version: u16,
38    screen_layout: u8,
39    ui_mode: u8,
40    smallest_screen_width_dp: u16,
41    screen_width_dp: u16,
42    screen_height_dp: u16,
43    locale_script: [u8; 4],
44    locale_variant: [u8; 8],
45    screen_layout2: u8,
46    color_mode: u8,
47    _screen_config_pad2: u16,
48    locale_script_was_computed: u8,
49    locale_numbering_system: [u8; 8],
50    _pad: [u8; 3],
51}
52
53impl RawConfig {
54    pub const SIZE: usize = 64;
55}
56
57/// A set of resource qualifiers. `Config::default()` matches any device.
58#[derive(Clone, Copy, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
59pub struct Config(pub(crate) RawConfig);
60
61/// Screen density buckets (`ResTable_config::density`).
62#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
63pub struct Density(pub u16);
64
65impl Density {
66    pub const ANY: Self = Self(0);
67    pub const LDPI: Self = Self(120);
68    pub const MDPI: Self = Self(160);
69    pub const TVDPI: Self = Self(213);
70    pub const HDPI: Self = Self(240);
71    pub const XHDPI: Self = Self(320);
72    pub const XXHDPI: Self = Self(480);
73    pub const XXXHDPI: Self = Self(640);
74    /// `anydpi`: scalable resources such as vector drawables.
75    pub const ANYDPI: Self = Self(0xfffe);
76    /// `nodpi`: never scaled.
77    pub const NODPI: Self = Self(0xffff);
78
79    /// The densities Android launchers request icons in, lowest first.
80    pub const LAUNCHER: [Self; 5] = [Self::MDPI, Self::HDPI, Self::XHDPI, Self::XXHDPI, Self::XXXHDPI];
81
82    const NAMES: [(Self, &'static str); 9] = [
83        (Self::LDPI, "ldpi"),
84        (Self::MDPI, "mdpi"),
85        (Self::TVDPI, "tvdpi"),
86        (Self::HDPI, "hdpi"),
87        (Self::XHDPI, "xhdpi"),
88        (Self::XXHDPI, "xxhdpi"),
89        (Self::XXXHDPI, "xxxhdpi"),
90        (Self::ANYDPI, "anydpi"),
91        (Self::NODPI, "nodpi"),
92    ];
93}
94
95impl fmt::Display for Density {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        match Self::NAMES.iter().find(|(d, _)| d == self) {
98            Some((_, name)) => f.write_str(name),
99            None => write!(f, "{}dpi", self.0),
100        }
101    }
102}
103
104/// `night` / `notnight`.
105#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
106pub enum NightMode {
107    #[default]
108    Any,
109    No,
110    Yes,
111}
112
113/// `port` / `land`.
114#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
115pub enum Orientation {
116    #[default]
117    Any,
118    Portrait,
119    Landscape,
120    Square,
121}
122
123/// `ldltr` / `ldrtl`.
124#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
125pub enum LayoutDirection {
126    #[default]
127    Any,
128    Ltr,
129    Rtl,
130}
131
132const MASK_NIGHT: u8 = 0x30;
133const UI_TYPE_VR_HEADSET: u8 = 0x07;
134const MASK_UI_TYPE: u8 = 0x0f;
135const MASK_LAYOUT_DIR: u8 = 0xc0;
136const MASK_SCREEN_SIZE: u8 = 0x0f;
137const MASK_SCREEN_LONG: u8 = 0x30;
138const MASK_ROUND: u8 = 0x03;
139const MASK_WIDE_GAMUT: u8 = 0x03;
140const MASK_HDR: u8 = 0x0c;
141const MASK_KEYS_HIDDEN: u8 = 0x03;
142const MASK_NAV_HIDDEN: u8 = 0x0c;
143
144impl Config {
145    /// A configuration with only a locale.
146    pub fn for_locale(locale: Locale) -> Self {
147        Self::default().with_locale(locale)
148    }
149
150    /// A configuration with only a density.
151    pub fn for_density(density: Density) -> Self {
152        Self::default().with_density(density)
153    }
154
155    pub fn is_default(&self) -> bool {
156        *self == Self::default()
157    }
158
159    pub fn locale(&self) -> Option<Locale> {
160        Locale::from_raw(&self.0)
161    }
162
163    pub fn with_locale(mut self, locale: Locale) -> Self {
164        locale.write_raw(&mut self.0);
165        self
166    }
167
168    pub fn density(&self) -> Density {
169        Density(self.0.density)
170    }
171
172    pub fn with_density(mut self, density: Density) -> Self {
173        self.0.density = density.0;
174        self
175    }
176
177    pub fn night_mode(&self) -> NightMode {
178        match self.0.ui_mode & MASK_NIGHT {
179            0x10 => NightMode::No,
180            0x20 => NightMode::Yes,
181            _ => NightMode::Any,
182        }
183    }
184
185    pub fn with_night_mode(mut self, mode: NightMode) -> Self {
186        let bits = match mode {
187            NightMode::Any => 0,
188            NightMode::No => 0x10,
189            NightMode::Yes => 0x20,
190        };
191        self.0.ui_mode = (self.0.ui_mode & !MASK_NIGHT) | bits;
192        self
193    }
194
195    pub fn orientation(&self) -> Orientation {
196        match self.0.orientation {
197            1 => Orientation::Portrait,
198            2 => Orientation::Landscape,
199            3 => Orientation::Square,
200            _ => Orientation::Any,
201        }
202    }
203
204    pub fn with_orientation(mut self, orientation: Orientation) -> Self {
205        self.0.orientation = orientation as u8;
206        self
207    }
208
209    pub fn layout_direction(&self) -> LayoutDirection {
210        match self.0.screen_layout & MASK_LAYOUT_DIR {
211            0x40 => LayoutDirection::Ltr,
212            0x80 => LayoutDirection::Rtl,
213            _ => LayoutDirection::Any,
214        }
215    }
216
217    pub fn with_layout_direction(mut self, direction: LayoutDirection) -> Self {
218        self.0.screen_layout = (self.0.screen_layout & !MASK_LAYOUT_DIR) | ((direction as u8) << 6);
219        self
220    }
221
222    /// The minimum API level (`-vNN`), or `0` when unqualified.
223    pub fn sdk_version(&self) -> u16 {
224        self.0.sdk_version
225    }
226
227    pub fn with_sdk_version(mut self, api_level: u16) -> Self {
228        self.0.sdk_version = api_level;
229        self
230    }
231
232    pub fn smallest_screen_width_dp(&self) -> u16 {
233        self.0.smallest_screen_width_dp
234    }
235
236    pub fn with_smallest_screen_width_dp(mut self, dp: u16) -> Self {
237        self.0.smallest_screen_width_dp = dp;
238        self
239    }
240
241    /// The size of the `ResTable_config` this crate writes.
242    pub(crate) const SIZE: usize = RawConfig::SIZE;
243
244    /// Reads a `ResTable_config`, honoring its own size field: fields past
245    /// it (in configurations written for older releases) read as zero, and
246    /// fields this crate does not know (from newer ones) are ignored.
247    pub(crate) fn read(bytes: &[u8]) -> Self {
248        let declared = bytes.get(..4).map_or(0, |size| u32::from_le_bytes(size.try_into().expect("four bytes")));
249        let len = bytes.len().min(declared as usize).min(RawConfig::SIZE);
250        Self(read_padded(&bytes[..len]))
251    }
252
253    /// This configuration with the minimum API level aapt2 implies for its
254    /// qualifiers, so releases that predate a qualifier never select the
255    /// resource: `night` becomes `night-v8`, `anydpi` becomes `anydpi-v21`.
256    pub(crate) fn with_implied_sdk_version(mut self) -> Self {
257        let c = &self.0;
258        let implied = [
259            (c.grammatical_inflection != 0, 34),
260            (c.ui_mode & MASK_UI_TYPE == UI_TYPE_VR_HEADSET || c.color_mode & (MASK_WIDE_GAMUT | MASK_HDR) != 0, 26),
261            (c.screen_layout2 & MASK_ROUND != 0, 23),
262            (c.density == Density::ANYDPI.0, 21),
263            (c.smallest_screen_width_dp != 0 || c.screen_width_dp != 0 || c.screen_height_dp != 0, 13),
264            (c.ui_mode != 0, 8),
265            (c.screen_layout & (MASK_SCREEN_SIZE | MASK_SCREEN_LONG) != 0 || c.density != 0, 4),
266        ];
267        let implied = implied.into_iter().filter(|(applies, _)| *applies).map(|(_, level)| level).max();
268        self.0.sdk_version = self.0.sdk_version.max(implied.unwrap_or(0));
269        self
270    }
271
272    /// The `ACONFIGURATION_*` bits in which this configuration differs from
273    /// the default, as stored in `ResTable_typeSpec` entry flags.
274    pub(crate) fn change_flags(&self) -> u32 {
275        let (a, b) = (&self.0, &RawConfig::default());
276        let mut flags = 0;
277        let mut set = |differs: bool, bit: u32| {
278            if differs {
279                flags |= bit;
280            }
281        };
282        set(a.mcc != b.mcc, 0x0001);
283        set(a.mnc != b.mnc, 0x0002);
284        set(
285            a.language != b.language
286                || a.country != b.country
287                || a.locale_script != b.locale_script
288                || a.locale_variant != b.locale_variant
289                || a.locale_numbering_system != b.locale_numbering_system,
290            0x0004,
291        );
292        set(a.touchscreen != b.touchscreen, 0x0008);
293        set(a.keyboard != b.keyboard, 0x0010);
294        set(a.input_flags & (MASK_KEYS_HIDDEN | MASK_NAV_HIDDEN) != 0, 0x0020);
295        set(a.navigation != b.navigation, 0x0040);
296        set(a.orientation != b.orientation, 0x0080);
297        set(a.density != b.density, 0x0100);
298        set((a.screen_width, a.screen_height, a.screen_width_dp, a.screen_height_dp) != (0, 0, 0, 0), 0x0200);
299        set(a.sdk_version != 0 || a.minor_version != 0, 0x0400);
300        set(a.screen_layout & !MASK_LAYOUT_DIR != 0, 0x0800);
301        set(a.ui_mode != 0, 0x1000);
302        set(a.smallest_screen_width_dp != 0, 0x2000);
303        set(a.screen_layout & MASK_LAYOUT_DIR != 0, 0x4000);
304        set(a.screen_layout2 & MASK_ROUND != 0, 0x8000);
305        set(a.color_mode != 0, 0x10000);
306        set(a.grammatical_inflection != 0, 0x20000);
307        flags
308    }
309}
310
311/// Qualifier tables: (raw value, directory name).
312const SCREEN_SIZES: [(u8, &str); 4] = [(1, "small"), (2, "normal"), (3, "large"), (4, "xlarge")];
313const SCREEN_LONG: [(u8, &str); 2] = [(0x10, "notlong"), (0x20, "long")];
314const ROUND: [(u8, &str); 2] = [(1, "notround"), (2, "round")];
315const WIDE_GAMUT: [(u8, &str); 2] = [(1, "nowidecg"), (2, "widecg")];
316const HDR: [(u8, &str); 2] = [(0x04, "lowdr"), (0x08, "highdr")];
317const ORIENTATIONS: [(u8, &str); 3] = [(1, "port"), (2, "land"), (3, "square")];
318const UI_TYPES: [(u8, &str); 6] =
319    [(2, "desk"), (3, "car"), (4, "television"), (5, "appliance"), (6, "watch"), (7, "vrheadset")];
320const NIGHT: [(u8, &str); 2] = [(0x10, "notnight"), (0x20, "night")];
321const GENDERS: [(u8, &str); 3] = [(1, "neuter"), (2, "feminine"), (3, "masculine")];
322const TOUCHSCREENS: [(u8, &str); 3] = [(1, "notouch"), (2, "stylus"), (3, "finger")];
323const KEYS_HIDDEN: [(u8, &str); 3] = [(1, "keysexposed"), (2, "keyshidden"), (3, "keyssoft")];
324const KEYBOARDS: [(u8, &str); 3] = [(1, "nokeys"), (2, "qwerty"), (3, "12key")];
325const NAV_HIDDEN: [(u8, &str); 2] = [(0x04, "navexposed"), (0x08, "navhidden")];
326const NAVIGATIONS: [(u8, &str); 4] = [(1, "nonav"), (2, "dpad"), (3, "trackball"), (4, "wheel")];
327const LAYOUT_DIRECTIONS: [(u8, &str); 2] = [(0x40, "ldltr"), (0x80, "ldrtl")];
328/// `mnc00`, which is distinct from no MNC.
329const MNC_ZERO: u16 = 0xffff;
330
331fn value_of(table: &[(u8, &str)], name: &str) -> Option<u8> {
332    table.iter().find(|(_, n)| *n == name).map(|(v, _)| *v)
333}
334
335impl fmt::Display for Config {
336    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
337        let c = &self.0;
338        let mut parts = Parts { f, first: true };
339        if c.mcc != 0 {
340            parts.push(format_args!("mcc{:03}", c.mcc))?;
341        }
342        if c.mnc != 0 {
343            parts.push(format_args!("mnc{}", if c.mnc == MNC_ZERO { 0 } else { c.mnc }))?;
344        }
345        if let Some(locale) = self.locale() {
346            parts.push(format_args!("{}", locale.qualifier()))?;
347        }
348        parts.named(&GENDERS, c.grammatical_inflection)?;
349        parts.named(&LAYOUT_DIRECTIONS, c.screen_layout & MASK_LAYOUT_DIR)?;
350        for (value, prefix) in [(c.smallest_screen_width_dp, "sw"), (c.screen_width_dp, "w"), (c.screen_height_dp, "h")]
351        {
352            if value != 0 {
353                parts.push(format_args!("{prefix}{value}dp"))?;
354            }
355        }
356        parts.named(&SCREEN_SIZES, c.screen_layout & MASK_SCREEN_SIZE)?;
357        parts.named(&SCREEN_LONG, c.screen_layout & MASK_SCREEN_LONG)?;
358        parts.named(&ROUND, c.screen_layout2 & MASK_ROUND)?;
359        parts.named(&WIDE_GAMUT, c.color_mode & MASK_WIDE_GAMUT)?;
360        parts.named(&HDR, c.color_mode & MASK_HDR)?;
361        parts.named(&ORIENTATIONS, c.orientation)?;
362        parts.named(&UI_TYPES, c.ui_mode & MASK_UI_TYPE)?;
363        parts.named(&NIGHT, c.ui_mode & MASK_NIGHT)?;
364        if c.density != 0 {
365            parts.push(format_args!("{}", Density(c.density)))?;
366        }
367        parts.named(&TOUCHSCREENS, c.touchscreen)?;
368        parts.named(&KEYS_HIDDEN, c.input_flags & MASK_KEYS_HIDDEN)?;
369        parts.named(&KEYBOARDS, c.keyboard)?;
370        parts.named(&NAV_HIDDEN, c.input_flags & MASK_NAV_HIDDEN)?;
371        parts.named(&NAVIGATIONS, c.navigation)?;
372        if c.screen_width != 0 || c.screen_height != 0 {
373            parts.push(format_args!("{}x{}", c.screen_width, c.screen_height))?;
374        }
375        if c.sdk_version != 0 {
376            parts.push(format_args!("v{}", c.sdk_version))?;
377        }
378        Ok(())
379    }
380}
381
382/// Writes qualifiers separated by `-`.
383struct Parts<'a, 'f> {
384    f: &'a mut fmt::Formatter<'f>,
385    first: bool,
386}
387
388impl Parts<'_, '_> {
389    fn push(&mut self, part: fmt::Arguments<'_>) -> fmt::Result {
390        if !std::mem::replace(&mut self.first, false) {
391            self.f.write_str("-")?;
392        }
393        self.f.write_fmt(part)
394    }
395
396    /// Writes the name of `value` in `table`, if it has one.
397    fn named(&mut self, table: &[(u8, &str)], value: u8) -> fmt::Result {
398        match table.iter().find(|(v, _)| *v == value) {
399            Some((_, name)) => self.push(format_args!("{name}")),
400            None => Ok(()),
401        }
402    }
403}
404
405impl fmt::Debug for Config {
406    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
407        if self.is_default() { f.write_str("Config(default)") } else { write!(f, "Config({self})") }
408    }
409}
410
411/// Why a resource qualifier string was rejected.
412#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
413#[non_exhaustive]
414pub enum QualifierError {
415    #[error("unrecognized resource qualifier {qualifier:?}")]
416    Unrecognized { qualifier: String },
417    #[error("resource qualifier {qualifier:?} is repeated, conflicts with another, or is out of order")]
418    Misplaced { qualifier: String },
419    #[error(transparent)]
420    Locale(#[from] LocaleError),
421}
422
423/// A qualifier position. Each takes at most one qualifier, in this order.
424#[derive(Clone, Copy)]
425enum Stage {
426    Locale,
427    /// Applies a matching qualifier and reports whether it matched.
428    Axis(fn(&mut RawConfig, &str) -> bool),
429}
430
431/// The qualifier positions in aapt2's order.
432const STAGES: [Stage; 24] = [
433    // Exactly three digits, not all zero.
434    Stage::Axis(|raw, part| set(&mut raw.mcc, number(part, "mcc", "").filter(|&mcc| part.len() == 6 && mcc != 0))),
435    // One to three digits.
436    Stage::Axis(|raw, part| {
437        let mnc = number(part, "mnc", "").filter(|_| part.len() <= 6);
438        set(&mut raw.mnc, mnc.map(|mnc| if mnc == 0 { MNC_ZERO } else { mnc }))
439    }),
440    Stage::Locale,
441    Stage::Axis(|raw, part| set(&mut raw.grammatical_inflection, value_of(&GENDERS, part))),
442    Stage::Axis(|raw, part| or(&mut raw.screen_layout, value_of(&LAYOUT_DIRECTIONS, part))),
443    Stage::Axis(|raw, part| set(&mut raw.smallest_screen_width_dp, number(part, "sw", "dp"))),
444    Stage::Axis(|raw, part| set(&mut raw.screen_width_dp, number(part, "w", "dp"))),
445    Stage::Axis(|raw, part| set(&mut raw.screen_height_dp, number(part, "h", "dp"))),
446    Stage::Axis(|raw, part| or(&mut raw.screen_layout, value_of(&SCREEN_SIZES, part))),
447    Stage::Axis(|raw, part| or(&mut raw.screen_layout, value_of(&SCREEN_LONG, part))),
448    Stage::Axis(|raw, part| or(&mut raw.screen_layout2, value_of(&ROUND, part))),
449    Stage::Axis(|raw, part| or(&mut raw.color_mode, value_of(&WIDE_GAMUT, part))),
450    Stage::Axis(|raw, part| or(&mut raw.color_mode, value_of(&HDR, part))),
451    Stage::Axis(|raw, part| set(&mut raw.orientation, value_of(&ORIENTATIONS, part))),
452    Stage::Axis(|raw, part| or(&mut raw.ui_mode, value_of(&UI_TYPES, part))),
453    Stage::Axis(|raw, part| or(&mut raw.ui_mode, value_of(&NIGHT, part))),
454    Stage::Axis(|raw, part| set(&mut raw.density, density(part))),
455    Stage::Axis(|raw, part| set(&mut raw.touchscreen, value_of(&TOUCHSCREENS, part))),
456    Stage::Axis(|raw, part| or(&mut raw.input_flags, value_of(&KEYS_HIDDEN, part))),
457    Stage::Axis(|raw, part| set(&mut raw.keyboard, value_of(&KEYBOARDS, part))),
458    Stage::Axis(|raw, part| or(&mut raw.input_flags, value_of(&NAV_HIDDEN, part))),
459    Stage::Axis(|raw, part| set(&mut raw.navigation, value_of(&NAVIGATIONS, part))),
460    Stage::Axis(|raw, part| {
461        let side = |digits| number(digits, "", "").filter(|&side| side != 0);
462        let size = part.split_once('x').and_then(|(w, h)| Some((side(w)?, side(h)?)));
463        // Like aapt2, require the longer side first.
464        size.filter(|(w, h)| w >= h).map(|(w, h)| (raw.screen_width, raw.screen_height) = (w, h)).is_some()
465    }),
466    Stage::Axis(|raw, part| set(&mut raw.sdk_version, number(part, "v", ""))),
467];
468
469impl Stage {
470    /// Whether this position would take `part` on its own.
471    fn accepts(self, part: &str) -> bool {
472        match self {
473            Self::Locale => looks_like_language(part),
474            Self::Axis(apply) => apply(&mut RawConfig::default(), part),
475        }
476    }
477}
478
479fn set<T>(field: &mut T, value: Option<T>) -> bool {
480    value.map(|value| *field = value).is_some()
481}
482
483/// Sets bits in a field that packs several qualifiers.
484fn or(field: &mut u8, value: Option<u8>) -> bool {
485    value.map(|value| *field |= value).is_some()
486}
487
488/// The number in `<prefix><number><suffix>`.
489fn number(part: &str, prefix: &str, suffix: &str) -> Option<u16> {
490    let digits = part.strip_prefix(prefix)?.strip_suffix(suffix)?;
491    digits.bytes().all(|b| b.is_ascii_digit()).then(|| digits.parse().ok()).flatten()
492}
493
494fn density(part: &str) -> Option<u16> {
495    match Density::NAMES.iter().find(|(_, name)| *name == part) {
496        Some((density, _)) => Some(density.0),
497        None => number(part, "", "dpi").filter(|&dpi| dpi != 0),
498    }
499}
500
501impl FromStr for Config {
502    type Err = QualifierError;
503
504    /// Parses resource directory qualifiers, e.g. `fr-rCA-night-xxhdpi-v26`.
505    /// Like aapt2, each kind of qualifier may appear once, in a fixed order.
506    fn from_str(text: &str) -> Result<Self, QualifierError> {
507        let mut raw = RawConfig::default();
508        let mut parts = text.split('-').filter(|_| !text.is_empty()).peekable();
509        let mut next = 0;
510        while let Some(part) = parts.next() {
511            let lower = part.to_ascii_lowercase();
512            let mut taken = None;
513            for (position, &stage) in STAGES.iter().enumerate().skip(next) {
514                let matched = match stage {
515                    Stage::Axis(apply) => apply(&mut raw, &lower),
516                    Stage::Locale if looks_like_language(&lower) => {
517                        let region = parts.next_if(|next| !lower.starts_with("b+") && is_region_qualifier(next));
518                        Locale::from_qualifier(&lower, region)?.write_raw(&mut raw);
519                        true
520                    }
521                    Stage::Locale => false,
522                };
523                if matched {
524                    taken = Some(position);
525                    break;
526                }
527            }
528            next = match taken {
529                Some(position) => position + 1,
530                None if STAGES[..next].iter().any(|stage| stage.accepts(&lower)) => {
531                    return Err(QualifierError::Misplaced { qualifier: part.to_owned() });
532                }
533                None => return Err(QualifierError::Unrecognized { qualifier: part.to_owned() }),
534            };
535        }
536        Ok(Self(raw))
537    }
538}
539
540/// `en`, `fil`, or `b+zh+Hant` (but not `car`, a UI mode).
541fn looks_like_language(part: &str) -> bool {
542    part.starts_with("b+")
543        || ((2..=3).contains(&part.len()) && part.bytes().all(|b| b.is_ascii_alphabetic()) && part != "car")
544}
545
546/// Android's `rUS` region form, in any case.
547fn is_region_qualifier(part: &str) -> bool {
548    part.len() == 3 && part.starts_with(['r', 'R']) && part[1..].bytes().all(|b| b.is_ascii_alphabetic())
549}
550
551/// A BCP 47 variant: 5 to 8 letters and digits, or a digit and 3 more.
552fn is_variant(subtag: &str) -> bool {
553    let alphanumeric = subtag.bytes().all(|b| b.is_ascii_alphanumeric());
554    alphanumeric && ((5..=8).contains(&subtag.len()) || (subtag.len() == 4 && subtag.as_bytes()[0].is_ascii_digit()))
555}
556
557/// Why a locale was rejected.
558#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
559#[non_exhaustive]
560pub enum LocaleError {
561    #[error("locale is empty")]
562    Empty,
563    #[error("language {language:?} must be 2 or 3 ASCII letters")]
564    InvalidLanguage { language: String },
565    #[error("locale subtag {subtag:?} is not a script, region, or variant in its place")]
566    InvalidSubtag { subtag: String },
567}
568
569/// A locale: a language with optional script, region, and variant.
570///
571/// Parses BCP 47 tags (`zh-Hant-TW`, `es-419`, `ca-ES-valencia`) as well as
572/// Android's qualifier forms (`fr-rCA`, `b+sr+Latn`).
573#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
574pub struct Locale {
575    language: Subtag<3>,
576    script: Option<Subtag<4>>,
577    region: Option<Subtag<3>>,
578    variant: Option<Subtag<8>>,
579}
580
581/// A short ASCII subtag, stored inline.
582#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
583struct Subtag<const N: usize> {
584    bytes: [u8; N],
585    len: u8,
586}
587
588impl<const N: usize> Subtag<N> {
589    /// `text` if it is ASCII of at most `N` bytes.
590    fn new(text: &str) -> Option<Self> {
591        let mut bytes = [0; N];
592        bytes.get_mut(..text.len()).filter(|_| text.is_ascii())?.copy_from_slice(text.as_bytes());
593        Some(Self { bytes, len: text.len() as u8 })
594    }
595
596    fn as_str(&self) -> &str {
597        std::str::from_utf8(&self.bytes[..usize::from(self.len)]).expect("subtags are ASCII")
598    }
599}
600
601impl Locale {
602    pub fn parse(text: &str) -> Result<Self, LocaleError> {
603        let (tag, separators) = match text.strip_prefix("b+") {
604            Some(rest) => (rest, ['+', '+']),
605            None => (text, ['-', '_']),
606        };
607        let mut subtags = tag.split(separators);
608        let language = subtags.next().filter(|l| !l.is_empty()).ok_or(LocaleError::Empty)?;
609        let invalid_language = || LocaleError::InvalidLanguage { language: language.to_owned() };
610        if !(2..=3).contains(&language.len()) || !language.bytes().all(|b| b.is_ascii_alphabetic()) {
611            return Err(invalid_language());
612        }
613        let language = Subtag::new(&language.to_ascii_lowercase()).ok_or_else(invalid_language)?;
614        let mut locale = Self { language, script: None, region: None, variant: None };
615        // Subtags come in order: script, region, variant; each at most once.
616        for subtag in subtags {
617            let invalid = || LocaleError::InvalidSubtag { subtag: subtag.to_owned() };
618            let region = if is_region_qualifier(subtag) { &subtag[1..] } else { subtag };
619            let alphabetic = |s: &str| s.bytes().all(|b| b.is_ascii_alphabetic());
620            let (script_open, region_open) =
621                (locale.script.is_none() && locale.region.is_none(), locale.region.is_none());
622            if locale.variant.is_some() {
623                return Err(invalid());
624            } else if script_open && subtag.len() == 4 && alphabetic(subtag) {
625                let script = subtag[..1].to_ascii_uppercase() + &subtag[1..].to_ascii_lowercase();
626                locale.script = Some(Subtag::new(&script).ok_or_else(invalid)?);
627            } else if region_open && region.len() == 2 && alphabetic(region) {
628                locale.region = Some(Subtag::new(&region.to_ascii_uppercase()).ok_or_else(invalid)?);
629            } else if region_open && region.len() == 3 && region.bytes().all(|b| b.is_ascii_digit()) {
630                locale.region = Some(Subtag::new(region).ok_or_else(invalid)?);
631            } else if is_variant(subtag) {
632                locale.variant = Some(Subtag::new(&subtag.to_ascii_lowercase()).ok_or_else(invalid)?);
633            } else {
634                return Err(invalid());
635            }
636        }
637        Ok(locale)
638    }
639
640    /// Parses a locale qualifier: a language (or a `b+` tag) and an
641    /// optional `rXX` region part.
642    fn from_qualifier(language: &str, region: Option<&str>) -> Result<Self, LocaleError> {
643        let mut locale = Self::parse(language)?;
644        if let Some(region) = region {
645            let invalid = || LocaleError::InvalidSubtag { subtag: region.to_owned() };
646            locale.region = Some(Subtag::new(&region[1..].to_ascii_uppercase()).ok_or_else(invalid)?);
647        }
648        Ok(locale)
649    }
650
651    pub fn language(&self) -> &str {
652        self.language.as_str()
653    }
654
655    pub fn script(&self) -> Option<&str> {
656        self.script.as_ref().map(Subtag::as_str)
657    }
658
659    pub fn region(&self) -> Option<&str> {
660        self.region.as_ref().map(Subtag::as_str)
661    }
662
663    /// The variant, such as `valencia`.
664    pub fn variant(&self) -> Option<&str> {
665        self.variant.as_ref().map(Subtag::as_str)
666    }
667
668    /// The resource directory qualifier: `fr`, `fr-rCA`, or `b+zh+Hant+TW`.
669    pub fn qualifier(&self) -> impl fmt::Display + '_ {
670        Qualifier(self)
671    }
672
673    fn from_raw(raw: &RawConfig) -> Option<Self> {
674        let language = unpack(raw.language, b'a')?;
675        let region = unpack(raw.country, b'0');
676        let script = (raw.locale_script != [0; 4] && raw.locale_script_was_computed == 0)
677            .then(|| std::str::from_utf8(&raw.locale_script).ok().and_then(Subtag::new))
678            .flatten();
679        let variant = raw.locale_variant.split(|&b| b == 0).next().filter(|v| !v.is_empty());
680        let variant = variant.and_then(|v| std::str::from_utf8(v).ok()).and_then(Subtag::new);
681        Some(Self { language, script, region, variant })
682    }
683
684    fn write_raw(&self, raw: &mut RawConfig) {
685        raw.language = pack(self.language, b'a');
686        raw.country = self.region.map_or([0; 2], |region| pack(region, b'0'));
687        raw.locale_script = self.script.map_or([0; 4], |script| script.bytes);
688        raw.locale_script_was_computed = 0;
689        raw.locale_variant = self.variant.map_or([0; 8], |variant| variant.bytes);
690    }
691}
692
693/// [`Locale::qualifier`].
694struct Qualifier<'a>(&'a Locale);
695
696impl fmt::Display for Qualifier<'_> {
697    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
698        let locale = self.0;
699        match (locale.script(), locale.region(), locale.variant()) {
700            (None, None, None) => f.write_str(locale.language()),
701            (None, Some(region), None) if region.len() == 2 => write!(f, "{}-r{region}", locale.language()),
702            (script, region, variant) => {
703                write!(f, "b+{}", locale.language())?;
704                [script, region, variant].into_iter().flatten().try_for_each(|part| write!(f, "+{part}"))
705            }
706        }
707    }
708}
709
710impl FromStr for Locale {
711    type Err = LocaleError;
712    fn from_str(text: &str) -> Result<Self, LocaleError> {
713        Self::parse(text)
714    }
715}
716
717impl fmt::Display for Locale {
718    /// Formats the locale as a BCP 47 tag, e.g. `zh-Hant-TW`.
719    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
720        f.write_str(self.language())?;
721        [self.script(), self.region(), self.variant()].into_iter().flatten().try_for_each(|part| write!(f, "-{part}"))
722    }
723}
724
725impl fmt::Debug for Locale {
726    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
727        write!(f, "Locale({self})")
728    }
729}
730
731/// `packLanguageOrRegion` from AOSP: two characters are stored verbatim;
732/// three are packed into 15 bits with the high bit set.
733fn pack(subtag: Subtag<3>, base: u8) -> [u8; 2] {
734    match *subtag.as_str().as_bytes() {
735        [a, b] => [a, b],
736        [a, b, c] => {
737            let [first, second, third] = [a, b, c].map(|x| x.wrapping_sub(base) & 0x7f);
738            [0x80 | (third << 2) | (second >> 3), (second << 5) | first]
739        }
740        _ => [0; 2],
741    }
742}
743
744fn unpack(raw: [u8; 2], base: u8) -> Option<Subtag<3>> {
745    if raw[0] & 0x80 != 0 {
746        let first = raw[1] & 0x1f;
747        let second = ((raw[1] & 0xe0) >> 5) | ((raw[0] & 0x03) << 3);
748        let third = (raw[0] & 0x7c) >> 2;
749        let bytes = [first, second, third].map(|c| c.wrapping_add(base));
750        return bytes.is_ascii().then_some(Subtag { bytes, len: 3 });
751    }
752    (raw[0] != 0 && raw.is_ascii()).then_some(Subtag { bytes: [raw[0], raw[1], 0], len: 2 })
753}
754
755#[cfg(test)]
756mod tests {
757    use super::*;
758    use crate::res::chunk::Put;
759
760    #[test]
761    fn locales_parse_in_every_notation() {
762        for (input, bcp47, qualifier) in [
763            ("fr", "fr", "fr"),
764            ("pt-BR", "pt-BR", "pt-rBR"),
765            ("pt-rBR", "pt-BR", "pt-rBR"),
766            ("zh-Hant-TW", "zh-Hant-TW", "b+zh+Hant+TW"),
767            ("b+sr+Latn", "sr-Latn", "b+sr+Latn"),
768            ("es-419", "es-419", "b+es+419"),
769            ("fil-PH", "fil-PH", "fil-rPH"),
770            ("b+ca+ES+valencia", "ca-ES-valencia", "b+ca+ES+valencia"),
771            ("de-1996", "de-1996", "b+de+1996"),
772        ] {
773            let locale = Locale::parse(input).unwrap();
774            assert_eq!(locale.to_string(), bcp47, "{input}");
775            assert_eq!(locale.qualifier().to_string(), qualifier, "{input}");
776        }
777        assert_eq!(Locale::parse(""), Err(LocaleError::Empty));
778        assert_eq!(Locale::parse("french"), Err(LocaleError::InvalidLanguage { language: "french".into() }));
779        for (tag, subtag) in [("en-USA", "USA"), ("en-Latn-Latn", "Latn"), ("en-US-GB", "GB"), ("ca-valencia-ES", "ES")]
780        {
781            assert_eq!(Locale::parse(tag), Err(LocaleError::InvalidSubtag { subtag: subtag.into() }), "{tag}");
782        }
783    }
784
785    #[test]
786    fn three_letter_codes_pack_and_unpack() {
787        for (text, base) in [("fil", b'a'), ("419", b'0'), ("ast", b'a')] {
788            let subtag = Subtag::new(text).unwrap();
789            assert_eq!(unpack(pack(subtag, base), base).unwrap().as_str(), text);
790        }
791    }
792
793    #[test]
794    fn qualifier_strings_round_trip_in_aapt2_order() {
795        for text in [
796            "",
797            "fr-rCA",
798            "b+zh+Hant+TW-night-xxhdpi",
799            "ldrtl-sw600dp-land-v17",
800            "mcc310-mnc4-en-rUS",
801            "mcc001-mnc0-en",
802            "mcc310-mnc0",
803            "b+ca+ES+valencia-v21",
804            "round-watch-anydpi-v26",
805            "large-long-notnight-tvdpi-finger-keyshidden-qwerty-navexposed-dpad-800x480-v4",
806        ] {
807            let config: Config = text.parse().unwrap();
808            assert_eq!(config.to_string(), text);
809        }
810    }
811
812    #[test]
813    fn qualifiers_are_rejected_where_aapt2_rejects_them() {
814        use QualifierError::*;
815        for (text, error) in [
816            ("xhdpi-bogus", Unrecognized { qualifier: "bogus".into() }),
817            ("night-notnight", Misplaced { qualifier: "notnight".into() }),
818            ("ldltr-ldrtl", Misplaced { qualifier: "ldrtl".into() }),
819            ("v21-anydpi", Misplaced { qualifier: "anydpi".into() }),
820            ("night-fr", Misplaced { qualifier: "fr".into() }),
821            ("xhdpi-xhdpi", Misplaced { qualifier: "xhdpi".into() }),
822            ("480x800", Unrecognized { qualifier: "480x800".into() }),
823            ("sw+1dp", Unrecognized { qualifier: "sw+1dp".into() }),
824            ("+800x480", Unrecognized { qualifier: "+800x480".into() }),
825            ("800x0", Unrecognized { qualifier: "800x0".into() }),
826            ("mcc1", Unrecognized { qualifier: "mcc1".into() }),
827            ("mcc0", Unrecognized { qualifier: "mcc0".into() }),
828            ("mcc000", Unrecognized { qualifier: "mcc000".into() }),
829            ("mcc0310", Unrecognized { qualifier: "mcc0310".into() }),
830            ("mnc0001", Unrecognized { qualifier: "mnc0001".into() }),
831            ("mnc65535", Unrecognized { qualifier: "mnc65535".into() }),
832            ("0dpi", Unrecognized { qualifier: "0dpi".into() }),
833        ] {
834            assert_eq!(text.parse::<Config>(), Err(error), "{text}");
835        }
836    }
837
838    #[test]
839    fn qualifiers_are_case_insensitive_like_aapt2() {
840        for (text, canonical) in [("en-RUS", "en-rUS"), ("B+en", "en"), ("B+ZH+HANT", "b+zh+Hant"), ("XHDPI", "xhdpi")]
841        {
842            assert_eq!(text.parse::<Config>().unwrap().to_string(), canonical, "{text}");
843        }
844    }
845
846    #[test]
847    fn implied_versions_match_aapt2() {
848        for (text, implied) in [
849            ("", ""),
850            ("ldrtl", "ldrtl"),
851            ("fr", "fr"),
852            ("large", "large-v4"),
853            ("xhdpi", "xhdpi-v4"),
854            ("nodpi", "nodpi-v4"),
855            ("night", "night-v8"),
856            ("car", "car-v8"),
857            ("sw600dp", "sw600dp-v13"),
858            ("anydpi", "anydpi-v21"),
859            ("anydpi-v26", "anydpi-v26"),
860            ("round", "round-v23"),
861            ("widecg", "widecg-v26"),
862            ("vrheadset", "vrheadset-v26"),
863            ("feminine", "feminine-v34"),
864            ("night-xhdpi", "night-xhdpi-v8"),
865        ] {
866            let config: Config = text.parse().unwrap();
867            assert_eq!(config.with_implied_sdk_version().to_string(), implied, "{text}");
868        }
869    }
870
871    #[test]
872    fn configurations_honor_their_size_field() {
873        let config: Config = "fr-xhdpi-v21".parse().unwrap();
874        let mut bytes = Vec::new();
875        bytes.put(&config.0);
876        assert_eq!(bytes.len(), Config::SIZE);
877        assert_eq!(Config::read(&bytes), config);
878        // A 24-byte configuration ends before the SDK version.
879        bytes[..4].copy_from_slice(&24u32.to_le_bytes());
880        assert_eq!(Config::read(&bytes).to_string(), "fr-xhdpi");
881    }
882
883    #[test]
884    fn change_flags_track_qualifiers() {
885        let config: Config = "fr-night-xhdpi-v21".parse().unwrap();
886        assert_eq!(config.change_flags(), 0x0004 | 0x1000 | 0x0100 | 0x0400);
887        assert_eq!(Config::default().change_flags(), 0);
888    }
889}