1use std::fmt;
9use std::str::FromStr;
10
11use binrw::binrw;
12
13use super::chunk::read_padded;
14
15#[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#[derive(Clone, Copy, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
59pub struct Config(pub(crate) RawConfig);
60
61#[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 pub const ANYDPI: Self = Self(0xfffe);
76 pub const NODPI: Self = Self(0xffff);
78
79 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#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
106pub enum NightMode {
107 #[default]
108 Any,
109 No,
110 Yes,
111}
112
113#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
115pub enum Orientation {
116 #[default]
117 Any,
118 Portrait,
119 Landscape,
120 Square,
121}
122
123#[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 pub fn for_locale(locale: Locale) -> Self {
147 Self::default().with_locale(locale)
148 }
149
150 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 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 pub(crate) const SIZE: usize = RawConfig::SIZE;
243
244 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 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 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
311const 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")];
328const 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
382struct 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 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#[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#[derive(Clone, Copy)]
425enum Stage {
426 Locale,
427 Axis(fn(&mut RawConfig, &str) -> bool),
429}
430
431const STAGES: [Stage; 24] = [
433 Stage::Axis(|raw, part| set(&mut raw.mcc, number(part, "mcc", "").filter(|&mcc| part.len() == 6 && mcc != 0))),
435 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 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 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
483fn or(field: &mut u8, value: Option<u8>) -> bool {
485 value.map(|value| *field |= value).is_some()
486}
487
488fn 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 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
540fn 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
546fn 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
551fn 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#[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#[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#[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 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 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(®ion.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 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(®ion[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 pub fn variant(&self) -> Option<&str> {
665 self.variant.as_ref().map(Subtag::as_str)
666 }
667
668 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
693struct 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 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
731fn 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 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}