Skip to main content

apk/res/
table.rs

1//! The resource table (`resources.arsc`).
2//!
3//! [`ResourceTable`] reads a table in place: parsing checks every chunk,
4//! offset, and entry once, and lookups then borrow from the input. Dense,
5//! sparse, 16-bit offset, and compact entries are understood, as are split
6//! packages and type ID offsets. Tables are written only by the resource
7//! compiler, in aapt2's dense layout.
8
9use std::borrow::Cow;
10use std::collections::BTreeSet;
11use std::fmt;
12
13use binrw::binrw;
14
15use super::chunk::{Chunk, ChunkError, ChunkType, Chunks, Header, Put, read_padded, write_chunk};
16use super::config::{Config, RawConfig};
17use super::id::{ResourceId, ResourceLookup, ResourceName, ResourceType};
18use super::string_pool::{Encoding, StringPool, StringPoolBuilder, StringPoolError};
19use super::value::{RawValue, Value};
20
21/// A malformed or unencodable resource table.
22#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
23#[non_exhaustive]
24pub enum TableError {
25    #[error(transparent)]
26    Chunk(#[from] ChunkError),
27    #[error(transparent)]
28    StringPool(#[from] StringPoolError),
29    #[error("package chunk precedes the table's value string pool")]
30    MissingValuePool,
31    #[error("resource table declares {expected} packages but contains {found} package chunks")]
32    PackageCountMismatch { expected: u32, found: u32 },
33    #[error("package ID {id} does not fit in a byte")]
34    PackageIdOutOfRange { id: u32 },
35    #[error("package string pool offset {offset:#x} is invalid")]
36    PoolOffsetOutOfRange { offset: u32 },
37    #[error("type ID {id} (offset by {offset}) is zero, too large, or declared twice")]
38    InvalidTypeId { id: u8, offset: u32 },
39    #[error("type {type_id} has no name in its package's type string pool")]
40    UnnamedType { type_id: u8 },
41    #[error("type {type_id} has entries but no preceding type spec")]
42    TypeWithoutSpec { type_id: u8 },
43    #[error("type {type_id} declares {count} entries, more than its chunk or a type can hold")]
44    EntryCountOutOfRange { type_id: u8, count: u32 },
45    #[error("sparse entries of type {type_id} are not in ascending order")]
46    UnsortedSparseEntries { type_id: u8 },
47    #[error("entry {index} of type {type_id} lies outside its type spec")]
48    EntryOutsideSpec { type_id: u8, index: u16 },
49    #[error("type {type_id} entries start at {offset:#x}, outside the chunk")]
50    EntriesOutOfRange { type_id: u8, offset: u32 },
51    #[error("entry {index} of type {type_id} is truncated or lies outside its chunk")]
52    TruncatedEntry { type_id: u8, index: u16 },
53    #[error("entry {index} of type {type_id} names key {key}, which is not in the key pool")]
54    KeyOutOfRange { type_id: u8, index: u16, key: u32 },
55    #[error("type `{ty}` has more than 65535 entries")]
56    TooManyEntries { ty: ResourceType },
57    #[error("package name {name:?} exceeds 127 UTF-16 code units")]
58    PackageNameTooLong { name: String },
59}
60
61#[binrw]
62#[brw(little)]
63struct TableHeader {
64    package_count: u32,
65}
66
67impl Header for TableHeader {
68    const MIN_SIZE: usize = 4;
69}
70
71#[binrw]
72#[brw(little)]
73struct PackageHeader {
74    id: u32,
75    name: [u16; 128],
76    type_strings: u32,
77    last_public_type: u32,
78    key_strings: u32,
79    last_public_key: u32,
80    /// Added in Android 7; older packages end before it.
81    type_id_offset: u32,
82}
83
84impl Header for PackageHeader {
85    const MIN_SIZE: usize = 4 + 256 + 16;
86}
87
88#[binrw]
89#[brw(little)]
90struct TypeSpecHeader {
91    id: u8,
92    res0: u8,
93    types_count: u16,
94    entry_count: u32,
95}
96
97impl Header for TypeSpecHeader {
98    const MIN_SIZE: usize = 8;
99}
100
101/// `ResTable_type` up to its configuration, which has its own size field.
102#[binrw]
103#[brw(little)]
104struct TypeHeader {
105    id: u8,
106    flags: u8,
107    reserved: u16,
108    entry_count: u32,
109    entries_start: u32,
110}
111
112impl Header for TypeHeader {
113    /// The fixed fields and the configuration's size field.
114    const MIN_SIZE: usize = 12 + 4;
115}
116
117/// A `ResTable_type` header as written, with its configuration.
118#[binrw::binwrite]
119#[bw(little)]
120struct TypeHeaderWithConfig {
121    fixed: TypeHeader,
122    config: RawConfig,
123}
124
125#[binrw]
126#[brw(little)]
127struct EntryHeader {
128    size: u16,
129    flags: u16,
130    key: u32,
131}
132
133const TYPE_FLAG_SPARSE: u8 = 0x01;
134const TYPE_FLAG_OFFSET16: u8 = 0x02;
135const ENTRY_FLAG_COMPLEX: u16 = 0x0001;
136const ENTRY_FLAG_COMPACT: u16 = 0x0008;
137const NO_ENTRY: u32 = u32::MAX;
138const SPEC_PUBLIC: u32 = 0x4000_0000;
139/// Entry indices are 16 bits wide.
140const MAX_ENTRIES: usize = 1 << 16;
141// Specs can reserve every 16-bit ID; Android limits each configuration's count to 65535.
142const MAX_CONFIG_ENTRIES: usize = u16::MAX as usize;
143
144/// A parsed `resources.arsc`, borrowing from its bytes.
145///
146/// ```no_run
147/// use apk::res::{ResourceTable, ResourceType};
148///
149/// let bytes = apk::Apk::open("app.apk")?.read("resources.arsc")?;
150/// let table = ResourceTable::parse(&bytes)?;
151/// let id = table.find(0x7f, ResourceType::String, "app_name").expect("app_name");
152/// println!("{}", table.get(id).unwrap().default_value().unwrap());
153/// # Ok::<(), apk::Error>(())
154/// ```
155#[derive(Clone, Debug)]
156pub struct ResourceTable<'a> {
157    strings: StringPool<'a>,
158    packages: Vec<Package<'a>>,
159}
160
161/// A package (`ResTable_package`), typically `0x7f` for an app. Package
162/// chunks with the same ID are merged.
163#[derive(Clone, Debug)]
164pub struct Package<'a> {
165    id: u8,
166    name: String,
167    types: Vec<TypeSpec<'a>>,
168}
169
170/// All configurations of one resource type within a package.
171#[derive(Clone, Debug)]
172pub struct TypeSpec<'a> {
173    id: u8,
174    name: Cow<'a, str>,
175    /// `ResTable_typeSpec` flags, one per entry.
176    flags: &'a [[u8; 4]],
177    configs: Vec<TypeConfig<'a>>,
178}
179
180/// The entries of one type for one configuration (`ResTable_type`).
181#[derive(Clone, Debug)]
182struct TypeConfig<'a> {
183    config: Config,
184    offsets: Offsets<'a>,
185    /// The chunk from `entries_start` on.
186    entries: &'a [u8],
187    /// The key pool of the package chunk this configuration came from.
188    keys: StringPool<'a>,
189}
190
191/// Where the entries of a [`TypeConfig`] start.
192#[derive(Clone, Copy, Debug)]
193enum Offsets<'a> {
194    /// A byte offset per entry, `NO_ENTRY` when absent.
195    Dense(&'a [[u8; 4]]),
196    /// An offset in 4-byte words per entry, `0xffff` when absent.
197    Offset16(&'a [[u8; 2]]),
198    /// `(index, offset in words)` for the defined entries, by index.
199    Sparse(&'a [[u8; 4]]),
200}
201
202impl<'a> Offsets<'a> {
203    fn slots(self) -> usize {
204        match self {
205            Self::Dense(offsets) | Self::Sparse(offsets) => offsets.len(),
206            Self::Offset16(offsets) => offsets.len(),
207        }
208    }
209
210    /// The entry index and byte offset in `slot`, unless the slot is empty.
211    fn slot(self, slot: usize) -> Option<(u16, usize)> {
212        match self {
213            Self::Dense(offsets) => {
214                let offset = u32::from_le_bytes(offsets[slot]);
215                (offset != NO_ENTRY).then_some((slot as u16, offset as usize))
216            }
217            Self::Offset16(offsets) => {
218                let words = u16::from_le_bytes(offsets[slot]);
219                (words != u16::MAX).then_some((slot as u16, usize::from(words) * 4))
220            }
221            Self::Sparse(pairs) => Some(sparse(pairs[slot])),
222        }
223    }
224
225    /// Every defined entry: its index and byte offset.
226    fn iter(self) -> impl Iterator<Item = (u16, usize)> + 'a {
227        (0..self.slots()).filter_map(move |slot| self.slot(slot))
228    }
229
230    /// The byte offset of entry `index`, if it is defined.
231    fn get(self, index: u16) -> Option<usize> {
232        match self {
233            Self::Sparse(pairs) => {
234                let slot = pairs.binary_search_by_key(&index, |&pair| sparse(pair).0).ok()?;
235                Some(sparse(pairs[slot]).1)
236            }
237            dense => {
238                let slot = usize::from(index);
239                (slot < dense.slots()).then(|| dense.slot(slot)).flatten().map(|(_, offset)| offset)
240            }
241        }
242    }
243}
244
245/// A `ResTable_sparseTypeEntry`: an entry index and an offset in words.
246fn sparse(pair: [u8; 4]) -> (u16, usize) {
247    (u16::from_le_bytes([pair[0], pair[1]]), usize::from(u16::from_le_bytes([pair[2], pair[3]])) * 4)
248}
249
250/// A `ResTable_entry` and its value, read in place.
251#[derive(Clone, Copy, Debug)]
252enum RawEntry<'a> {
253    Simple {
254        key: u32,
255        value: RawValue,
256    },
257    /// A style, attribute, array, or plural: `(name, value)` items.
258    Bag {
259        key: u32,
260        parent: u32,
261        items: &'a [[u8; 12]],
262    },
263}
264
265impl<'a> RawEntry<'a> {
266    fn read(bytes: &'a [u8]) -> Option<Self> {
267        let header: EntryHeader = read_padded(bytes.get(..8)?);
268        if header.flags & ENTRY_FLAG_COMPACT != 0 {
269            // The key is in `size`, and the data type in the high byte of `flags`.
270            let value = RawValue::new((header.flags >> 8) as u8, header.key);
271            return Some(Self::Simple { key: header.size.into(), value });
272        }
273        if header.size < 8 {
274            return None;
275        }
276        let body = bytes.get(usize::from(header.size)..)?;
277        if header.flags & ENTRY_FLAG_COMPLEX == 0 {
278            return Some(Self::Simple { key: header.key, value: RawValue::read(body)? });
279        }
280        let bag = bytes.get(8..16).filter(|_| header.size >= 16)?;
281        let parent = u32::from_le_bytes(bag[..4].try_into().ok()?);
282        let count = u32::from_le_bytes(bag[4..].try_into().ok()?);
283        let items = body.as_chunks::<12>().0.get(..count as usize)?;
284        Some(Self::Bag { key: header.key, parent, items })
285    }
286
287    fn key(self) -> u32 {
288        match self {
289            Self::Simple { key, .. } | Self::Bag { key, .. } => key,
290        }
291    }
292}
293
294/// A resource's value in one configuration.
295#[derive(Clone, Debug, PartialEq)]
296pub enum EntryValue<'a> {
297    Simple(Value),
298    /// A style, attribute, array, or plural.
299    Bag(Bag<'a>),
300}
301
302impl fmt::Display for EntryValue<'_> {
303    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
304        match self {
305            Self::Simple(value) => write!(f, "{value}"),
306            Self::Bag(bag) => write!(f, "{bag}"),
307        }
308    }
309}
310
311/// A bag of keyed values, read in place: items are decoded only as they
312/// are iterated.
313#[derive(Clone, Copy, Debug, PartialEq)]
314pub struct Bag<'a> {
315    parent: u32,
316    items: &'a [[u8; 12]],
317    strings: StringPool<'a>,
318}
319
320impl<'a> Bag<'a> {
321    /// The bag this one inherits from, such as a style's parent.
322    pub fn parent(&self) -> Option<ResourceId> {
323        (self.parent != 0).then_some(ResourceId(self.parent))
324    }
325
326    pub fn len(&self) -> usize {
327        self.items.len()
328    }
329
330    pub fn is_empty(&self) -> bool {
331        self.items.is_empty()
332    }
333
334    /// Each item's key (an attribute or index) and value.
335    pub fn items(&self) -> impl ExactSizeIterator<Item = (ResourceId, Value)> + 'a {
336        let strings = self.strings;
337        self.items.iter().map(move |item| {
338            let (key, value) = item.split_at(4);
339            let key = ResourceId(u32::from_le_bytes(key.try_into().expect("four bytes")));
340            (key, decode(strings, RawValue::read(value).expect("items are value-sized")))
341        })
342    }
343}
344
345impl fmt::Display for Bag<'_> {
346    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
347        f.write_str("{")?;
348        if let Some(parent) = self.parent() {
349            write!(f, "parent={parent} ")?;
350        }
351        for (i, (key, value)) in self.items().enumerate() {
352            write!(f, "{}{key}={value}", if i == 0 { "" } else { ", " })?;
353        }
354        f.write_str("}")
355    }
356}
357
358/// Decodes a value whose strings index `strings`. A string missing from the
359/// pool stays raw.
360fn decode(strings: StringPool<'_>, raw: RawValue) -> Value {
361    let string = |index| strings.get(index).map(Cow::into_owned).ok_or(());
362    Value::from_raw(raw, string).unwrap_or(Value::Raw { data_type: raw.data_type, data: raw.data })
363}
364
365impl<'a> ResourceTable<'a> {
366    /// Parses and checks a `resources.arsc` file.
367    pub fn parse(bytes: &'a [u8]) -> Result<Self, TableError> {
368        let root = Chunk::parse_root(bytes, ChunkType::TABLE)?;
369        let header = root.header::<TableHeader>()?;
370        let mut package_count = 0;
371        let mut strings = None;
372        let mut packages: Vec<Package<'a>> = Vec::new();
373        for child in root.children() {
374            let child = child?;
375            match child.ty {
376                ChunkType::STRING_POOL if strings.is_none() => strings = Some(StringPool::parse(&child)?),
377                ChunkType::TABLE_PACKAGE if strings.is_none() => return Err(TableError::MissingValuePool),
378                ChunkType::TABLE_PACKAGE => {
379                    parse_package(&child, &mut packages)?;
380                    package_count += 1;
381                }
382                _ => {}
383            }
384        }
385        if package_count != header.package_count {
386            return Err(TableError::PackageCountMismatch { expected: header.package_count, found: package_count });
387        }
388        Ok(Self { strings: strings.unwrap_or_default(), packages })
389    }
390
391    pub fn packages(&self) -> &[Package<'a>] {
392        &self.packages
393    }
394
395    /// The resource with ID `id`, if any configuration defines it.
396    pub fn get(&self, id: ResourceId) -> Option<Resource<'_, 'a>> {
397        let package = self.packages.iter().find(|p| p.id == id.package())?;
398        let spec = package.types.iter().find(|t| t.id == id.type_id())?;
399        let resource = Resource { table: self, package, spec, index: id.entry() };
400        resource.entries().next().is_some().then_some(resource)
401    }
402
403    /// Looks up a resource by type and name in package `package_id`.
404    pub fn find(&self, package_id: u8, ty: ResourceType, name: &str) -> Option<ResourceId> {
405        let package = self.packages.iter().find(|p| p.id == package_id)?;
406        let spec = package.types.iter().find(|t| t.name == ty.as_str())?;
407        let index = spec.configs.iter().find_map(|config| {
408            config.offsets.iter().find_map(|(index, offset)| {
409                let entry = RawEntry::read(config.entries.get(offset..)?)?;
410                config.keys.equals(entry.key(), name).then_some(index)
411            })
412        })?;
413        Some(ResourceId::new(package.id, spec.id, index))
414    }
415
416    /// Every defined resource, by package, type, and entry.
417    pub fn resources(&self) -> impl Iterator<Item = Resource<'_, 'a>> {
418        self.packages.iter().flat_map(move |package| {
419            package.types.iter().flat_map(move |spec| {
420                // Indices fit in 16 bits: specs hold at most `MAX_ENTRIES`.
421                let indices = (0..spec.entry_count()).map(|index| index as u16);
422                let defined = indices.filter(|&index| spec.configs.iter().any(|c| c.offsets.get(index).is_some()));
423                defined.map(move |index| Resource { table: self, package, spec, index })
424            })
425        })
426    }
427}
428
429impl ResourceLookup for ResourceTable<'_> {
430    fn lookup(&self, ty: ResourceType, name: &ResourceName) -> Option<ResourceId> {
431        self.find(ResourceId::APP_PACKAGE, ty, name.as_ref())
432    }
433}
434
435impl fmt::Display for ResourceTable<'_> {
436    /// Lists every resource, in the spirit of `aapt2 dump resources`.
437    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
438        for resource in self.resources() {
439            writeln!(f, "{} {resource}", resource.id())?;
440            for (config, value) in resource.values() {
441                match config.is_default() {
442                    true => writeln!(f, "  (default): {value}")?,
443                    false => writeln!(f, "  {config}: {value}")?,
444                }
445            }
446        }
447        Ok(())
448    }
449}
450
451impl<'a> Package<'a> {
452    pub fn id(&self) -> u8 {
453        self.id
454    }
455
456    pub fn name(&self) -> &str {
457        &self.name
458    }
459
460    /// The package's types, by ID.
461    pub fn types(&self) -> &[TypeSpec<'a>] {
462        &self.types
463    }
464}
465
466impl TypeSpec<'_> {
467    pub fn id(&self) -> u8 {
468        self.id
469    }
470
471    /// The type name, e.g. `string`.
472    pub fn name(&self) -> &str {
473        &self.name
474    }
475
476    /// The number of entry IDs in this type.
477    pub fn entry_count(&self) -> usize {
478        self.flags.len()
479    }
480
481    /// Whether entry `index` is public (declared in `public.xml`).
482    pub fn is_public(&self, index: u16) -> bool {
483        self.flags.get(usize::from(index)).is_some_and(|&flags| u32::from_le_bytes(flags) & SPEC_PUBLIC != 0)
484    }
485
486    /// The configurations this type has values for.
487    pub fn configs(&self) -> impl Iterator<Item = &Config> {
488        self.configs.iter().map(|c| &c.config)
489    }
490}
491
492/// One resource of a [`ResourceTable`], across all its configurations.
493#[derive(Clone, Copy, Debug)]
494pub struct Resource<'t, 'a> {
495    table: &'t ResourceTable<'a>,
496    package: &'t Package<'a>,
497    spec: &'t TypeSpec<'a>,
498    index: u16,
499}
500
501impl<'t, 'a> Resource<'t, 'a> {
502    pub fn id(&self) -> ResourceId {
503        ResourceId::new(self.package.id, self.spec.id, self.index)
504    }
505
506    /// The entry name, from the first configuration that defines it.
507    pub fn name(&self) -> Cow<'a, str> {
508        let name = self.entries().next().and_then(|(config, entry)| config.keys.get(entry.key()));
509        name.unwrap_or_default()
510    }
511
512    pub fn type_name(&self) -> &'t str {
513        &self.spec.name
514    }
515
516    pub fn is_public(&self) -> bool {
517        self.spec.is_public(self.index)
518    }
519
520    fn entries(&self) -> impl Iterator<Item = (&'t TypeConfig<'a>, RawEntry<'a>)> + 't {
521        let index = self.index;
522        self.spec.configs.iter().filter_map(move |c| Some((c, entry_at(c, index)?)))
523    }
524
525    /// The defined values, one per configuration.
526    pub fn values(&self) -> impl Iterator<Item = (&'t Config, EntryValue<'a>)> + 't {
527        let strings = self.table.strings;
528        self.entries().map(move |(config, entry)| (&config.config, entry_value(strings, entry)))
529    }
530
531    /// The value for exactly `config`, if defined.
532    pub fn value(&self, config: &Config) -> Option<EntryValue<'a>> {
533        let (_, entry) = self.entries().find(|(c, _)| c.config == *config)?;
534        Some(entry_value(self.table.strings, entry))
535    }
536
537    /// The value in the default configuration.
538    pub fn default_value(&self) -> Option<EntryValue<'a>> {
539        self.value(&Config::default())
540    }
541}
542
543impl fmt::Display for Resource<'_, '_> {
544    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
545        write!(f, "{}:{}/{}", self.package.name, self.type_name(), self.name())
546    }
547}
548
549/// Entry `index` of `config`; entries were checked when the table was parsed.
550fn entry_at<'a>(config: &TypeConfig<'a>, index: u16) -> Option<RawEntry<'a>> {
551    RawEntry::read(config.entries.get(config.offsets.get(index)?..)?)
552}
553
554fn entry_value<'a>(strings: StringPool<'a>, entry: RawEntry<'a>) -> EntryValue<'a> {
555    match entry {
556        RawEntry::Simple { value, .. } => EntryValue::Simple(decode(strings, value)),
557        RawEntry::Bag { parent, items, .. } => EntryValue::Bag(Bag { parent, items, strings }),
558    }
559}
560
561/// Parses one package chunk, merging it into `packages` by ID.
562fn parse_package<'a>(chunk: &Chunk<'a>, packages: &mut Vec<Package<'a>>) -> Result<(), TableError> {
563    let header: PackageHeader = chunk.header()?;
564    let id = u8::try_from(header.id).map_err(|_| TableError::PackageIdOutOfRange { id: header.id })?;
565    let header_end = chunk.bytes.len() - chunk.body.len();
566    let pool_at = |offset: u32| -> Result<StringPool<'a>, TableError> {
567        let bytes = chunk.bytes.get(offset as usize..).filter(|_| offset as usize >= header_end);
568        let pool = Chunks::new(bytes.ok_or(TableError::PoolOffsetOutOfRange { offset })?).next();
569        Ok(StringPool::parse(&pool.ok_or(ChunkError::Missing)??)?)
570    };
571    let (type_names, keys) = (pool_at(header.type_strings)?, pool_at(header.key_strings)?);
572    let package = match packages.iter().position(|p| p.id == id) {
573        Some(index) => &mut packages[index],
574        None => {
575            let name_len = header.name.iter().position(|&c| c == 0).unwrap_or(header.name.len());
576            let name = String::from_utf16_lossy(&header.name[..name_len]);
577            packages.push(Package { id, name, types: Vec::new() });
578            packages.last_mut().expect("just pushed")
579        }
580    };
581    for child in chunk.children() {
582        let child = child?;
583        match child.ty {
584            ChunkType::TABLE_TYPE_SPEC => {
585                let spec: TypeSpecHeader = child.header()?;
586                // Type IDs in a chunk are relative to the package's offset;
587                // the type name pool is indexed by the relative ID.
588                let invalid = TableError::InvalidTypeId { id: spec.id, offset: header.type_id_offset };
589                let type_id = u32::from(spec.id)
590                    .checked_add(header.type_id_offset)
591                    .and_then(|id| u8::try_from(id).ok())
592                    .filter(|_| spec.id != 0)
593                    .ok_or(invalid.clone())?;
594                if package.types.iter().any(|t| t.id == type_id) {
595                    return Err(invalid);
596                }
597                let name = type_names.get(u32::from(spec.id) - 1).ok_or(TableError::UnnamedType { type_id })?;
598                let count = spec.entry_count as usize;
599                let overflow = TableError::EntryCountOutOfRange { type_id, count: spec.entry_count };
600                let flags = child.body.as_chunks::<4>().0.get(..count).filter(|_| count <= MAX_ENTRIES);
601                package.types.push(TypeSpec { id: type_id, name, flags: flags.ok_or(overflow)?, configs: Vec::new() });
602            }
603            ChunkType::TABLE_TYPE => {
604                let type_header: TypeHeader = child.header()?;
605                let missing = TableError::TypeWithoutSpec { type_id: type_header.id };
606                let type_id = u32::from(type_header.id)
607                    .checked_add(header.type_id_offset)
608                    .and_then(|id| u8::try_from(id).ok())
609                    .filter(|_| type_header.id != 0)
610                    .ok_or(missing.clone())?;
611                let spec = package.types.iter_mut().find(|t| t.id == type_id).ok_or(missing)?;
612                let config = parse_type(&child, &type_header, type_id, keys)?;
613                check_entries(&config, spec, type_id)?;
614                spec.configs.push(config);
615            }
616            _ => {}
617        }
618    }
619    package.types.sort_by_key(|t| t.id);
620    Ok(())
621}
622
623fn parse_type<'a>(
624    chunk: &Chunk<'a>,
625    header: &TypeHeader,
626    type_id: u8,
627    keys: StringPool<'a>,
628) -> Result<TypeConfig<'a>, TableError> {
629    let config = Config::read(&chunk.header[12..]);
630    let count = header.entry_count as usize;
631    let overflow = TableError::EntryCountOutOfRange { type_id, count: header.entry_count };
632    if count > MAX_CONFIG_ENTRIES {
633        return Err(overflow);
634    }
635    let offsets = if header.flags & TYPE_FLAG_SPARSE != 0 {
636        let pairs = chunk.body.as_chunks::<4>().0.get(..count).ok_or(overflow)?;
637        if !pairs.windows(2).all(|w| sparse(w[0]).0 < sparse(w[1]).0) {
638            return Err(TableError::UnsortedSparseEntries { type_id });
639        }
640        Offsets::Sparse(pairs)
641    } else if header.flags & TYPE_FLAG_OFFSET16 != 0 {
642        Offsets::Offset16(chunk.body.as_chunks::<2>().0.get(..count).ok_or(overflow)?)
643    } else {
644        Offsets::Dense(chunk.body.as_chunks::<4>().0.get(..count).ok_or(overflow)?)
645    };
646    let offset_width =
647        if header.flags & TYPE_FLAG_OFFSET16 != 0 && header.flags & TYPE_FLAG_SPARSE == 0 { 2 } else { 4 };
648    let metadata_end = chunk.bytes.len() - chunk.body.len() + count * offset_width;
649    let entries = chunk
650        .bytes
651        .get(header.entries_start as usize..)
652        .filter(|_| header.entries_start as usize >= metadata_end)
653        .ok_or(TableError::EntriesOutOfRange { type_id, offset: header.entries_start })?;
654    Ok(TypeConfig { config, offsets, entries, keys })
655}
656
657/// Checks that every entry of `config` is inside its spec and chunk and
658/// names a key that exists, in constant time per entry. (String values are
659/// checked when they are read.)
660fn check_entries(config: &TypeConfig<'_>, spec: &TypeSpec<'_>, type_id: u8) -> Result<(), TableError> {
661    for (index, offset) in config.offsets.iter() {
662        if usize::from(index) >= spec.entry_count() {
663            return Err(TableError::EntryOutsideSpec { type_id, index });
664        }
665        let entry = config.entries.get(offset..).and_then(RawEntry::read);
666        let key = entry.ok_or(TableError::TruncatedEntry { type_id, index })?.key();
667        if !config.keys.contains(key) {
668            return Err(TableError::KeyOutOfRange { type_id, index, key });
669        }
670    }
671    Ok(())
672}
673
674/// One type of a table being written: its entries' names and, per
675/// configuration, their encoded values.
676pub(crate) struct TypeData<'a> {
677    pub ty: ResourceType,
678    pub entries: Vec<EntryData<'a>>,
679}
680
681pub(crate) struct EntryData<'a> {
682    pub name: &'a str,
683    pub values: Vec<(Config, RawValue)>,
684}
685
686/// Writes a one-package table in aapt2's dense layout. Type IDs are
687/// assigned in order from 1; string values index `strings`.
688pub(crate) fn write(
689    package_id: u8,
690    package_name: &str,
691    strings: &StringPoolBuilder<'_>,
692    types: &[TypeData<'_>],
693) -> Result<Vec<u8>, TableError> {
694    let mut type_names = StringPoolBuilder::default();
695    let mut keys = StringPoolBuilder::default();
696    let mut body = Vec::new();
697    for (index, data) in types.iter().enumerate() {
698        type_names.push(data.ty.as_str());
699        write_type(index as u8 + 1, data, &mut keys, &mut body)?;
700    }
701    let mut pools = Vec::new();
702    type_names.write(&mut pools, Encoding::Utf16)?;
703    let key_offset = pools.len();
704    keys.write(&mut pools, Encoding::Utf8)?;
705
706    let mut name = [0u16; 128];
707    let units: Vec<u16> = package_name.encode_utf16().collect();
708    if units.len() >= name.len() {
709        return Err(TableError::PackageNameTooLong { name: package_name.to_owned() });
710    }
711    name[..units.len()].copy_from_slice(&units);
712    const HEADER_SIZE: u32 = 288;
713    let package = PackageHeader {
714        id: package_id.into(),
715        name,
716        type_strings: HEADER_SIZE,
717        last_public_type: types.len() as u32,
718        key_strings: HEADER_SIZE + key_offset as u32,
719        last_public_key: keys.len() as u32,
720        type_id_offset: 0,
721    };
722    let mut out = Vec::new();
723    write_chunk(&mut out, ChunkType::TABLE, &TableHeader { package_count: 1 }, |out| {
724        strings.write(out, Encoding::Utf8)?;
725        write_chunk(out, ChunkType::TABLE_PACKAGE, &package, |out| {
726            out.extend_from_slice(&pools);
727            out.extend_from_slice(&body);
728            Ok::<_, TableError>(())
729        })
730    })?;
731    Ok(out)
732}
733
734fn write_type<'a>(
735    id: u8,
736    data: &TypeData<'a>,
737    keys: &mut StringPoolBuilder<'a>,
738    out: &mut Vec<u8>,
739) -> Result<(), TableError> {
740    let count = data.entries.len();
741    if count > MAX_CONFIG_ENTRIES {
742        return Err(TableError::TooManyEntries { ty: data.ty });
743    }
744    let configs: BTreeSet<&Config> = data.entries.iter().flat_map(|e| e.values.iter().map(|(c, _)| c)).collect();
745    let spec = TypeSpecHeader { id, res0: 0, types_count: configs.len() as u16, entry_count: count as u32 };
746    write_chunk(out, ChunkType::TABLE_TYPE_SPEC, &spec, |out| {
747        // Each entry's flags: the configuration axes its values vary along.
748        for entry in &data.entries {
749            out.put_u32(entry.values.iter().fold(0, |flags, (config, _)| flags | config.change_flags()));
750        }
751        Ok::<_, TableError>(())
752    })?;
753    let keys: Vec<u32> = data.entries.iter().map(|e| keys.intern(e.name)).collect();
754    for config in configs {
755        let mut offsets = Vec::with_capacity(count);
756        let mut entries = Vec::new();
757        for (entry, &key) in data.entries.iter().zip(&keys) {
758            let Some((_, value)) = entry.values.iter().find(|(c, _)| c == config) else {
759                offsets.put_u32(NO_ENTRY);
760                continue;
761            };
762            offsets.put_u32(u32::try_from(entries.len()).map_err(|_| ChunkError::TooLarge)?);
763            entries.put(&EntryHeader { size: 8, flags: 0, key });
764            entries.put(value);
765        }
766        let entries_start = (8 + 12 + Config::SIZE + offsets.len()) as u32;
767        let fixed = TypeHeader { id, flags: 0, reserved: 0, entry_count: count as u32, entries_start };
768        write_chunk(out, ChunkType::TABLE_TYPE, &TypeHeaderWithConfig { fixed, config: config.0 }, |out| {
769            out.extend_from_slice(&offsets);
770            out.extend_from_slice(&entries);
771            Ok::<_, TableError>(())
772        })?;
773    }
774    Ok(())
775}
776
777#[cfg(test)]
778mod tests {
779    use super::*;
780    use crate::res::value::data_type;
781
782    /// Two strings (one localized) and a boolean resource.
783    fn sample() -> Vec<u8> {
784        let fr: Config = "fr".parse().unwrap();
785        let mut strings = StringPoolBuilder::default();
786        let mut string = |s| RawValue::new(data_type::STRING, strings.intern(s));
787        let types = vec![
788            TypeData {
789                ty: ResourceType::String,
790                entries: vec![
791                    EntryData {
792                        name: "app_name",
793                        values: vec![(Config::default(), string("Hello")), (fr, string("Bonjour"))],
794                    },
795                    EntryData { name: "other", values: vec![(Config::default(), string("x"))] },
796                ],
797            },
798            TypeData {
799                ty: ResourceType::Bool,
800                entries: vec![EntryData {
801                    name: "enabled",
802                    values: vec![(Config::default(), RawValue::new(data_type::INT_BOOLEAN, u32::MAX))],
803                }],
804            },
805        ];
806        write(0x7f, "org.example", &strings, &types).unwrap()
807    }
808
809    #[test]
810    fn writes_and_reads_back() {
811        let bytes = sample();
812        let table = ResourceTable::parse(&bytes).unwrap();
813        let id = table.find(0x7f, ResourceType::String, "app_name").unwrap();
814        assert_eq!(id, ResourceId(0x7f01_0000));
815        let resource = table.get(id).unwrap();
816        assert_eq!(resource.to_string(), "org.example:string/app_name");
817        assert!(matches!(resource.name(), Cow::Borrowed("app_name")));
818        assert_eq!(resource.value(&"fr".parse().unwrap()), Some(EntryValue::Simple("Bonjour".into())));
819        assert_eq!(resource.default_value(), Some(EntryValue::Simple("Hello".into())));
820        let enabled = table.find(0x7f, ResourceType::Bool, "enabled").unwrap();
821        assert_eq!(table.get(enabled).unwrap().default_value(), Some(EntryValue::Simple(Value::Bool(true))));
822        assert_eq!(table.resources().count(), 3);
823        assert!(table.get(ResourceId(0x7f01_0002)).is_none());
824    }
825
826    #[test]
827    fn dense_writer_respects_androids_16_bit_entry_count() {
828        let data = TypeData {
829            ty: ResourceType::String,
830            entries: (0..65_536).map(|_| EntryData { name: "reserved", values: Vec::new() }).collect(),
831        };
832        assert_eq!(
833            write(0x7f, "org.example", &StringPoolBuilder::default(), &[data]),
834            Err(TableError::TooManyEntries { ty: ResourceType::String })
835        );
836    }
837
838    #[test]
839    fn entries_outside_their_type_spec_are_rejected() {
840        let mut bytes = sample();
841        // Shrink the first type spec (`string`, two entries) to one entry.
842        let spec = bytes.windows(4).position(|w| w == [0x02, 0x02, 0x10, 0x00]).unwrap();
843        bytes[spec + 12..spec + 16].copy_from_slice(&1u32.to_le_bytes());
844        assert!(matches!(ResourceTable::parse(&bytes), Err(TableError::EntryOutsideSpec { type_id: 1, index: 1 })));
845    }
846
847    #[test]
848    fn entries_cannot_overlap_type_metadata() {
849        let original = sample();
850        let root = Chunk::parse_root(&original, ChunkType::TABLE).unwrap();
851        let package = root.children().map(Result::unwrap).find(|c| c.ty == ChunkType::TABLE_PACKAGE).unwrap();
852        let ty = package.children().map(Result::unwrap).find(|c| c.ty == ChunkType::TABLE_TYPE).unwrap();
853        let at = ty.bytes.as_ptr() as usize - original.as_ptr() as usize;
854        for offset in [0u32, 8, 84, 88] {
855            let mut bytes = original.clone();
856            bytes[at + 16..at + 20].copy_from_slice(&offset.to_le_bytes());
857            assert!(matches!(ResourceTable::parse(&bytes), Err(TableError::EntriesOutOfRange { .. })), "{offset}");
858        }
859    }
860
861    #[test]
862    fn simple_entry_headers_must_cover_their_fixed_fields() {
863        for size in 0u16..8 {
864            let mut bytes = vec![0; 16];
865            bytes[..2].copy_from_slice(&size.to_le_bytes());
866            assert!(RawEntry::read(&bytes).is_none(), "{size}");
867        }
868    }
869
870    #[test]
871    fn type_id_offsets_shift_ids_but_not_names() {
872        let mut bytes = sample();
873        let package = bytes.windows(4).position(|w| w == [0x00, 0x02, 0x20, 0x01]).unwrap();
874        // typeIdOffset is the package header's last field.
875        bytes[package + 284..package + 288].copy_from_slice(&2u32.to_le_bytes());
876        let table = ResourceTable::parse(&bytes).unwrap();
877        let types: Vec<(u8, &str)> = table.packages()[0].types().iter().map(|t| (t.id(), t.name())).collect();
878        assert_eq!(types, [(3, "string"), (4, "bool")]);
879        assert_eq!(table.find(0x7f, ResourceType::String, "app_name"), Some(ResourceId(0x7f03_0000)));
880    }
881
882    #[test]
883    fn mandatory_headers_must_be_present() {
884        let empty_table = [0x02, 0x00, 0x08, 0x00, 0x08, 0x00, 0x00, 0x00];
885        assert!(matches!(
886            ResourceTable::parse(&empty_table),
887            Err(TableError::Chunk(ChunkError::HeaderTooShort { size: 8, min: 12, .. }))
888        ));
889    }
890
891    #[test]
892    fn truncated_tables_fail_cleanly() {
893        let bytes = sample();
894        for len in 0..bytes.len() {
895            let mut truncated = bytes[..len].to_vec();
896            if truncated.len() >= 8 {
897                // Keep the outer size consistent so inner chunks are exercised.
898                let size = truncated.len() as u32;
899                truncated[4..8].copy_from_slice(&size.to_le_bytes());
900            }
901            assert!(ResourceTable::parse(&truncated).is_err(), "truncation at {len} parsed");
902        }
903    }
904
905    #[test]
906    fn declared_packages_must_exist() {
907        let mut bytes = sample();
908        bytes[8..12].copy_from_slice(&2u32.to_le_bytes());
909        assert!(matches!(
910            ResourceTable::parse(&bytes),
911            Err(TableError::PackageCountMismatch { expected: 2, found: 1 })
912        ));
913    }
914}