Skip to main content

apk/res/
chunk.rs

1//! `ResChunk_header` framing shared by resource tables and binary XML.
2//!
3//! Reading is zero-copy: [`Chunks`] walks a byte slice and yields bounds-checked
4//! [`Chunk`] views. Writing appends to a `Vec<u8>` and patches each chunk's
5//! sizes once its body is known.
6
7use std::fmt;
8use std::io::Cursor;
9
10use binrw::meta::{ReadEndian, WriteEndian};
11use binrw::{BinRead, BinWrite, binrw};
12
13/// A chunk type code (`ResChunk_header::type`).
14#[derive(Clone, Copy, Eq, Hash, PartialEq)]
15pub struct ChunkType(pub u16);
16
17impl ChunkType {
18    pub const NULL: Self = Self(0x0000);
19    pub const STRING_POOL: Self = Self(0x0001);
20    pub const TABLE: Self = Self(0x0002);
21    pub const XML: Self = Self(0x0003);
22    pub const XML_START_NAMESPACE: Self = Self(0x0100);
23    pub const XML_END_NAMESPACE: Self = Self(0x0101);
24    pub const XML_START_ELEMENT: Self = Self(0x0102);
25    pub const XML_END_ELEMENT: Self = Self(0x0103);
26    pub const XML_CDATA: Self = Self(0x0104);
27    pub const XML_RESOURCE_MAP: Self = Self(0x0180);
28    pub const TABLE_PACKAGE: Self = Self(0x0200);
29    pub const TABLE_TYPE: Self = Self(0x0201);
30    pub const TABLE_TYPE_SPEC: Self = Self(0x0202);
31    pub const TABLE_LIBRARY: Self = Self(0x0203);
32
33    fn name(self) -> Option<&'static str> {
34        Some(match self {
35            Self::NULL => "null",
36            Self::STRING_POOL => "string pool",
37            Self::TABLE => "resource table",
38            Self::XML => "XML document",
39            Self::XML_START_NAMESPACE => "XML namespace start",
40            Self::XML_END_NAMESPACE => "XML namespace end",
41            Self::XML_START_ELEMENT => "XML element start",
42            Self::XML_END_ELEMENT => "XML element end",
43            Self::XML_CDATA => "XML text",
44            Self::XML_RESOURCE_MAP => "XML resource map",
45            Self::TABLE_PACKAGE => "package",
46            Self::TABLE_TYPE => "type",
47            Self::TABLE_TYPE_SPEC => "type spec",
48            Self::TABLE_LIBRARY => "library",
49            _ => return None,
50        })
51    }
52}
53
54impl fmt::Display for ChunkType {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        match self.name() {
57            Some(name) => write!(f, "{name} ({:#06x})", self.0),
58            None => write!(f, "unknown ({:#06x})", self.0),
59        }
60    }
61}
62
63impl fmt::Debug for ChunkType {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        write!(f, "ChunkType({:#06x})", self.0)
66    }
67}
68
69/// Malformed chunk framing.
70#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
71#[non_exhaustive]
72pub enum ChunkError {
73    #[error("expected a chunk but found no data")]
74    Missing,
75    #[error("chunk header at offset {offset:#x} is truncated")]
76    TruncatedHeader { offset: usize },
77    #[error("{ty} chunk at offset {offset:#x} declares header size {header_size} and total size {size}")]
78    InvalidSizes { ty: ChunkType, offset: usize, header_size: u16, size: u32 },
79    #[error("{ty} chunk at offset {offset:#x} needs {size} bytes but only {available} remain")]
80    Overflow { ty: ChunkType, offset: usize, size: u32, available: usize },
81    #[error("expected a {expected} chunk, found {found}")]
82    UnexpectedType { expected: ChunkType, found: ChunkType },
83    #[error("{len} bytes of trailing data follow the root chunk")]
84    TrailingData { len: usize },
85    #[error("{ty} chunk header is {size} bytes; the format needs at least {min}")]
86    HeaderTooShort { ty: ChunkType, size: usize, min: usize },
87    #[error("chunk exceeds the format's 4 GiB size limit")]
88    TooLarge,
89}
90
91/// `ResChunk_header`.
92#[binrw]
93#[brw(little)]
94#[derive(Clone, Copy, Debug, Eq, PartialEq)]
95pub(crate) struct ChunkHeader {
96    pub ty: u16,
97    pub header_size: u16,
98    pub size: u32,
99}
100
101impl ChunkHeader {
102    pub const SIZE: usize = 8;
103}
104
105/// A bounds-checked view of one chunk.
106#[derive(Clone, Copy, Debug, Eq, PartialEq)]
107pub(crate) struct Chunk<'a> {
108    pub ty: ChunkType,
109    /// The full chunk, including its header.
110    pub bytes: &'a [u8],
111    /// The chunk-specific header that follows the 8-byte `ResChunk_header`.
112    pub header: &'a [u8],
113    /// Everything after the header.
114    pub body: &'a [u8],
115}
116
117impl<'a> Chunk<'a> {
118    /// Parses the only chunk in `bytes`, which must be of type `expected` and
119    /// fill `bytes` exactly.
120    pub fn parse_root(bytes: &'a [u8], expected: ChunkType) -> Result<Self, ChunkError> {
121        let chunk = Chunks::new(bytes).next().ok_or(ChunkError::Missing)??;
122        if chunk.ty != expected {
123            return Err(ChunkError::UnexpectedType { expected, found: chunk.ty });
124        }
125        match bytes.len() - chunk.bytes.len() {
126            0 => Ok(chunk),
127            len => Err(ChunkError::TrailingData { len }),
128        }
129    }
130
131    /// Reads the chunk-specific header as `T`. Fields that older tools did
132    /// not write read as zero, but the fields every version has must be present.
133    pub fn header<T>(&self) -> Result<T, ChunkError>
134    where
135        T: Header + BinRead + ReadEndian,
136        for<'b> T::Args<'b>: Default,
137    {
138        if self.header.len() < T::MIN_SIZE {
139            let base = ChunkHeader::SIZE;
140            return Err(ChunkError::HeaderTooShort {
141                ty: self.ty,
142                size: base + self.header.len(),
143                min: base + T::MIN_SIZE,
144            });
145        }
146        Ok(read_padded(self.header))
147    }
148
149    pub fn children(&self) -> Chunks<'a> {
150        Chunks::new(self.body)
151    }
152
153    /// Whether this chunk has type `expected`.
154    pub fn expect(&self, expected: ChunkType) -> Result<(), ChunkError> {
155        if self.ty == expected { Ok(()) } else { Err(ChunkError::UnexpectedType { expected, found: self.ty }) }
156    }
157}
158
159/// A chunk-specific header record.
160pub(crate) trait Header {
161    /// The size of the oldest form of the header, after `ResChunk_header`.
162    const MIN_SIZE: usize;
163}
164
165/// Reads a fixed-size record from `bytes`, treating missing trailing bytes as zero.
166///
167/// Records read this way contain only plain integers and arrays, so decoding
168/// a zero-extended buffer cannot fail.
169pub(crate) fn read_padded<T>(bytes: &[u8]) -> T
170where
171    T: BinRead + ReadEndian,
172    for<'b> T::Args<'b>: Default,
173{
174    const MAX_RECORD: usize = 512;
175    let mut padded = [0u8; MAX_RECORD];
176    let len = bytes.len().min(MAX_RECORD);
177    padded[..len].copy_from_slice(&bytes[..len]);
178    T::read(&mut Cursor::new(&padded[..])).expect("fixed-size record fits in a zero-extended buffer")
179}
180
181/// Iterates over consecutive chunks in a byte slice. Iteration stops after
182/// the first error.
183pub(crate) struct Chunks<'a> {
184    data: &'a [u8],
185    offset: usize,
186}
187
188impl<'a> Chunks<'a> {
189    pub fn new(data: &'a [u8]) -> Self {
190        Self { data, offset: 0 }
191    }
192
193    fn next_chunk(&mut self) -> Result<Chunk<'a>, ChunkError> {
194        let offset = self.offset;
195        if self.data.len() < ChunkHeader::SIZE {
196            return Err(ChunkError::TruncatedHeader { offset });
197        }
198        let header: ChunkHeader = read_padded(self.data);
199        let ty = ChunkType(header.ty);
200        let (header_size, size) = (usize::from(header.header_size), header.size as usize);
201        if header_size < ChunkHeader::SIZE || size < header_size {
202            return Err(ChunkError::InvalidSizes { ty, offset, header_size: header.header_size, size: header.size });
203        }
204        if size > self.data.len() {
205            return Err(ChunkError::Overflow { ty, offset, size: header.size, available: self.data.len() });
206        }
207        let bytes = &self.data[..size];
208        self.data = &self.data[size..];
209        self.offset += size;
210        Ok(Chunk { ty, bytes, header: &bytes[ChunkHeader::SIZE..header_size], body: &bytes[header_size..] })
211    }
212}
213
214impl<'a> Iterator for Chunks<'a> {
215    type Item = Result<Chunk<'a>, ChunkError>;
216
217    fn next(&mut self) -> Option<Self::Item> {
218        if self.data.is_empty() {
219            return None;
220        }
221        let chunk = self.next_chunk();
222        if chunk.is_err() {
223            self.data = &[];
224        }
225        Some(chunk)
226    }
227}
228
229/// Little-endian appends for building chunks in memory.
230pub(crate) trait Put {
231    fn put_u16(&mut self, value: u16);
232    fn put_u32(&mut self, value: u32);
233    /// Appends a `binrw` record.
234    fn put<T>(&mut self, value: &T)
235    where
236        T: BinWrite + WriteEndian,
237        for<'b> T::Args<'b>: Default;
238    /// Pads with zeros to a multiple of four bytes.
239    fn align4(&mut self);
240}
241
242impl Put for Vec<u8> {
243    fn put_u16(&mut self, value: u16) {
244        self.extend_from_slice(&value.to_le_bytes());
245    }
246
247    fn put_u32(&mut self, value: u32) {
248        self.extend_from_slice(&value.to_le_bytes());
249    }
250
251    fn put<T>(&mut self, value: &T)
252    where
253        T: BinWrite + WriteEndian,
254        for<'b> T::Args<'b>: Default,
255    {
256        let mut cursor = Cursor::new(self);
257        cursor.set_position(cursor.get_ref().len() as u64);
258        value.write(&mut cursor).expect("plain records always serialize into a Vec");
259    }
260
261    fn align4(&mut self) {
262        self.resize(self.len().next_multiple_of(4), 0);
263    }
264}
265
266/// Appends a chunk: the 8-byte base header, the chunk-specific `header`
267/// record, then whatever `body` writes. Sizes are patched afterwards.
268pub(crate) fn write_chunk<H, E>(
269    out: &mut Vec<u8>,
270    ty: ChunkType,
271    header: &H,
272    body: impl FnOnce(&mut Vec<u8>) -> Result<(), E>,
273) -> Result<(), E>
274where
275    H: BinWrite + WriteEndian,
276    for<'b> H::Args<'b>: Default,
277    E: From<ChunkError>,
278{
279    let start = out.len();
280    out.put(&ChunkHeader { ty: ty.0, header_size: 0, size: 0 });
281    out.put(header);
282    let header_size = u16::try_from(out.len() - start).map_err(|_| ChunkError::TooLarge)?;
283    body(out)?;
284    let size = u32::try_from(out.len() - start).map_err(|_| ChunkError::TooLarge)?;
285    out[start + 2..start + 4].copy_from_slice(&header_size.to_le_bytes());
286    out[start + 4..start + 8].copy_from_slice(&size.to_le_bytes());
287    Ok(())
288}
289
290/// A chunk-specific header with no fields.
291#[binrw]
292#[brw(little)]
293pub(crate) struct NoHeader;
294
295impl Header for NoHeader {
296    const MIN_SIZE: usize = 0;
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302
303    #[test]
304    fn rejects_overlapping_or_truncated_chunks() {
305        let too_small = [0x02, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x00, 0x00];
306        assert_eq!(
307            Chunks::new(&too_small).next(),
308            Some(Err(ChunkError::InvalidSizes { ty: ChunkType::TABLE, offset: 0, header_size: 12, size: 8 }))
309        );
310        let too_big = [0x02, 0x00, 0x08, 0x00, 0xff, 0x00, 0x00, 0x00];
311        assert_eq!(
312            Chunks::new(&too_big).next(),
313            Some(Err(ChunkError::Overflow { ty: ChunkType::TABLE, offset: 0, size: 255, available: 8 }))
314        );
315        assert_eq!(Chunks::new(&[0x02, 0x00]).next(), Some(Err(ChunkError::TruncatedHeader { offset: 0 })));
316    }
317
318    #[test]
319    fn writes_nested_chunks_with_patched_sizes() {
320        let mut out = Vec::new();
321        write_chunk::<_, ChunkError>(&mut out, ChunkType::XML, &NoHeader, |out| {
322            write_chunk(out, ChunkType::XML_RESOURCE_MAP, &NoHeader, |out| {
323                out.put_u32(0x0101_0003);
324                Ok::<_, ChunkError>(())
325            })
326        })
327        .unwrap();
328        let root = Chunk::parse_root(&out, ChunkType::XML).unwrap();
329        let child = root.children().next().unwrap().unwrap();
330        assert_eq!(child.ty, ChunkType::XML_RESOURCE_MAP);
331        assert_eq!(child.body, 0x0101_0003u32.to_le_bytes());
332        assert_eq!(
333            Chunk::parse_root(&out, ChunkType::TABLE).unwrap_err(),
334            ChunkError::UnexpectedType { expected: ChunkType::TABLE, found: ChunkType::XML }
335        );
336    }
337}