use std::fmt;
use std::str::FromStr;
use binrw::binrw;
use super::chunk::read_padded;
#[binrw]
#[brw(little)]
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub(crate) struct RawConfig {
#[bw(calc = Self::SIZE as u32)]
#[br(temp)]
_size: u32,
mcc: u16,
mnc: u16,
language: [u8; 2],
country: [u8; 2],
orientation: u8,
touchscreen: u8,
density: u16,
keyboard: u8,
navigation: u8,
input_flags: u8,
grammatical_inflection: u8,
screen_width: u16,
screen_height: u16,
sdk_version: u16,
minor_version: u16,
screen_layout: u8,
ui_mode: u8,
smallest_screen_width_dp: u16,
screen_width_dp: u16,
screen_height_dp: u16,
locale_script: [u8; 4],
locale_variant: [u8; 8],
screen_layout2: u8,
color_mode: u8,
_screen_config_pad2: u16,
locale_script_was_computed: u8,
locale_numbering_system: [u8; 8],
_pad: [u8; 3],
}
impl RawConfig {
pub const SIZE: usize = 64;
}
#[derive(Clone, Copy, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Config(pub(crate) RawConfig);
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Density(pub u16);
impl Density {
pub const ANY: Self = Self(0);
pub const LDPI: Self = Self(120);
pub const MDPI: Self = Self(160);
pub const TVDPI: Self = Self(213);
pub const HDPI: Self = Self(240);
pub const XHDPI: Self = Self(320);
pub const XXHDPI: Self = Self(480);
pub const XXXHDPI: Self = Self(640);
pub const ANYDPI: Self = Self(0xfffe);
pub const NODPI: Self = Self(0xffff);
pub const LAUNCHER: [Self; 5] = [Self::MDPI, Self::HDPI, Self::XHDPI, Self::XXHDPI, Self::XXXHDPI];
const NAMES: [(Self, &'static str); 9] = [
(Self::LDPI, "ldpi"),
(Self::MDPI, "mdpi"),
(Self::TVDPI, "tvdpi"),
(Self::HDPI, "hdpi"),
(Self::XHDPI, "xhdpi"),
(Self::XXHDPI, "xxhdpi"),
(Self::XXXHDPI, "xxxhdpi"),
(Self::ANYDPI, "anydpi"),
(Self::NODPI, "nodpi"),
];
}
impl fmt::Display for Density {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match Self::NAMES.iter().find(|(d, _)| d == self) {
Some((_, name)) => f.write_str(name),
None => write!(f, "{}dpi", self.0),
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub enum NightMode {
#[default]
Any,
No,
Yes,
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub enum Orientation {
#[default]
Any,
Portrait,
Landscape,
Square,
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub enum LayoutDirection {
#[default]
Any,
Ltr,
Rtl,
}
const MASK_NIGHT: u8 = 0x30;
const UI_TYPE_VR_HEADSET: u8 = 0x07;
const MASK_UI_TYPE: u8 = 0x0f;
const MASK_LAYOUT_DIR: u8 = 0xc0;
const MASK_SCREEN_SIZE: u8 = 0x0f;
const MASK_SCREEN_LONG: u8 = 0x30;
const MASK_ROUND: u8 = 0x03;
const MASK_WIDE_GAMUT: u8 = 0x03;
const MASK_HDR: u8 = 0x0c;
const MASK_KEYS_HIDDEN: u8 = 0x03;
const MASK_NAV_HIDDEN: u8 = 0x0c;
impl Config {
pub fn for_locale(locale: Locale) -> Self {
Self::default().with_locale(locale)
}
pub fn for_density(density: Density) -> Self {
Self::default().with_density(density)
}
pub fn is_default(&self) -> bool {
*self == Self::default()
}
pub fn locale(&self) -> Option<Locale> {
Locale::from_raw(&self.0)
}
pub fn with_locale(mut self, locale: Locale) -> Self {
locale.write_raw(&mut self.0);
self
}
pub fn density(&self) -> Density {
Density(self.0.density)
}
pub fn with_density(mut self, density: Density) -> Self {
self.0.density = density.0;
self
}
pub fn night_mode(&self) -> NightMode {
match self.0.ui_mode & MASK_NIGHT {
0x10 => NightMode::No,
0x20 => NightMode::Yes,
_ => NightMode::Any,
}
}
pub fn with_night_mode(mut self, mode: NightMode) -> Self {
let bits = match mode {
NightMode::Any => 0,
NightMode::No => 0x10,
NightMode::Yes => 0x20,
};
self.0.ui_mode = (self.0.ui_mode & !MASK_NIGHT) | bits;
self
}
pub fn orientation(&self) -> Orientation {
match self.0.orientation {
1 => Orientation::Portrait,
2 => Orientation::Landscape,
3 => Orientation::Square,
_ => Orientation::Any,
}
}
pub fn with_orientation(mut self, orientation: Orientation) -> Self {
self.0.orientation = orientation as u8;
self
}
pub fn layout_direction(&self) -> LayoutDirection {
match self.0.screen_layout & MASK_LAYOUT_DIR {
0x40 => LayoutDirection::Ltr,
0x80 => LayoutDirection::Rtl,
_ => LayoutDirection::Any,
}
}
pub fn with_layout_direction(mut self, direction: LayoutDirection) -> Self {
self.0.screen_layout = (self.0.screen_layout & !MASK_LAYOUT_DIR) | ((direction as u8) << 6);
self
}
pub fn sdk_version(&self) -> u16 {
self.0.sdk_version
}
pub fn with_sdk_version(mut self, api_level: u16) -> Self {
self.0.sdk_version = api_level;
self
}
pub fn smallest_screen_width_dp(&self) -> u16 {
self.0.smallest_screen_width_dp
}
pub fn with_smallest_screen_width_dp(mut self, dp: u16) -> Self {
self.0.smallest_screen_width_dp = dp;
self
}
pub(crate) const SIZE: usize = RawConfig::SIZE;
pub(crate) fn read(bytes: &[u8]) -> Self {
let declared = bytes.get(..4).map_or(0, |size| u32::from_le_bytes(size.try_into().expect("four bytes")));
let len = bytes.len().min(declared as usize).min(RawConfig::SIZE);
Self(read_padded(&bytes[..len]))
}
pub(crate) fn with_implied_sdk_version(mut self) -> Self {
let c = &self.0;
let implied = [
(c.grammatical_inflection != 0, 34),
(c.ui_mode & MASK_UI_TYPE == UI_TYPE_VR_HEADSET || c.color_mode & (MASK_WIDE_GAMUT | MASK_HDR) != 0, 26),
(c.screen_layout2 & MASK_ROUND != 0, 23),
(c.density == Density::ANYDPI.0, 21),
(c.smallest_screen_width_dp != 0 || c.screen_width_dp != 0 || c.screen_height_dp != 0, 13),
(c.ui_mode != 0, 8),
(c.screen_layout & (MASK_SCREEN_SIZE | MASK_SCREEN_LONG) != 0 || c.density != 0, 4),
];
let implied = implied.into_iter().filter(|(applies, _)| *applies).map(|(_, level)| level).max();
self.0.sdk_version = self.0.sdk_version.max(implied.unwrap_or(0));
self
}
pub(crate) fn change_flags(&self) -> u32 {
let (a, b) = (&self.0, &RawConfig::default());
let mut flags = 0;
let mut set = |differs: bool, bit: u32| {
if differs {
flags |= bit;
}
};
set(a.mcc != b.mcc, 0x0001);
set(a.mnc != b.mnc, 0x0002);
set(
a.language != b.language
|| a.country != b.country
|| a.locale_script != b.locale_script
|| a.locale_variant != b.locale_variant
|| a.locale_numbering_system != b.locale_numbering_system,
0x0004,
);
set(a.touchscreen != b.touchscreen, 0x0008);
set(a.keyboard != b.keyboard, 0x0010);
set(a.input_flags & (MASK_KEYS_HIDDEN | MASK_NAV_HIDDEN) != 0, 0x0020);
set(a.navigation != b.navigation, 0x0040);
set(a.orientation != b.orientation, 0x0080);
set(a.density != b.density, 0x0100);
set((a.screen_width, a.screen_height, a.screen_width_dp, a.screen_height_dp) != (0, 0, 0, 0), 0x0200);
set(a.sdk_version != 0 || a.minor_version != 0, 0x0400);
set(a.screen_layout & !MASK_LAYOUT_DIR != 0, 0x0800);
set(a.ui_mode != 0, 0x1000);
set(a.smallest_screen_width_dp != 0, 0x2000);
set(a.screen_layout & MASK_LAYOUT_DIR != 0, 0x4000);
set(a.screen_layout2 & MASK_ROUND != 0, 0x8000);
set(a.color_mode != 0, 0x10000);
set(a.grammatical_inflection != 0, 0x20000);
flags
}
}
const SCREEN_SIZES: [(u8, &str); 4] = [(1, "small"), (2, "normal"), (3, "large"), (4, "xlarge")];
const SCREEN_LONG: [(u8, &str); 2] = [(0x10, "notlong"), (0x20, "long")];
const ROUND: [(u8, &str); 2] = [(1, "notround"), (2, "round")];
const WIDE_GAMUT: [(u8, &str); 2] = [(1, "nowidecg"), (2, "widecg")];
const HDR: [(u8, &str); 2] = [(0x04, "lowdr"), (0x08, "highdr")];
const ORIENTATIONS: [(u8, &str); 3] = [(1, "port"), (2, "land"), (3, "square")];
const UI_TYPES: [(u8, &str); 6] =
[(2, "desk"), (3, "car"), (4, "television"), (5, "appliance"), (6, "watch"), (7, "vrheadset")];
const NIGHT: [(u8, &str); 2] = [(0x10, "notnight"), (0x20, "night")];
const GENDERS: [(u8, &str); 3] = [(1, "neuter"), (2, "feminine"), (3, "masculine")];
const TOUCHSCREENS: [(u8, &str); 3] = [(1, "notouch"), (2, "stylus"), (3, "finger")];
const KEYS_HIDDEN: [(u8, &str); 3] = [(1, "keysexposed"), (2, "keyshidden"), (3, "keyssoft")];
const KEYBOARDS: [(u8, &str); 3] = [(1, "nokeys"), (2, "qwerty"), (3, "12key")];
const NAV_HIDDEN: [(u8, &str); 2] = [(0x04, "navexposed"), (0x08, "navhidden")];
const NAVIGATIONS: [(u8, &str); 4] = [(1, "nonav"), (2, "dpad"), (3, "trackball"), (4, "wheel")];
const LAYOUT_DIRECTIONS: [(u8, &str); 2] = [(0x40, "ldltr"), (0x80, "ldrtl")];
const MNC_ZERO: u16 = 0xffff;
fn value_of(table: &[(u8, &str)], name: &str) -> Option<u8> {
table.iter().find(|(_, n)| *n == name).map(|(v, _)| *v)
}
impl fmt::Display for Config {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let c = &self.0;
let mut parts = Parts { f, first: true };
if c.mcc != 0 {
parts.push(format_args!("mcc{:03}", c.mcc))?;
}
if c.mnc != 0 {
parts.push(format_args!("mnc{}", if c.mnc == MNC_ZERO { 0 } else { c.mnc }))?;
}
if let Some(locale) = self.locale() {
parts.push(format_args!("{}", locale.qualifier()))?;
}
parts.named(&GENDERS, c.grammatical_inflection)?;
parts.named(&LAYOUT_DIRECTIONS, c.screen_layout & MASK_LAYOUT_DIR)?;
for (value, prefix) in [(c.smallest_screen_width_dp, "sw"), (c.screen_width_dp, "w"), (c.screen_height_dp, "h")]
{
if value != 0 {
parts.push(format_args!("{prefix}{value}dp"))?;
}
}
parts.named(&SCREEN_SIZES, c.screen_layout & MASK_SCREEN_SIZE)?;
parts.named(&SCREEN_LONG, c.screen_layout & MASK_SCREEN_LONG)?;
parts.named(&ROUND, c.screen_layout2 & MASK_ROUND)?;
parts.named(&WIDE_GAMUT, c.color_mode & MASK_WIDE_GAMUT)?;
parts.named(&HDR, c.color_mode & MASK_HDR)?;
parts.named(&ORIENTATIONS, c.orientation)?;
parts.named(&UI_TYPES, c.ui_mode & MASK_UI_TYPE)?;
parts.named(&NIGHT, c.ui_mode & MASK_NIGHT)?;
if c.density != 0 {
parts.push(format_args!("{}", Density(c.density)))?;
}
parts.named(&TOUCHSCREENS, c.touchscreen)?;
parts.named(&KEYS_HIDDEN, c.input_flags & MASK_KEYS_HIDDEN)?;
parts.named(&KEYBOARDS, c.keyboard)?;
parts.named(&NAV_HIDDEN, c.input_flags & MASK_NAV_HIDDEN)?;
parts.named(&NAVIGATIONS, c.navigation)?;
if c.screen_width != 0 || c.screen_height != 0 {
parts.push(format_args!("{}x{}", c.screen_width, c.screen_height))?;
}
if c.sdk_version != 0 {
parts.push(format_args!("v{}", c.sdk_version))?;
}
Ok(())
}
}
struct Parts<'a, 'f> {
f: &'a mut fmt::Formatter<'f>,
first: bool,
}
impl Parts<'_, '_> {
fn push(&mut self, part: fmt::Arguments<'_>) -> fmt::Result {
if !std::mem::replace(&mut self.first, false) {
self.f.write_str("-")?;
}
self.f.write_fmt(part)
}
fn named(&mut self, table: &[(u8, &str)], value: u8) -> fmt::Result {
match table.iter().find(|(v, _)| *v == value) {
Some((_, name)) => self.push(format_args!("{name}")),
None => Ok(()),
}
}
}
impl fmt::Debug for Config {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_default() { f.write_str("Config(default)") } else { write!(f, "Config({self})") }
}
}
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum QualifierError {
#[error("unrecognized resource qualifier {qualifier:?}")]
Unrecognized { qualifier: String },
#[error("resource qualifier {qualifier:?} is repeated, conflicts with another, or is out of order")]
Misplaced { qualifier: String },
#[error(transparent)]
Locale(#[from] LocaleError),
}
#[derive(Clone, Copy)]
enum Stage {
Locale,
Axis(fn(&mut RawConfig, &str) -> bool),
}
const STAGES: [Stage; 24] = [
Stage::Axis(|raw, part| set(&mut raw.mcc, number(part, "mcc", "").filter(|&mcc| part.len() == 6 && mcc != 0))),
Stage::Axis(|raw, part| {
let mnc = number(part, "mnc", "").filter(|_| part.len() <= 6);
set(&mut raw.mnc, mnc.map(|mnc| if mnc == 0 { MNC_ZERO } else { mnc }))
}),
Stage::Locale,
Stage::Axis(|raw, part| set(&mut raw.grammatical_inflection, value_of(&GENDERS, part))),
Stage::Axis(|raw, part| or(&mut raw.screen_layout, value_of(&LAYOUT_DIRECTIONS, part))),
Stage::Axis(|raw, part| set(&mut raw.smallest_screen_width_dp, number(part, "sw", "dp"))),
Stage::Axis(|raw, part| set(&mut raw.screen_width_dp, number(part, "w", "dp"))),
Stage::Axis(|raw, part| set(&mut raw.screen_height_dp, number(part, "h", "dp"))),
Stage::Axis(|raw, part| or(&mut raw.screen_layout, value_of(&SCREEN_SIZES, part))),
Stage::Axis(|raw, part| or(&mut raw.screen_layout, value_of(&SCREEN_LONG, part))),
Stage::Axis(|raw, part| or(&mut raw.screen_layout2, value_of(&ROUND, part))),
Stage::Axis(|raw, part| or(&mut raw.color_mode, value_of(&WIDE_GAMUT, part))),
Stage::Axis(|raw, part| or(&mut raw.color_mode, value_of(&HDR, part))),
Stage::Axis(|raw, part| set(&mut raw.orientation, value_of(&ORIENTATIONS, part))),
Stage::Axis(|raw, part| or(&mut raw.ui_mode, value_of(&UI_TYPES, part))),
Stage::Axis(|raw, part| or(&mut raw.ui_mode, value_of(&NIGHT, part))),
Stage::Axis(|raw, part| set(&mut raw.density, density(part))),
Stage::Axis(|raw, part| set(&mut raw.touchscreen, value_of(&TOUCHSCREENS, part))),
Stage::Axis(|raw, part| or(&mut raw.input_flags, value_of(&KEYS_HIDDEN, part))),
Stage::Axis(|raw, part| set(&mut raw.keyboard, value_of(&KEYBOARDS, part))),
Stage::Axis(|raw, part| or(&mut raw.input_flags, value_of(&NAV_HIDDEN, part))),
Stage::Axis(|raw, part| set(&mut raw.navigation, value_of(&NAVIGATIONS, part))),
Stage::Axis(|raw, part| {
let side = |digits| number(digits, "", "").filter(|&side| side != 0);
let size = part.split_once('x').and_then(|(w, h)| Some((side(w)?, side(h)?)));
size.filter(|(w, h)| w >= h).map(|(w, h)| (raw.screen_width, raw.screen_height) = (w, h)).is_some()
}),
Stage::Axis(|raw, part| set(&mut raw.sdk_version, number(part, "v", ""))),
];
impl Stage {
fn accepts(self, part: &str) -> bool {
match self {
Self::Locale => looks_like_language(part),
Self::Axis(apply) => apply(&mut RawConfig::default(), part),
}
}
}
fn set<T>(field: &mut T, value: Option<T>) -> bool {
value.map(|value| *field = value).is_some()
}
fn or(field: &mut u8, value: Option<u8>) -> bool {
value.map(|value| *field |= value).is_some()
}
fn number(part: &str, prefix: &str, suffix: &str) -> Option<u16> {
let digits = part.strip_prefix(prefix)?.strip_suffix(suffix)?;
digits.bytes().all(|b| b.is_ascii_digit()).then(|| digits.parse().ok()).flatten()
}
fn density(part: &str) -> Option<u16> {
match Density::NAMES.iter().find(|(_, name)| *name == part) {
Some((density, _)) => Some(density.0),
None => number(part, "", "dpi").filter(|&dpi| dpi != 0),
}
}
impl FromStr for Config {
type Err = QualifierError;
fn from_str(text: &str) -> Result<Self, QualifierError> {
let mut raw = RawConfig::default();
let mut parts = text.split('-').filter(|_| !text.is_empty()).peekable();
let mut next = 0;
while let Some(part) = parts.next() {
let lower = part.to_ascii_lowercase();
let mut taken = None;
for (position, &stage) in STAGES.iter().enumerate().skip(next) {
let matched = match stage {
Stage::Axis(apply) => apply(&mut raw, &lower),
Stage::Locale if looks_like_language(&lower) => {
let region = parts.next_if(|next| !lower.starts_with("b+") && is_region_qualifier(next));
Locale::from_qualifier(&lower, region)?.write_raw(&mut raw);
true
}
Stage::Locale => false,
};
if matched {
taken = Some(position);
break;
}
}
next = match taken {
Some(position) => position + 1,
None if STAGES[..next].iter().any(|stage| stage.accepts(&lower)) => {
return Err(QualifierError::Misplaced { qualifier: part.to_owned() });
}
None => return Err(QualifierError::Unrecognized { qualifier: part.to_owned() }),
};
}
Ok(Self(raw))
}
}
fn looks_like_language(part: &str) -> bool {
part.starts_with("b+")
|| ((2..=3).contains(&part.len()) && part.bytes().all(|b| b.is_ascii_alphabetic()) && part != "car")
}
fn is_region_qualifier(part: &str) -> bool {
part.len() == 3 && part.starts_with(['r', 'R']) && part[1..].bytes().all(|b| b.is_ascii_alphabetic())
}
fn is_variant(subtag: &str) -> bool {
let alphanumeric = subtag.bytes().all(|b| b.is_ascii_alphanumeric());
alphanumeric && ((5..=8).contains(&subtag.len()) || (subtag.len() == 4 && subtag.as_bytes()[0].is_ascii_digit()))
}
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum LocaleError {
#[error("locale is empty")]
Empty,
#[error("language {language:?} must be 2 or 3 ASCII letters")]
InvalidLanguage { language: String },
#[error("locale subtag {subtag:?} is not a script, region, or variant in its place")]
InvalidSubtag { subtag: String },
}
#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Locale {
language: Subtag<3>,
script: Option<Subtag<4>>,
region: Option<Subtag<3>>,
variant: Option<Subtag<8>>,
}
#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
struct Subtag<const N: usize> {
bytes: [u8; N],
len: u8,
}
impl<const N: usize> Subtag<N> {
fn new(text: &str) -> Option<Self> {
let mut bytes = [0; N];
bytes.get_mut(..text.len()).filter(|_| text.is_ascii())?.copy_from_slice(text.as_bytes());
Some(Self { bytes, len: text.len() as u8 })
}
fn as_str(&self) -> &str {
std::str::from_utf8(&self.bytes[..usize::from(self.len)]).expect("subtags are ASCII")
}
}
impl Locale {
pub fn parse(text: &str) -> Result<Self, LocaleError> {
let (tag, separators) = match text.strip_prefix("b+") {
Some(rest) => (rest, ['+', '+']),
None => (text, ['-', '_']),
};
let mut subtags = tag.split(separators);
let language = subtags.next().filter(|l| !l.is_empty()).ok_or(LocaleError::Empty)?;
let invalid_language = || LocaleError::InvalidLanguage { language: language.to_owned() };
if !(2..=3).contains(&language.len()) || !language.bytes().all(|b| b.is_ascii_alphabetic()) {
return Err(invalid_language());
}
let language = Subtag::new(&language.to_ascii_lowercase()).ok_or_else(invalid_language)?;
let mut locale = Self { language, script: None, region: None, variant: None };
for subtag in subtags {
let invalid = || LocaleError::InvalidSubtag { subtag: subtag.to_owned() };
let region = if is_region_qualifier(subtag) { &subtag[1..] } else { subtag };
let alphabetic = |s: &str| s.bytes().all(|b| b.is_ascii_alphabetic());
let (script_open, region_open) =
(locale.script.is_none() && locale.region.is_none(), locale.region.is_none());
if locale.variant.is_some() {
return Err(invalid());
} else if script_open && subtag.len() == 4 && alphabetic(subtag) {
let script = subtag[..1].to_ascii_uppercase() + &subtag[1..].to_ascii_lowercase();
locale.script = Some(Subtag::new(&script).ok_or_else(invalid)?);
} else if region_open && region.len() == 2 && alphabetic(region) {
locale.region = Some(Subtag::new(®ion.to_ascii_uppercase()).ok_or_else(invalid)?);
} else if region_open && region.len() == 3 && region.bytes().all(|b| b.is_ascii_digit()) {
locale.region = Some(Subtag::new(region).ok_or_else(invalid)?);
} else if is_variant(subtag) {
locale.variant = Some(Subtag::new(&subtag.to_ascii_lowercase()).ok_or_else(invalid)?);
} else {
return Err(invalid());
}
}
Ok(locale)
}
fn from_qualifier(language: &str, region: Option<&str>) -> Result<Self, LocaleError> {
let mut locale = Self::parse(language)?;
if let Some(region) = region {
let invalid = || LocaleError::InvalidSubtag { subtag: region.to_owned() };
locale.region = Some(Subtag::new(®ion[1..].to_ascii_uppercase()).ok_or_else(invalid)?);
}
Ok(locale)
}
pub fn language(&self) -> &str {
self.language.as_str()
}
pub fn script(&self) -> Option<&str> {
self.script.as_ref().map(Subtag::as_str)
}
pub fn region(&self) -> Option<&str> {
self.region.as_ref().map(Subtag::as_str)
}
pub fn variant(&self) -> Option<&str> {
self.variant.as_ref().map(Subtag::as_str)
}
pub fn qualifier(&self) -> impl fmt::Display + '_ {
Qualifier(self)
}
fn from_raw(raw: &RawConfig) -> Option<Self> {
let language = unpack(raw.language, b'a')?;
let region = unpack(raw.country, b'0');
let script = (raw.locale_script != [0; 4] && raw.locale_script_was_computed == 0)
.then(|| std::str::from_utf8(&raw.locale_script).ok().and_then(Subtag::new))
.flatten();
let variant = raw.locale_variant.split(|&b| b == 0).next().filter(|v| !v.is_empty());
let variant = variant.and_then(|v| std::str::from_utf8(v).ok()).and_then(Subtag::new);
Some(Self { language, script, region, variant })
}
fn write_raw(&self, raw: &mut RawConfig) {
raw.language = pack(self.language, b'a');
raw.country = self.region.map_or([0; 2], |region| pack(region, b'0'));
raw.locale_script = self.script.map_or([0; 4], |script| script.bytes);
raw.locale_script_was_computed = 0;
raw.locale_variant = self.variant.map_or([0; 8], |variant| variant.bytes);
}
}
struct Qualifier<'a>(&'a Locale);
impl fmt::Display for Qualifier<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let locale = self.0;
match (locale.script(), locale.region(), locale.variant()) {
(None, None, None) => f.write_str(locale.language()),
(None, Some(region), None) if region.len() == 2 => write!(f, "{}-r{region}", locale.language()),
(script, region, variant) => {
write!(f, "b+{}", locale.language())?;
[script, region, variant].into_iter().flatten().try_for_each(|part| write!(f, "+{part}"))
}
}
}
}
impl FromStr for Locale {
type Err = LocaleError;
fn from_str(text: &str) -> Result<Self, LocaleError> {
Self::parse(text)
}
}
impl fmt::Display for Locale {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.language())?;
[self.script(), self.region(), self.variant()].into_iter().flatten().try_for_each(|part| write!(f, "-{part}"))
}
}
impl fmt::Debug for Locale {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Locale({self})")
}
}
fn pack(subtag: Subtag<3>, base: u8) -> [u8; 2] {
match *subtag.as_str().as_bytes() {
[a, b] => [a, b],
[a, b, c] => {
let [first, second, third] = [a, b, c].map(|x| x.wrapping_sub(base) & 0x7f);
[0x80 | (third << 2) | (second >> 3), (second << 5) | first]
}
_ => [0; 2],
}
}
fn unpack(raw: [u8; 2], base: u8) -> Option<Subtag<3>> {
if raw[0] & 0x80 != 0 {
let first = raw[1] & 0x1f;
let second = ((raw[1] & 0xe0) >> 5) | ((raw[0] & 0x03) << 3);
let third = (raw[0] & 0x7c) >> 2;
let bytes = [first, second, third].map(|c| c.wrapping_add(base));
return bytes.is_ascii().then_some(Subtag { bytes, len: 3 });
}
(raw[0] != 0 && raw.is_ascii()).then_some(Subtag { bytes: [raw[0], raw[1], 0], len: 2 })
}
#[cfg(test)]
mod tests {
use super::*;
use crate::res::chunk::Put;
#[test]
fn locales_parse_in_every_notation() {
for (input, bcp47, qualifier) in [
("fr", "fr", "fr"),
("pt-BR", "pt-BR", "pt-rBR"),
("pt-rBR", "pt-BR", "pt-rBR"),
("zh-Hant-TW", "zh-Hant-TW", "b+zh+Hant+TW"),
("b+sr+Latn", "sr-Latn", "b+sr+Latn"),
("es-419", "es-419", "b+es+419"),
("fil-PH", "fil-PH", "fil-rPH"),
("b+ca+ES+valencia", "ca-ES-valencia", "b+ca+ES+valencia"),
("de-1996", "de-1996", "b+de+1996"),
] {
let locale = Locale::parse(input).unwrap();
assert_eq!(locale.to_string(), bcp47, "{input}");
assert_eq!(locale.qualifier().to_string(), qualifier, "{input}");
}
assert_eq!(Locale::parse(""), Err(LocaleError::Empty));
assert_eq!(Locale::parse("french"), Err(LocaleError::InvalidLanguage { language: "french".into() }));
for (tag, subtag) in [("en-USA", "USA"), ("en-Latn-Latn", "Latn"), ("en-US-GB", "GB"), ("ca-valencia-ES", "ES")]
{
assert_eq!(Locale::parse(tag), Err(LocaleError::InvalidSubtag { subtag: subtag.into() }), "{tag}");
}
}
#[test]
fn three_letter_codes_pack_and_unpack() {
for (text, base) in [("fil", b'a'), ("419", b'0'), ("ast", b'a')] {
let subtag = Subtag::new(text).unwrap();
assert_eq!(unpack(pack(subtag, base), base).unwrap().as_str(), text);
}
}
#[test]
fn qualifier_strings_round_trip_in_aapt2_order() {
for text in [
"",
"fr-rCA",
"b+zh+Hant+TW-night-xxhdpi",
"ldrtl-sw600dp-land-v17",
"mcc310-mnc4-en-rUS",
"mcc001-mnc0-en",
"mcc310-mnc0",
"b+ca+ES+valencia-v21",
"round-watch-anydpi-v26",
"large-long-notnight-tvdpi-finger-keyshidden-qwerty-navexposed-dpad-800x480-v4",
] {
let config: Config = text.parse().unwrap();
assert_eq!(config.to_string(), text);
}
}
#[test]
fn qualifiers_are_rejected_where_aapt2_rejects_them() {
use QualifierError::*;
for (text, error) in [
("xhdpi-bogus", Unrecognized { qualifier: "bogus".into() }),
("night-notnight", Misplaced { qualifier: "notnight".into() }),
("ldltr-ldrtl", Misplaced { qualifier: "ldrtl".into() }),
("v21-anydpi", Misplaced { qualifier: "anydpi".into() }),
("night-fr", Misplaced { qualifier: "fr".into() }),
("xhdpi-xhdpi", Misplaced { qualifier: "xhdpi".into() }),
("480x800", Unrecognized { qualifier: "480x800".into() }),
("sw+1dp", Unrecognized { qualifier: "sw+1dp".into() }),
("+800x480", Unrecognized { qualifier: "+800x480".into() }),
("800x0", Unrecognized { qualifier: "800x0".into() }),
("mcc1", Unrecognized { qualifier: "mcc1".into() }),
("mcc0", Unrecognized { qualifier: "mcc0".into() }),
("mcc000", Unrecognized { qualifier: "mcc000".into() }),
("mcc0310", Unrecognized { qualifier: "mcc0310".into() }),
("mnc0001", Unrecognized { qualifier: "mnc0001".into() }),
("mnc65535", Unrecognized { qualifier: "mnc65535".into() }),
("0dpi", Unrecognized { qualifier: "0dpi".into() }),
] {
assert_eq!(text.parse::<Config>(), Err(error), "{text}");
}
}
#[test]
fn qualifiers_are_case_insensitive_like_aapt2() {
for (text, canonical) in [("en-RUS", "en-rUS"), ("B+en", "en"), ("B+ZH+HANT", "b+zh+Hant"), ("XHDPI", "xhdpi")]
{
assert_eq!(text.parse::<Config>().unwrap().to_string(), canonical, "{text}");
}
}
#[test]
fn implied_versions_match_aapt2() {
for (text, implied) in [
("", ""),
("ldrtl", "ldrtl"),
("fr", "fr"),
("large", "large-v4"),
("xhdpi", "xhdpi-v4"),
("nodpi", "nodpi-v4"),
("night", "night-v8"),
("car", "car-v8"),
("sw600dp", "sw600dp-v13"),
("anydpi", "anydpi-v21"),
("anydpi-v26", "anydpi-v26"),
("round", "round-v23"),
("widecg", "widecg-v26"),
("vrheadset", "vrheadset-v26"),
("feminine", "feminine-v34"),
("night-xhdpi", "night-xhdpi-v8"),
] {
let config: Config = text.parse().unwrap();
assert_eq!(config.with_implied_sdk_version().to_string(), implied, "{text}");
}
}
#[test]
fn configurations_honor_their_size_field() {
let config: Config = "fr-xhdpi-v21".parse().unwrap();
let mut bytes = Vec::new();
bytes.put(&config.0);
assert_eq!(bytes.len(), Config::SIZE);
assert_eq!(Config::read(&bytes), config);
bytes[..4].copy_from_slice(&24u32.to_le_bytes());
assert_eq!(Config::read(&bytes).to_string(), "fr-xhdpi");
}
#[test]
fn change_flags_track_qualifiers() {
let config: Config = "fr-night-xhdpi-v21".parse().unwrap();
assert_eq!(config.change_flags(), 0x0004 | 0x1000 | 0x0100 | 0x0400);
assert_eq!(Config::default().change_flags(), 0);
}
}