1use 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#[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 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#[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 const MIN_SIZE: usize = 12 + 4;
115}
116
117#[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;
139const MAX_ENTRIES: usize = 1 << 16;
141const MAX_CONFIG_ENTRIES: usize = u16::MAX as usize;
143
144#[derive(Clone, Debug)]
156pub struct ResourceTable<'a> {
157 strings: StringPool<'a>,
158 packages: Vec<Package<'a>>,
159}
160
161#[derive(Clone, Debug)]
164pub struct Package<'a> {
165 id: u8,
166 name: String,
167 types: Vec<TypeSpec<'a>>,
168}
169
170#[derive(Clone, Debug)]
172pub struct TypeSpec<'a> {
173 id: u8,
174 name: Cow<'a, str>,
175 flags: &'a [[u8; 4]],
177 configs: Vec<TypeConfig<'a>>,
178}
179
180#[derive(Clone, Debug)]
182struct TypeConfig<'a> {
183 config: Config,
184 offsets: Offsets<'a>,
185 entries: &'a [u8],
187 keys: StringPool<'a>,
189}
190
191#[derive(Clone, Copy, Debug)]
193enum Offsets<'a> {
194 Dense(&'a [[u8; 4]]),
196 Offset16(&'a [[u8; 2]]),
198 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 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 fn iter(self) -> impl Iterator<Item = (u16, usize)> + 'a {
227 (0..self.slots()).filter_map(move |slot| self.slot(slot))
228 }
229
230 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
245fn 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#[derive(Clone, Copy, Debug)]
252enum RawEntry<'a> {
253 Simple {
254 key: u32,
255 value: RawValue,
256 },
257 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 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#[derive(Clone, Debug, PartialEq)]
296pub enum EntryValue<'a> {
297 Simple(Value),
298 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#[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 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 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
358fn 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 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 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 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 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 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 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 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 pub fn name(&self) -> &str {
473 &self.name
474 }
475
476 pub fn entry_count(&self) -> usize {
478 self.flags.len()
479 }
480
481 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 pub fn configs(&self) -> impl Iterator<Item = &Config> {
488 self.configs.iter().map(|c| &c.config)
489 }
490}
491
492#[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 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 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 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 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
549fn 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
561fn 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 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
657fn 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
674pub(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
686pub(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 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 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 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 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 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}