Skip to main content

rpi_loader_ota/
bundle.rs

1//! The container: what a bundle is on the wire, and how to check one.
2//!
3//! # Layout
4//!
5//! Little-endian throughout.
6//!
7//! ```text
8//! magic   : 4 bytes             per-application; see [`Format`]
9//! version : u8      = 2
10//! reserved: u8      = 0
11//! count   : u16                 number of entries
12//! entries : count x {
13//!             role    : u8      see [`Role`]
14//!             path_len: u8
15//!             path    : [u8; path_len]
16//!             size    : u32
17//!             data    : [u8; size]
18//!           }
19//! crc32   : u32                 IEEE CRC-32 over every preceding byte
20//! ```
21//!
22//! A bundle is a list of **(path, role, bytes)**. Where each entry lands is
23//! the bundle's business, not the receiving firmware's — which is the whole
24//! difference from the format this replaces, where a kernel came first and
25//! everything after it went into one directory compiled into the device.
26//!
27//! # Why there is no per-entry checksum
28//!
29//! It was in the design and did not survive being written down. Its purpose
30//! was to let a device skip rewriting a file whose bytes are already on the
31//! card — but the entry's data is in memory beside the comparison, so the
32//! expected checksum can be computed there with [`checksum`] for nothing.
33//! A copy on the wire would be four bytes per entry that no reader needs,
34//! and a second place for the same number to be wrong.
35//!
36//! The whole-bundle checksum stays, because that one covers something no
37//! local computation can: that the bytes which arrived are the bytes that
38//! were sent.
39
40use crc::{CRC_32_ISO_HDLC, Crc};
41
42/// Bundle format version this crate reads and writes.
43///
44/// Version 1 existed, carried a kernel plus a flat list of names, and is
45/// deliberately not readable here — see the crate documentation.
46pub const VERSION: u8 = 2;
47
48/// Magic, version, reserved byte and entry count.
49const HEADER_LEN: usize = 8;
50
51/// The trailing whole-bundle CRC-32.
52const TRAILER_LEN: usize = 4;
53
54/// A header and a checksum with nothing between them.
55const MIN_LEN: usize = HEADER_LEN + TRAILER_LEN;
56
57/// IEEE CRC-32, the same one the host packer computes.
58///
59/// A `static` rather than a `const` because [`Checksum`] borrows it for the
60/// life of the program: a `const` is a fresh temporary at every mention and
61/// cannot be borrowed past the expression naming it.
62static CRC32: Crc<u32> = Crc::<u32>::new(&CRC_32_ISO_HDLC);
63
64/// The IEEE CRC-32 of `data`, as it appears in a bundle.
65///
66/// Exposed so that a device comparing a file on its card against an entry
67/// checksums both the same way. Getting that wrong would not corrupt
68/// anything — it would silently rewrite every file on every update, which
69/// is worse, because nothing would ever report it.
70pub fn checksum(data: &[u8]) -> u32 {
71    CRC32.checksum(data)
72}
73
74/// The same checksum, over data that arrives in pieces.
75///
76/// [`checksum`] is what a caller holding the whole thing wants. This is for
77/// the other side of the comparison — a file already on a card, read a chunk
78/// at a time precisely so that checking it needs no second copy of it in
79/// memory.
80pub struct Checksum(crc::Digest<'static, u32>);
81
82impl Checksum {
83    /// Starts one.
84    pub fn new() -> Checksum {
85        Checksum(CRC32.digest())
86    }
87
88    /// Adds the next piece.
89    pub fn update(&mut self, data: &[u8]) {
90        self.0.update(data);
91    }
92
93    /// The checksum of everything added, comparable with [`checksum`] of the
94    /// same bytes.
95    pub fn finish(self) -> u32 {
96        self.0.finalize()
97    }
98}
99
100impl Default for Checksum {
101    fn default() -> Checksum {
102        Checksum::new()
103    }
104}
105
106impl core::fmt::Debug for Checksum {
107    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
108        // `crc::Digest` is not `Debug`, and what it holds mid-stream is not
109        // something a reader could act on anyway.
110        f.write_str("Checksum(..)")
111    }
112}
113
114/// What an application must agree with its packer about.
115///
116/// Everything else a bundle needs to describe is in the bundle.
117#[derive(Clone, Copy, Debug, PartialEq, Eq)]
118pub struct Format {
119    /// Four bytes identifying which application a bundle was built for, so
120    /// that one board rejects another's update.
121    ///
122    /// It is compiled into the firmware and written by the packer, and it
123    /// is the only compatibility check there is: nothing else in a bundle
124    /// says which board it belongs to. A project shipping more than one
125    /// architecture should therefore vary it — `WAT7` against `WAT8` —
126    /// since an image for the wrong architecture is otherwise a bundle that
127    /// installs perfectly and does not boot.
128    pub magic: [u8; 4],
129    /// Most entries to accept.
130    ///
131    /// A bound on what a single upload can ask the device to write, checked
132    /// before any entry is walked.
133    pub max_entries: usize,
134}
135
136/// How the device should treat an entry.
137///
138/// The byte values are wire values and say nothing about the order entries
139/// are written in — that order is the installer's, and it is not this one.
140#[derive(Clone, Copy, Debug, PartialEq, Eq)]
141pub enum Role {
142    /// Anything the application owns: assets, settings, data.
143    File,
144    /// The boot image, to be routed to whichever kernel slot is not
145    /// running. At most one per bundle.
146    Kernel,
147    /// A Raspberry Pi firmware file — `bootcode.bin`, `start*.elf`,
148    /// `fixup*.dat`.
149    Firmware,
150    /// `config.txt`. At most one per bundle, because the installer edits
151    /// the file this entry lands as.
152    Config,
153}
154
155impl Role {
156    /// The wire byte for this role.
157    pub fn as_byte(self) -> u8 {
158        match self {
159            Role::File => 0,
160            Role::Kernel => 1,
161            Role::Firmware => 2,
162            Role::Config => 3,
163        }
164    }
165
166    /// The role a wire byte names, or `None` if it names none.
167    pub fn from_byte(byte: u8) -> Option<Role> {
168        match byte {
169            0 => Some(Role::File),
170            1 => Some(Role::Kernel),
171            2 => Some(Role::Firmware),
172            3 => Some(Role::Config),
173            _ => None,
174        }
175    }
176}
177
178/// One file in a bundle, borrowed from the buffer it arrived in.
179#[derive(Clone, Copy, Debug, PartialEq, Eq)]
180pub struct Entry<'a> {
181    /// How the device should treat it.
182    pub role: Role,
183    /// Where it goes, relative to the root of the volume. Always a valid
184    /// relative path when it came from [`Bundle::parse`].
185    pub path: &'a str,
186    /// The file's contents.
187    pub data: &'a [u8],
188}
189
190/// Why a bundle was rejected, reading one or building one.
191///
192/// Most variants can arise either way, because most of what makes a bundle
193/// wrong is a property of its entries rather than of its bytes — and one
194/// vocabulary for both directions is what keeps a packer from cheerfully
195/// building something no device will take. The exceptions are marked.
196#[derive(Clone, Copy, Debug, PartialEq, Eq)]
197pub enum Error {
198    /// Fewer bytes than a header and a checksum. Reading only.
199    TooShort,
200    /// The magic did not match the one this application expects. Reading
201    /// only.
202    BadMagic,
203    /// A format version this build does not read. Carries the version seen.
204    /// Reading only.
205    BadVersion(u8),
206    /// The trailing CRC-32 did not match the content. Reading only.
207    BadChecksum,
208    /// No entries at all, or more than [`Format::max_entries`].
209    BadEntryCount(usize),
210    /// An entry ran past the end of the bundle, or the entries did not fill
211    /// it exactly. Reading only.
212    Truncated,
213    /// An entry named a role this build does not know. Carries the byte.
214    /// Reading only.
215    BadRole(u8),
216    /// An entry's path was not valid UTF-8. Reading only — a path being
217    /// built is already a `&str`.
218    PathNotUtf8,
219    /// A path longer than the 255 bytes its length field can hold. Building
220    /// only.
221    PathTooLong,
222    /// A file larger than the 4 GiB its size field can hold. Building only.
223    DataTooLarge,
224    /// An entry's path was empty, absolute, or contained `.`, `..`, an
225    /// empty component or a backslash.
226    BadPath,
227    /// Two entries wanted the same path.
228    DuplicatePath,
229    /// More than one entry claimed a role that allows only one.
230    DuplicateRole(Role),
231    /// A `start*.elf` arrived without its `fixup*.dat`, or the reverse.
232    UnpairedFirmware,
233}
234
235impl Error {
236    /// The HTTP status to answer this with, for a device that took the
237    /// bundle over HTTP.
238    ///
239    /// Always 400, and that is the content of the method rather than a
240    /// placeholder: every rejection in this module is a statement about the
241    /// bytes that arrived, so none of them is ever the receiver's fault. An
242    /// installer whose errors are not all client errors wraps this one and
243    /// answers for the rest itself.
244    pub fn http_status(&self) -> u16 {
245        400
246    }
247}
248
249impl core::fmt::Display for Error {
250    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
251        let text = match self {
252            Error::TooShort => "bundle too short",
253            Error::BadMagic => "not a bundle for this device",
254            Error::BadVersion(_) => "unsupported bundle version",
255            Error::BadChecksum => "checksum mismatch",
256            Error::BadEntryCount(_) => "bad entry count",
257            Error::Truncated => "bundle truncated",
258            Error::BadRole(_) => "unknown entry role",
259            Error::PathNotUtf8 => "bad file path",
260            Error::PathTooLong => "file path too long",
261            Error::DataTooLarge => "file too large",
262            Error::BadPath => "unsafe file path",
263            Error::DuplicatePath => "two entries with the same path",
264            Error::DuplicateRole(_) => "two entries with the same role",
265            Error::UnpairedFirmware => "firmware file without its pair",
266        };
267        f.write_str(text)
268    }
269}
270
271impl core::error::Error for Error {}
272
273/// A validated bundle: a handle onto the buffer it was parsed from.
274///
275/// Holding one is the proof that every check in this module passed, which
276/// is why walking it with [`iter`](Bundle::iter) cannot fail.
277#[derive(Clone, Copy, Debug)]
278pub struct Bundle<'a> {
279    /// Exactly the entry region — header and trailing checksum removed.
280    entries: &'a [u8],
281    count: usize,
282}
283
284impl<'a> Bundle<'a> {
285    /// Checks `bytes` against `format` and returns a handle to it.
286    ///
287    /// Allocates nothing: the result borrows the buffer.
288    ///
289    /// # Order of checks
290    ///
291    /// The checksum is verified **before** any entry is walked. A transfer
292    /// that lost bytes produces lengths that still look plausible, and
293    /// reporting the first one that happens not to fit describes a symptom
294    /// rather than what went wrong.
295    pub fn parse(format: &Format, bytes: &'a [u8]) -> Result<Bundle<'a>, Error> {
296        if bytes.len() < MIN_LEN {
297            return Err(Error::TooShort);
298        }
299        if bytes[..4] != format.magic {
300            return Err(Error::BadMagic);
301        }
302        if bytes[4] != VERSION {
303            return Err(Error::BadVersion(bytes[4]));
304        }
305
306        let split = bytes.len() - TRAILER_LEN;
307        let stored = u32::from_le_bytes([
308            bytes[split],
309            bytes[split + 1],
310            bytes[split + 2],
311            bytes[split + 3],
312        ]);
313        if checksum(&bytes[..split]) != stored {
314            return Err(Error::BadChecksum);
315        }
316
317        let count = u16::from_le_bytes([bytes[6], bytes[7]]) as usize;
318        if count == 0 || count > format.max_entries {
319            return Err(Error::BadEntryCount(count));
320        }
321
322        // Walked here, and again by every `iter()` afterwards. Two passes
323        // over a buffer already in memory, in exchange for an iterator that
324        // needs no error type and a handle that cannot name a bundle it has
325        // not checked.
326        let entries = &bytes[HEADER_LEN..split];
327        let mut rest = entries;
328        for _ in 0..count {
329            let (_, next) = split_entry(rest)?;
330            rest = next;
331        }
332        // The count and the bytes have to agree exactly. Anything left over
333        // is a bundle whose header does not describe it.
334        if !rest.is_empty() {
335            return Err(Error::Truncated);
336        }
337
338        // Structure first, then meaning: everything above is about whether
339        // there is a bundle here at all, and `check_entries` is about
340        // whether the entries in it make sense. A torn transfer trips the
341        // first kind, and saying so is more use than reporting whichever
342        // rule the wreckage happened to break.
343        let bundle = Bundle { entries, count };
344        check_entries(bundle.iter())?;
345        Ok(bundle)
346    }
347
348    /// How many entries it holds.
349    pub fn count(&self) -> usize {
350        self.count
351    }
352
353    /// The entries, in the order they were packed.
354    ///
355    /// Not the order they should be written in — see [`Role`].
356    pub fn iter(&self) -> Iter<'a> {
357        Iter {
358            rest: self.entries,
359            left: self.count,
360        }
361    }
362
363    /// The entry carrying the boot image, if the bundle has one.
364    ///
365    /// A bundle without a kernel is valid and useful: it updates a website
366    /// or a settings file without rewriting an image that has not changed.
367    pub fn kernel(&self) -> Option<Entry<'a>> {
368        self.iter().find(|entry| entry.role == Role::Kernel)
369    }
370}
371
372/// Every rule about a set of entries that does not depend on which
373/// direction they came from.
374///
375/// Both halves of this module run it: [`Bundle::parse`] after it has proved
376/// there is a bundle there at all, and [`encode`] before it writes one. That
377/// is the point — a packer that could build what a device rejects, or the
378/// reverse, is the drift this crate exists to remove.
379///
380/// Takes a cloneable iterator rather than a slice so that a decoded bundle
381/// can be checked without collecting it into one, which is what keeps
382/// reading allocation-free.
383fn check_entries<'a, I>(entries: I) -> Result<(), Error>
384where
385    I: Iterator<Item = Entry<'a>> + Clone,
386{
387    for entry in entries.clone() {
388        check_path(entry.path)?;
389    }
390    check_unique_paths(entries.clone())?;
391    check_single(entries.clone(), Role::Kernel)?;
392    check_single(entries.clone(), Role::Config)?;
393    check_firmware_pairs(entries)
394}
395
396/// Rejects two entries wanting the same destination.
397///
398/// The second would simply overwrite the first, so the file the packer
399/// meant to ship is not the one that lands — a failure with no symptom at
400/// the time and a confusing one later.
401fn check_unique_paths<'a, I>(entries: I) -> Result<(), Error>
402where
403    I: Iterator<Item = Entry<'a>> + Clone,
404{
405    for (index, entry) in entries.clone().enumerate() {
406        if entries
407            .clone()
408            .skip(index + 1)
409            .any(|other| other.path == entry.path)
410        {
411            return Err(Error::DuplicatePath);
412        }
413    }
414    Ok(())
415}
416
417/// Rejects a second entry claiming a role that admits only one.
418fn check_single<'a, I>(entries: I, role: Role) -> Result<(), Error>
419where
420    I: Iterator<Item = Entry<'a>>,
421{
422    if entries.filter(|entry| entry.role == role).count() > 1 {
423        return Err(Error::DuplicateRole(role));
424    }
425    Ok(())
426}
427
428/// Rejects a `start*.elf` without its `fixup*.dat`, or the reverse.
429///
430/// The two are released together and a mismatched pair does not boot, so
431/// shipping one of them is a packing mistake whose cost is a board that
432/// needs its card pulled. Checking it is a few lines here and impossible
433/// anywhere later.
434///
435/// Only the pairing is checked, not the names: an entry marked
436/// [`Role::Firmware`] may be called anything. A list of the files Raspberry
437/// Pi currently ships would reject the one they add next, and this crate
438/// has no way to be right about that list over time.
439fn check_firmware_pairs<'a, I>(entries: I) -> Result<(), Error>
440where
441    I: Iterator<Item = Entry<'a>> + Clone,
442{
443    for entry in entries.clone().filter(|entry| entry.role == Role::Firmware) {
444        let name = basename(entry.path);
445        let paired = if let Some(suffix) = strip_around(name, "start", ".elf") {
446            has_firmware(entries.clone(), "fixup", suffix, ".dat")
447        } else if let Some(suffix) = strip_around(name, "fixup", ".dat") {
448            has_firmware(entries.clone(), "start", suffix, ".elf")
449        } else {
450            // `bootcode.bin`, or anything else with no partner.
451            true
452        };
453        if !paired {
454            return Err(Error::UnpairedFirmware);
455        }
456    }
457    Ok(())
458}
459
460/// Whether a firmware entry named `<prefix><middle><extension>` is present,
461/// comparing the way FAT does.
462fn has_firmware<'a, I>(entries: I, prefix: &str, middle: &str, extension: &str) -> bool
463where
464    I: Iterator<Item = Entry<'a>>,
465{
466    entries
467        .filter(|entry| entry.role == Role::Firmware)
468        .any(|entry| {
469            strip_around(basename(entry.path), prefix, extension)
470                .is_some_and(|found| found.eq_ignore_ascii_case(middle))
471        })
472}
473
474/// Builds a bundle from `entries`.
475///
476/// The entries are written in the order given. That order carries no
477/// meaning — an installer decides what to write when from
478/// [`Role`], not from where an entry sits — so a packer is free to emit
479/// them in whatever order is convenient to produce.
480///
481/// # Errors
482///
483/// Every rule [`Bundle::parse`] would apply to the result is applied here
484/// first, so this cannot produce a bundle its own parser rejects, plus the
485/// two limits of the container itself: [`Error::PathTooLong`] and
486/// [`Error::DataTooLarge`].
487#[cfg(feature = "alloc")]
488pub fn encode(format: &Format, entries: &[Entry<'_>]) -> Result<alloc::vec::Vec<u8>, Error> {
489    // A count that does not fit the header's `u16` is caught here rather
490    // than by the cast below, which would otherwise write a plausible small
491    // number and produce a bundle describing a fraction of itself.
492    if entries.is_empty() || entries.len() > format.max_entries || entries.len() > u16::MAX as usize
493    {
494        return Err(Error::BadEntryCount(entries.len()));
495    }
496    check_entries(entries.iter().copied())?;
497
498    let mut size = HEADER_LEN + TRAILER_LEN;
499    for entry in entries {
500        if entry.path.len() > u8::MAX as usize {
501            return Err(Error::PathTooLong);
502        }
503        // Unreachable where `usize` is 32 bits, and unreachable in practice
504        // anywhere else — a 4 GiB entry is not a thing anyone packs. Here
505        // because the alternative is a silently truncated length field, and
506        // a check that never fires costs one comparison per entry.
507        if entry.data.len() as u64 > u32::MAX as u64 {
508            return Err(Error::DataTooLarge);
509        }
510        size += 1 + 1 + entry.path.len() + 4 + entry.data.len();
511    }
512
513    // Sized up front. A bundle carrying a kernel and the Raspberry Pi
514    // firmware is several megabytes, and growing a buffer to that from
515    // nothing copies it a dozen times for no reason.
516    let mut out = alloc::vec::Vec::with_capacity(size);
517    out.extend_from_slice(&format.magic);
518    out.push(VERSION);
519    out.push(0);
520    out.extend_from_slice(&(entries.len() as u16).to_le_bytes());
521    for entry in entries {
522        out.push(entry.role.as_byte());
523        out.push(entry.path.len() as u8);
524        out.extend_from_slice(entry.path.as_bytes());
525        out.extend_from_slice(&(entry.data.len() as u32).to_le_bytes());
526        out.extend_from_slice(entry.data);
527    }
528    out.extend_from_slice(&checksum(&out).to_le_bytes());
529    Ok(out)
530}
531
532/// Walks the entries of a [`Bundle`].
533#[derive(Clone, Debug)]
534pub struct Iter<'a> {
535    rest: &'a [u8],
536    left: usize,
537}
538
539impl<'a> Iterator for Iter<'a> {
540    type Item = Entry<'a>;
541
542    fn next(&mut self) -> Option<Entry<'a>> {
543        if self.left == 0 {
544            return None;
545        }
546        // `Bundle::parse` walked this exact region and rejected everything
547        // `split_entry` can fail on, so the failing arm is unreachable for
548        // any `Bundle` that exists. Ending the walk is the conservative
549        // response to a bug reaching it: a caller sees fewer entries than
550        // `count()` promised, rather than an entry that is not there.
551        let (entry, rest) = split_entry(self.rest).ok()?;
552        self.rest = rest;
553        self.left -= 1;
554        Some(entry)
555    }
556
557    fn size_hint(&self) -> (usize, Option<usize>) {
558        // No lower bound, for the same reason `next` can stop early.
559        (0, Some(self.left))
560    }
561}
562
563/// Splits one entry off the front of `rest`, returning it and what follows.
564fn split_entry(rest: &[u8]) -> Result<(Entry<'_>, &[u8]), Error> {
565    let (&role, rest) = rest.split_first().ok_or(Error::Truncated)?;
566    let role = Role::from_byte(role).ok_or(Error::BadRole(role))?;
567
568    let (&path_len, rest) = rest.split_first().ok_or(Error::Truncated)?;
569    let (path, rest) = rest
570        .split_at_checked(path_len as usize)
571        .ok_or(Error::Truncated)?;
572    let path = core::str::from_utf8(path).map_err(|_| Error::PathNotUtf8)?;
573
574    let (size, rest) = rest.split_at_checked(4).ok_or(Error::Truncated)?;
575    let size = u32::from_le_bytes([size[0], size[1], size[2], size[3]]) as usize;
576    let (data, rest) = rest.split_at_checked(size).ok_or(Error::Truncated)?;
577
578    Ok((Entry { role, path, data }, rest))
579}
580
581/// Rejects a path that is not a plain relative one.
582///
583/// This is about a bundle escaping the volume it is being written to, and
584/// is unrelated to whether writing a particular file is a good idea — a
585/// bundle is allowed to replace `config.txt` and the boot firmware, because
586/// being unable to is what made a card reader necessary.
587fn check_path(path: &str) -> Result<(), Error> {
588    if path.is_empty() {
589        return Err(Error::BadPath);
590    }
591    // Redundant with the empty-component rule below, which a leading
592    // separator also trips. Kept because "absolute paths are rejected"
593    // should be readable here rather than derived.
594    if path.starts_with('/') {
595        return Err(Error::BadPath);
596    }
597    // A backslash is a separator to some tools and an ordinary character to
598    // a FAT writer, so a path carrying one lands as a single strangely
599    // named file instead of the two levels whoever packed it meant.
600    if path.contains('\\') {
601        return Err(Error::BadPath);
602    }
603    for component in path.split('/') {
604        if component.is_empty() || component == "." || component == ".." {
605            return Err(Error::BadPath);
606        }
607    }
608    Ok(())
609}
610
611/// The last component of `path`.
612fn basename(path: &str) -> &str {
613    path.rsplit_once('/').map_or(path, |(_, name)| name)
614}
615
616/// The middle of `<prefix><middle><suffix>`, matching the ends the way FAT
617/// compares names.
618fn strip_around<'a>(name: &'a str, prefix: &str, suffix: &str) -> Option<&'a str> {
619    if name.len() < prefix.len() + suffix.len() {
620        return None;
621    }
622    let (head, rest) = name.split_at_checked(prefix.len())?;
623    if !head.eq_ignore_ascii_case(prefix) {
624        return None;
625    }
626    let (middle, tail) = rest.split_at_checked(rest.len() - suffix.len())?;
627    if !tail.eq_ignore_ascii_case(suffix) {
628        return None;
629    }
630    Some(middle)
631}
632
633#[cfg(test)]
634mod tests {
635    use super::*;
636
637    const FORMAT: Format = Format {
638        magic: *b"TEST",
639        max_entries: 8,
640    };
641
642    /// Builds bundles, including malformed ones.
643    ///
644    /// Deliberately not [`encode`], which cannot produce most of what the
645    /// decoder has to reject — and which, being the other half of the code
646    /// under test, would agree with it about a format neither had got
647    /// right. The golden vectors below are the check on that.
648    struct Builder {
649        magic: [u8; 4],
650        version: u8,
651        count: Option<u16>,
652        entries: Vec<(u8, Vec<u8>, Vec<u8>)>,
653        trailing: Vec<u8>,
654        corrupt: bool,
655    }
656
657    impl Builder {
658        fn new() -> Self {
659            Builder {
660                magic: *b"TEST",
661                version: VERSION,
662                count: None,
663                entries: Vec::new(),
664                trailing: Vec::new(),
665                corrupt: false,
666            }
667        }
668
669        fn entry(mut self, role: u8, path: &str, data: &[u8]) -> Self {
670            self.entries
671                .push((role, path.as_bytes().to_vec(), data.to_vec()));
672            self
673        }
674
675        fn raw_path_entry(mut self, role: u8, path: &[u8], data: &[u8]) -> Self {
676            self.entries.push((role, path.to_vec(), data.to_vec()));
677            self
678        }
679
680        fn build(self) -> Vec<u8> {
681            let mut out = Vec::new();
682            out.extend_from_slice(&self.magic);
683            out.push(self.version);
684            out.push(0);
685            let count = self.count.unwrap_or(self.entries.len() as u16);
686            out.extend_from_slice(&count.to_le_bytes());
687            for (role, path, data) in &self.entries {
688                out.push(*role);
689                out.push(path.len() as u8);
690                out.extend_from_slice(path);
691                out.extend_from_slice(&(data.len() as u32).to_le_bytes());
692                out.extend_from_slice(data);
693            }
694            out.extend_from_slice(&self.trailing);
695            let crc = checksum(&out) ^ if self.corrupt { 1 } else { 0 };
696            out.extend_from_slice(&crc.to_le_bytes());
697            out
698        }
699    }
700
701    fn one_file() -> Vec<u8> {
702        Builder::new().entry(0, "WWW/INDEX.HTM", b"hello").build()
703    }
704
705    fn parse(bytes: &[u8]) -> Result<Bundle<'_>, Error> {
706        Bundle::parse(&FORMAT, bytes)
707    }
708
709    fn error(bytes: &[u8]) -> Error {
710        parse(bytes).expect_err("expected this bundle to be rejected")
711    }
712
713    #[test]
714    fn checksum_matches_the_standard_vector() {
715        // The IEEE CRC-32 check value, so a change of polynomial or of
716        // reflection is caught here rather than by a board that will not
717        // boot.
718        assert_eq!(checksum(b"123456789"), 0xCBF4_3926);
719    }
720
721    #[test]
722    fn a_well_formed_bundle_parses() {
723        let bytes = Builder::new()
724            .entry(1, "KERNEL7.IMG", b"kernel bytes")
725            .entry(0, "WWW/INDEX.HTM", b"<html>")
726            .entry(3, "CONFIG.TXT", b"arm_64bit=0\n")
727            .build();
728        let bundle = parse(&bytes).unwrap();
729
730        assert_eq!(bundle.count(), 3);
731        let entries: Vec<_> = bundle.iter().collect();
732        assert_eq!(entries.len(), 3);
733        assert_eq!(entries[0].role, Role::Kernel);
734        assert_eq!(entries[0].path, "KERNEL7.IMG");
735        assert_eq!(entries[0].data, b"kernel bytes");
736        assert_eq!(entries[1].role, Role::File);
737        assert_eq!(entries[1].path, "WWW/INDEX.HTM");
738        assert_eq!(entries[2].role, Role::Config);
739        assert_eq!(bundle.kernel().unwrap().data, b"kernel bytes");
740    }
741
742    #[test]
743    fn a_bundle_with_no_kernel_is_valid() {
744        let bytes = Builder::new().entry(0, "WATER.CFG", b"zone=1").build();
745        let bundle = parse(&bytes).unwrap();
746        assert_eq!(bundle.count(), 1);
747        assert!(bundle.kernel().is_none());
748    }
749
750    #[test]
751    fn an_empty_file_is_valid() {
752        let bytes = Builder::new().entry(0, "EMPTY.TXT", b"").build();
753        assert_eq!(parse(&bytes).unwrap().iter().next().unwrap().data, b"");
754    }
755
756    #[test]
757    fn too_short() {
758        assert_eq!(error(&[]), Error::TooShort);
759        assert_eq!(error(&one_file()[..MIN_LEN - 1]), Error::TooShort);
760    }
761
762    #[test]
763    fn bad_magic() {
764        let mut bytes = one_file();
765        bytes[0] = b'X';
766        assert_eq!(error(&bytes), Error::BadMagic);
767    }
768
769    #[test]
770    fn bad_version() {
771        // The version this replaces, which is the one a stale bundle left
772        // in a `target/` directory would carry.
773        let mut builder = Builder::new().entry(0, "A.TXT", b"a");
774        builder.version = 1;
775        assert_eq!(error(&builder.build()), Error::BadVersion(1));
776    }
777
778    #[test]
779    fn bad_checksum() {
780        let mut bytes = one_file();
781        let last = bytes.len() - TRAILER_LEN - 1;
782        bytes[last] ^= 0xFF;
783        assert_eq!(error(&bytes), Error::BadChecksum);
784    }
785
786    #[test]
787    fn the_checksum_is_checked_before_the_entries() {
788        // A count of zero and a broken checksum together: the checksum is
789        // what a torn transfer actually looks like, and reporting the count
790        // would send someone to look at the packer.
791        let mut builder = Builder::new().entry(0, "A.TXT", b"a");
792        builder.count = Some(0);
793        builder.corrupt = true;
794        assert_eq!(error(&builder.build()), Error::BadChecksum);
795    }
796
797    #[test]
798    fn bad_entry_count() {
799        let mut builder = Builder::new().entry(0, "A.TXT", b"a");
800        builder.count = Some(0);
801        assert_eq!(error(&builder.build()), Error::BadEntryCount(0));
802
803        let mut builder = Builder::new();
804        for index in 0..9 {
805            builder = builder.entry(0, &format!("F{index}.TXT"), b"x");
806        }
807        assert_eq!(error(&builder.build()), Error::BadEntryCount(9));
808    }
809
810    #[test]
811    fn an_entry_running_past_the_end_is_truncated() {
812        let mut builder = Builder::new().entry(0, "A.TXT", b"abc");
813        builder.count = Some(2);
814        assert_eq!(error(&builder.build()), Error::Truncated);
815    }
816
817    #[test]
818    fn slack_between_the_last_entry_and_the_checksum_is_truncated() {
819        let mut builder = Builder::new().entry(0, "A.TXT", b"abc");
820        builder.trailing = vec![0; 3];
821        assert_eq!(error(&builder.build()), Error::Truncated);
822    }
823
824    #[test]
825    fn bad_role() {
826        let bytes = Builder::new().entry(4, "A.TXT", b"a").build();
827        assert_eq!(error(&bytes), Error::BadRole(4));
828    }
829
830    #[test]
831    fn path_not_utf8() {
832        let bytes = Builder::new()
833            .raw_path_entry(0, &[0xFF, 0xFE], b"a")
834            .build();
835        assert_eq!(error(&bytes), Error::PathNotUtf8);
836    }
837
838    #[test]
839    fn unsafe_paths() {
840        for path in [
841            "",
842            "/ABS.TXT",
843            "../ESCAPE.TXT",
844            "WWW/../ESCAPE.TXT",
845            "./HERE.TXT",
846            "WWW//DOUBLE.TXT",
847            "TRAILING/",
848            "WWW\\INDEX.HTM",
849        ] {
850            let bytes = Builder::new().entry(0, path, b"x").build();
851            assert_eq!(error(&bytes), Error::BadPath, "path {path:?} was accepted");
852        }
853    }
854
855    #[test]
856    fn nested_paths_are_fine() {
857        let bytes = Builder::new().entry(0, "CERTS/ROOTS/CA.PEM", b"x").build();
858        assert_eq!(parse(&bytes).unwrap().count(), 1);
859    }
860
861    #[test]
862    fn duplicate_path() {
863        let bytes = Builder::new()
864            .entry(0, "WWW/INDEX.HTM", b"one")
865            .entry(0, "WWW/INDEX.HTM", b"two")
866            .build();
867        assert_eq!(error(&bytes), Error::DuplicatePath);
868    }
869
870    #[test]
871    fn two_kernels() {
872        let bytes = Builder::new()
873            .entry(1, "KERNEL7.IMG", b"a")
874            .entry(1, "KERNEL8.IMG", b"b")
875            .build();
876        assert_eq!(error(&bytes), Error::DuplicateRole(Role::Kernel));
877    }
878
879    #[test]
880    fn two_configs() {
881        let bytes = Builder::new()
882            .entry(3, "CONFIG.TXT", b"a")
883            .entry(3, "BOOT/CONFIG.TXT", b"b")
884            .build();
885        assert_eq!(error(&bytes), Error::DuplicateRole(Role::Config));
886    }
887
888    #[test]
889    fn a_start_without_its_fixup_is_unpaired() {
890        let bytes = Builder::new().entry(2, "START.ELF", b"a").build();
891        assert_eq!(error(&bytes), Error::UnpairedFirmware);
892    }
893
894    #[test]
895    fn a_fixup_without_its_start_is_unpaired() {
896        let bytes = Builder::new().entry(2, "FIXUP.DAT", b"a").build();
897        assert_eq!(error(&bytes), Error::UnpairedFirmware);
898    }
899
900    #[test]
901    fn a_pair_with_mismatched_suffixes_is_unpaired() {
902        let bytes = Builder::new()
903            .entry(2, "START4.ELF", b"a")
904            .entry(2, "FIXUP.DAT", b"b")
905            .build();
906        assert_eq!(error(&bytes), Error::UnpairedFirmware);
907    }
908
909    #[test]
910    fn a_matched_pair_is_accepted_whatever_the_case_and_suffix() {
911        for (start, fixup) in [
912            ("START.ELF", "FIXUP.DAT"),
913            ("start.elf", "fixup.dat"),
914            ("Start4.Elf", "fixup4.DAT"),
915            ("BOOT/start_x.elf", "FIXUP_X.DAT"),
916        ] {
917            let bytes = Builder::new()
918                .entry(2, start, b"a")
919                .entry(2, fixup, b"b")
920                .build();
921            assert!(parse(&bytes).is_ok(), "{start} + {fixup} was rejected");
922        }
923    }
924
925    #[test]
926    fn firmware_with_no_partner_needs_none() {
927        let bytes = Builder::new().entry(2, "BOOTCODE.BIN", b"a").build();
928        assert_eq!(parse(&bytes).unwrap().count(), 1);
929    }
930
931    #[test]
932    fn every_rejection_is_the_senders_fault() {
933        assert_eq!(Error::TooShort.http_status(), 400);
934        assert_eq!(Error::BadChecksum.http_status(), 400);
935        assert_eq!(Error::UnpairedFirmware.http_status(), 400);
936    }
937
938    #[test]
939    fn roles_round_trip_through_their_wire_bytes() {
940        for role in [Role::File, Role::Kernel, Role::Firmware, Role::Config] {
941            assert_eq!(Role::from_byte(role.as_byte()), Some(role));
942        }
943        assert_eq!(Role::from_byte(4), None);
944    }
945
946    // --- Golden vectors -------------------------------------------------
947    //
948    // Laid out by hand from the format at the top of this module, byte by
949    // byte, and never regenerated from `encode`. That is the whole value of
950    // them: a round trip only proves the encoder and the decoder agree, and
951    // they would agree just as happily about a container that had drifted.
952    // These fail instead.
953    //
954    // The checksums were computed by Python's `zlib.crc32` — a third
955    // implementation, so the constant in this crate is pinned to something
956    // outside it.
957
958    /// One file, nothing else. The smallest bundle that is not an error.
959    const GOLDEN_ONE_FILE: &[u8] = &[
960        b'T', b'E', b'S', b'T', // magic
961        2,    // version
962        0,    // reserved
963        1, 0, // count = 1
964        0, // [0] role = file
965        5, // path_len
966        b'A', b'.', b'T', b'X', b'T', // path
967        3, 0, 0, 0, // size = 3
968        b'a', b'b', b'c', // data
969        0xE9, 0xB1, 0x6D, 0xB9, // crc32 of everything above
970    ];
971
972    /// A kernel, a nested asset and a `config.txt` — one entry of every
973    /// role that an ordinary update carries.
974    const GOLDEN_FULL: &[u8] = &[
975        b'T', b'E', b'S', b'T', // magic
976        2,    // version
977        0,    // reserved
978        3, 0,  // count = 3
979        1,  // [0] role = kernel
980        11, // path_len
981        b'K', b'E', b'R', b'N', b'E', b'L', b'7', b'.', b'I', b'M', b'G', 4, 0, 0,
982        0, // size = 4
983        b'k', b'r', b'n', b'l', 0,  // [1] role = file
984        13, // path_len
985        b'W', b'W', b'W', b'/', b'I', b'N', b'D', b'E', b'X', b'.', b'H', b'T', b'M', 6, 0, 0,
986        0, // size = 6
987        b'<', b'h', b't', b'm', b'l', b'>', 3,  // [2] role = config
988        10, // path_len
989        b'C', b'O', b'N', b'F', b'I', b'G', b'.', b'T', b'X', b'T', 12, 0, 0, 0, // size = 12
990        b'a', b'r', b'm', b'_', b'6', b'4', b'b', b'i', b't', b'=', b'0', b'\n', 0xB0, 0xBD, 0xAA,
991        0xA1, // crc32
992    ];
993
994    /// A firmware pair and no kernel — an update that replaces the
995    /// Raspberry Pi firmware and leaves the boot image alone.
996    const GOLDEN_FIRMWARE: &[u8] = &[
997        b'T', b'E', b'S', b'T', // magic
998        2,    // version
999        0,    // reserved
1000        2, 0, // count = 2
1001        2, // [0] role = firmware
1002        9, // path_len
1003        b'S', b'T', b'A', b'R', b'T', b'.', b'E', b'L', b'F', 2, 0, 0, 0, // size = 2
1004        b's', b'e', 2, // [1] role = firmware
1005        9, // path_len
1006        b'F', b'I', b'X', b'U', b'P', b'.', b'D', b'A', b'T', 2, 0, 0, 0, // size = 2
1007        b'f', b'd', 0x4F, 0xA4, 0x7B, 0xD1, // crc32
1008    ];
1009
1010    /// Both directions against bytes neither direction produced.
1011    fn check_golden(bytes: &[u8], entries: &[Entry<'_>]) {
1012        let bundle = parse(bytes).expect("the golden vector should parse");
1013        assert_eq!(bundle.iter().collect::<Vec<_>>(), entries);
1014        assert_eq!(encode(&FORMAT, entries).unwrap(), bytes);
1015    }
1016
1017    #[test]
1018    fn golden_one_file() {
1019        check_golden(
1020            GOLDEN_ONE_FILE,
1021            &[Entry {
1022                role: Role::File,
1023                path: "A.TXT",
1024                data: b"abc",
1025            }],
1026        );
1027    }
1028
1029    #[test]
1030    fn golden_full() {
1031        check_golden(
1032            GOLDEN_FULL,
1033            &[
1034                Entry {
1035                    role: Role::Kernel,
1036                    path: "KERNEL7.IMG",
1037                    data: b"krnl",
1038                },
1039                Entry {
1040                    role: Role::File,
1041                    path: "WWW/INDEX.HTM",
1042                    data: b"<html>",
1043                },
1044                Entry {
1045                    role: Role::Config,
1046                    path: "CONFIG.TXT",
1047                    data: b"arm_64bit=0\n",
1048                },
1049            ],
1050        );
1051    }
1052
1053    #[test]
1054    fn golden_firmware() {
1055        check_golden(
1056            GOLDEN_FIRMWARE,
1057            &[
1058                Entry {
1059                    role: Role::Firmware,
1060                    path: "START.ELF",
1061                    data: b"se",
1062                },
1063                Entry {
1064                    role: Role::Firmware,
1065                    path: "FIXUP.DAT",
1066                    data: b"fd",
1067                },
1068            ],
1069        );
1070    }
1071
1072    // --- Encoding -------------------------------------------------------
1073
1074    #[test]
1075    fn what_is_encoded_parses_back() {
1076        let big = vec![0xA5; 100_000];
1077        let entries = [
1078            Entry {
1079                role: Role::Kernel,
1080                path: "KERNEL8.IMG",
1081                data: &big,
1082            },
1083            Entry {
1084                role: Role::File,
1085                path: "CERTS/ROOTS/CA.PEM",
1086                data: b"-----BEGIN",
1087            },
1088            Entry {
1089                role: Role::File,
1090                path: "EMPTY.BIN",
1091                data: b"",
1092            },
1093        ];
1094        let bytes = encode(&FORMAT, &entries).unwrap();
1095        let bundle = parse(&bytes).unwrap();
1096        assert_eq!(bundle.iter().collect::<Vec<_>>(), entries);
1097        assert_eq!(bundle.kernel().unwrap().data.len(), 100_000);
1098    }
1099
1100    #[test]
1101    fn encoding_reserves_exactly_what_it_writes() {
1102        // Not a style point: the buffer is megabytes in practice, and a
1103        // capacity that is merely close still reallocates and copies.
1104        let entries = [Entry {
1105            role: Role::File,
1106            path: "A.TXT",
1107            data: b"abc",
1108        }];
1109        let bytes = encode(&FORMAT, &entries).unwrap();
1110        assert_eq!(bytes.len(), bytes.capacity());
1111    }
1112
1113    #[test]
1114    fn another_applications_bundle_is_rejected() {
1115        let entries = [Entry {
1116            role: Role::File,
1117            path: "A.TXT",
1118            data: b"abc",
1119        }];
1120        let bytes = encode(
1121            &Format {
1122                magic: *b"OTHR",
1123                max_entries: 8,
1124            },
1125            &entries,
1126        )
1127        .unwrap();
1128        assert_eq!(error(&bytes), Error::BadMagic);
1129    }
1130
1131    #[test]
1132    fn encoding_applies_every_rule_the_parser_would() {
1133        let file = |path| Entry {
1134            role: Role::File,
1135            path,
1136            data: b"x" as &[u8],
1137        };
1138
1139        assert_eq!(
1140            encode(&FORMAT, &[]).unwrap_err(),
1141            Error::BadEntryCount(0),
1142            "an empty bundle"
1143        );
1144
1145        let many: Vec<_> = ["A", "B", "C", "D", "E", "F", "G", "H", "I"]
1146            .iter()
1147            .map(|path| file(path))
1148            .collect();
1149        assert_eq!(
1150            encode(&FORMAT, &many).unwrap_err(),
1151            Error::BadEntryCount(9),
1152            "more entries than the format allows"
1153        );
1154
1155        assert_eq!(
1156            encode(&FORMAT, &[file("../ESCAPE.TXT")]).unwrap_err(),
1157            Error::BadPath
1158        );
1159        assert_eq!(
1160            encode(&FORMAT, &[file("A.TXT"), file("A.TXT")]).unwrap_err(),
1161            Error::DuplicatePath
1162        );
1163        assert_eq!(
1164            encode(
1165                &FORMAT,
1166                &[
1167                    Entry {
1168                        role: Role::Kernel,
1169                        path: "KERNEL7.IMG",
1170                        data: b"a"
1171                    },
1172                    Entry {
1173                        role: Role::Kernel,
1174                        path: "KERNEL8.IMG",
1175                        data: b"b"
1176                    },
1177                ]
1178            )
1179            .unwrap_err(),
1180            Error::DuplicateRole(Role::Kernel)
1181        );
1182        assert_eq!(
1183            encode(
1184                &FORMAT,
1185                &[Entry {
1186                    role: Role::Firmware,
1187                    path: "START.ELF",
1188                    data: b"a"
1189                }]
1190            )
1191            .unwrap_err(),
1192            Error::UnpairedFirmware
1193        );
1194    }
1195
1196    #[test]
1197    fn a_path_longer_than_its_length_field() {
1198        let long = "A".repeat(256);
1199        assert_eq!(
1200            encode(
1201                &FORMAT,
1202                &[Entry {
1203                    role: Role::File,
1204                    path: &long,
1205                    data: b"x"
1206                }]
1207            )
1208            .unwrap_err(),
1209            Error::PathTooLong
1210        );
1211
1212        // 255 is the largest a `u8` length field can describe, so it has to
1213        // work rather than being one past a limit nobody tested.
1214        let limit = "A".repeat(255);
1215        assert!(
1216            encode(
1217                &FORMAT,
1218                &[Entry {
1219                    role: Role::File,
1220                    path: &limit,
1221                    data: b"x"
1222                }]
1223            )
1224            .is_ok()
1225        );
1226    }
1227}