use crc::{CRC_32_ISO_HDLC, Crc};
pub const VERSION: u8 = 2;
const HEADER_LEN: usize = 8;
const TRAILER_LEN: usize = 4;
const MIN_LEN: usize = HEADER_LEN + TRAILER_LEN;
static CRC32: Crc<u32> = Crc::<u32>::new(&CRC_32_ISO_HDLC);
pub fn checksum(data: &[u8]) -> u32 {
CRC32.checksum(data)
}
pub struct Checksum(crc::Digest<'static, u32>);
impl Checksum {
pub fn new() -> Checksum {
Checksum(CRC32.digest())
}
pub fn update(&mut self, data: &[u8]) {
self.0.update(data);
}
pub fn finish(self) -> u32 {
self.0.finalize()
}
}
impl Default for Checksum {
fn default() -> Checksum {
Checksum::new()
}
}
impl core::fmt::Debug for Checksum {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("Checksum(..)")
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Format {
pub magic: [u8; 4],
pub max_entries: usize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Role {
File,
Kernel,
Firmware,
Config,
}
impl Role {
pub fn as_byte(self) -> u8 {
match self {
Role::File => 0,
Role::Kernel => 1,
Role::Firmware => 2,
Role::Config => 3,
}
}
pub fn from_byte(byte: u8) -> Option<Role> {
match byte {
0 => Some(Role::File),
1 => Some(Role::Kernel),
2 => Some(Role::Firmware),
3 => Some(Role::Config),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Entry<'a> {
pub role: Role,
pub path: &'a str,
pub data: &'a [u8],
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Error {
TooShort,
BadMagic,
BadVersion(u8),
BadChecksum,
BadEntryCount(usize),
Truncated,
BadRole(u8),
PathNotUtf8,
PathTooLong,
DataTooLarge,
BadPath,
DuplicatePath,
DuplicateRole(Role),
UnpairedFirmware,
}
impl Error {
pub fn http_status(&self) -> u16 {
400
}
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let text = match self {
Error::TooShort => "bundle too short",
Error::BadMagic => "not a bundle for this device",
Error::BadVersion(_) => "unsupported bundle version",
Error::BadChecksum => "checksum mismatch",
Error::BadEntryCount(_) => "bad entry count",
Error::Truncated => "bundle truncated",
Error::BadRole(_) => "unknown entry role",
Error::PathNotUtf8 => "bad file path",
Error::PathTooLong => "file path too long",
Error::DataTooLarge => "file too large",
Error::BadPath => "unsafe file path",
Error::DuplicatePath => "two entries with the same path",
Error::DuplicateRole(_) => "two entries with the same role",
Error::UnpairedFirmware => "firmware file without its pair",
};
f.write_str(text)
}
}
impl core::error::Error for Error {}
#[derive(Clone, Copy, Debug)]
pub struct Bundle<'a> {
entries: &'a [u8],
count: usize,
}
impl<'a> Bundle<'a> {
pub fn parse(format: &Format, bytes: &'a [u8]) -> Result<Bundle<'a>, Error> {
if bytes.len() < MIN_LEN {
return Err(Error::TooShort);
}
if bytes[..4] != format.magic {
return Err(Error::BadMagic);
}
if bytes[4] != VERSION {
return Err(Error::BadVersion(bytes[4]));
}
let split = bytes.len() - TRAILER_LEN;
let stored = u32::from_le_bytes([
bytes[split],
bytes[split + 1],
bytes[split + 2],
bytes[split + 3],
]);
if checksum(&bytes[..split]) != stored {
return Err(Error::BadChecksum);
}
let count = u16::from_le_bytes([bytes[6], bytes[7]]) as usize;
if count == 0 || count > format.max_entries {
return Err(Error::BadEntryCount(count));
}
let entries = &bytes[HEADER_LEN..split];
let mut rest = entries;
for _ in 0..count {
let (_, next) = split_entry(rest)?;
rest = next;
}
if !rest.is_empty() {
return Err(Error::Truncated);
}
let bundle = Bundle { entries, count };
check_entries(bundle.iter())?;
Ok(bundle)
}
pub fn count(&self) -> usize {
self.count
}
pub fn iter(&self) -> Iter<'a> {
Iter {
rest: self.entries,
left: self.count,
}
}
pub fn kernel(&self) -> Option<Entry<'a>> {
self.iter().find(|entry| entry.role == Role::Kernel)
}
}
fn check_entries<'a, I>(entries: I) -> Result<(), Error>
where
I: Iterator<Item = Entry<'a>> + Clone,
{
for entry in entries.clone() {
check_path(entry.path)?;
}
check_unique_paths(entries.clone())?;
check_single(entries.clone(), Role::Kernel)?;
check_single(entries.clone(), Role::Config)?;
check_firmware_pairs(entries)
}
fn check_unique_paths<'a, I>(entries: I) -> Result<(), Error>
where
I: Iterator<Item = Entry<'a>> + Clone,
{
for (index, entry) in entries.clone().enumerate() {
if entries
.clone()
.skip(index + 1)
.any(|other| other.path == entry.path)
{
return Err(Error::DuplicatePath);
}
}
Ok(())
}
fn check_single<'a, I>(entries: I, role: Role) -> Result<(), Error>
where
I: Iterator<Item = Entry<'a>>,
{
if entries.filter(|entry| entry.role == role).count() > 1 {
return Err(Error::DuplicateRole(role));
}
Ok(())
}
fn check_firmware_pairs<'a, I>(entries: I) -> Result<(), Error>
where
I: Iterator<Item = Entry<'a>> + Clone,
{
for entry in entries.clone().filter(|entry| entry.role == Role::Firmware) {
let name = basename(entry.path);
let paired = if let Some(suffix) = strip_around(name, "start", ".elf") {
has_firmware(entries.clone(), "fixup", suffix, ".dat")
} else if let Some(suffix) = strip_around(name, "fixup", ".dat") {
has_firmware(entries.clone(), "start", suffix, ".elf")
} else {
true
};
if !paired {
return Err(Error::UnpairedFirmware);
}
}
Ok(())
}
fn has_firmware<'a, I>(entries: I, prefix: &str, middle: &str, extension: &str) -> bool
where
I: Iterator<Item = Entry<'a>>,
{
entries
.filter(|entry| entry.role == Role::Firmware)
.any(|entry| {
strip_around(basename(entry.path), prefix, extension)
.is_some_and(|found| found.eq_ignore_ascii_case(middle))
})
}
#[cfg(feature = "alloc")]
pub fn encode(format: &Format, entries: &[Entry<'_>]) -> Result<alloc::vec::Vec<u8>, Error> {
if entries.is_empty() || entries.len() > format.max_entries || entries.len() > u16::MAX as usize
{
return Err(Error::BadEntryCount(entries.len()));
}
check_entries(entries.iter().copied())?;
let mut size = HEADER_LEN + TRAILER_LEN;
for entry in entries {
if entry.path.len() > u8::MAX as usize {
return Err(Error::PathTooLong);
}
if entry.data.len() as u64 > u32::MAX as u64 {
return Err(Error::DataTooLarge);
}
size += 1 + 1 + entry.path.len() + 4 + entry.data.len();
}
let mut out = alloc::vec::Vec::with_capacity(size);
out.extend_from_slice(&format.magic);
out.push(VERSION);
out.push(0);
out.extend_from_slice(&(entries.len() as u16).to_le_bytes());
for entry in entries {
out.push(entry.role.as_byte());
out.push(entry.path.len() as u8);
out.extend_from_slice(entry.path.as_bytes());
out.extend_from_slice(&(entry.data.len() as u32).to_le_bytes());
out.extend_from_slice(entry.data);
}
out.extend_from_slice(&checksum(&out).to_le_bytes());
Ok(out)
}
#[derive(Clone, Debug)]
pub struct Iter<'a> {
rest: &'a [u8],
left: usize,
}
impl<'a> Iterator for Iter<'a> {
type Item = Entry<'a>;
fn next(&mut self) -> Option<Entry<'a>> {
if self.left == 0 {
return None;
}
let (entry, rest) = split_entry(self.rest).ok()?;
self.rest = rest;
self.left -= 1;
Some(entry)
}
fn size_hint(&self) -> (usize, Option<usize>) {
(0, Some(self.left))
}
}
fn split_entry(rest: &[u8]) -> Result<(Entry<'_>, &[u8]), Error> {
let (&role, rest) = rest.split_first().ok_or(Error::Truncated)?;
let role = Role::from_byte(role).ok_or(Error::BadRole(role))?;
let (&path_len, rest) = rest.split_first().ok_or(Error::Truncated)?;
let (path, rest) = rest
.split_at_checked(path_len as usize)
.ok_or(Error::Truncated)?;
let path = core::str::from_utf8(path).map_err(|_| Error::PathNotUtf8)?;
let (size, rest) = rest.split_at_checked(4).ok_or(Error::Truncated)?;
let size = u32::from_le_bytes([size[0], size[1], size[2], size[3]]) as usize;
let (data, rest) = rest.split_at_checked(size).ok_or(Error::Truncated)?;
Ok((Entry { role, path, data }, rest))
}
fn check_path(path: &str) -> Result<(), Error> {
if path.is_empty() {
return Err(Error::BadPath);
}
if path.starts_with('/') {
return Err(Error::BadPath);
}
if path.contains('\\') {
return Err(Error::BadPath);
}
for component in path.split('/') {
if component.is_empty() || component == "." || component == ".." {
return Err(Error::BadPath);
}
}
Ok(())
}
fn basename(path: &str) -> &str {
path.rsplit_once('/').map_or(path, |(_, name)| name)
}
fn strip_around<'a>(name: &'a str, prefix: &str, suffix: &str) -> Option<&'a str> {
if name.len() < prefix.len() + suffix.len() {
return None;
}
let (head, rest) = name.split_at_checked(prefix.len())?;
if !head.eq_ignore_ascii_case(prefix) {
return None;
}
let (middle, tail) = rest.split_at_checked(rest.len() - suffix.len())?;
if !tail.eq_ignore_ascii_case(suffix) {
return None;
}
Some(middle)
}
#[cfg(test)]
mod tests {
use super::*;
const FORMAT: Format = Format {
magic: *b"TEST",
max_entries: 8,
};
struct Builder {
magic: [u8; 4],
version: u8,
count: Option<u16>,
entries: Vec<(u8, Vec<u8>, Vec<u8>)>,
trailing: Vec<u8>,
corrupt: bool,
}
impl Builder {
fn new() -> Self {
Builder {
magic: *b"TEST",
version: VERSION,
count: None,
entries: Vec::new(),
trailing: Vec::new(),
corrupt: false,
}
}
fn entry(mut self, role: u8, path: &str, data: &[u8]) -> Self {
self.entries
.push((role, path.as_bytes().to_vec(), data.to_vec()));
self
}
fn raw_path_entry(mut self, role: u8, path: &[u8], data: &[u8]) -> Self {
self.entries.push((role, path.to_vec(), data.to_vec()));
self
}
fn build(self) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&self.magic);
out.push(self.version);
out.push(0);
let count = self.count.unwrap_or(self.entries.len() as u16);
out.extend_from_slice(&count.to_le_bytes());
for (role, path, data) in &self.entries {
out.push(*role);
out.push(path.len() as u8);
out.extend_from_slice(path);
out.extend_from_slice(&(data.len() as u32).to_le_bytes());
out.extend_from_slice(data);
}
out.extend_from_slice(&self.trailing);
let crc = checksum(&out) ^ if self.corrupt { 1 } else { 0 };
out.extend_from_slice(&crc.to_le_bytes());
out
}
}
fn one_file() -> Vec<u8> {
Builder::new().entry(0, "WWW/INDEX.HTM", b"hello").build()
}
fn parse(bytes: &[u8]) -> Result<Bundle<'_>, Error> {
Bundle::parse(&FORMAT, bytes)
}
fn error(bytes: &[u8]) -> Error {
parse(bytes).expect_err("expected this bundle to be rejected")
}
#[test]
fn checksum_matches_the_standard_vector() {
assert_eq!(checksum(b"123456789"), 0xCBF4_3926);
}
#[test]
fn a_well_formed_bundle_parses() {
let bytes = Builder::new()
.entry(1, "KERNEL7.IMG", b"kernel bytes")
.entry(0, "WWW/INDEX.HTM", b"<html>")
.entry(3, "CONFIG.TXT", b"arm_64bit=0\n")
.build();
let bundle = parse(&bytes).unwrap();
assert_eq!(bundle.count(), 3);
let entries: Vec<_> = bundle.iter().collect();
assert_eq!(entries.len(), 3);
assert_eq!(entries[0].role, Role::Kernel);
assert_eq!(entries[0].path, "KERNEL7.IMG");
assert_eq!(entries[0].data, b"kernel bytes");
assert_eq!(entries[1].role, Role::File);
assert_eq!(entries[1].path, "WWW/INDEX.HTM");
assert_eq!(entries[2].role, Role::Config);
assert_eq!(bundle.kernel().unwrap().data, b"kernel bytes");
}
#[test]
fn a_bundle_with_no_kernel_is_valid() {
let bytes = Builder::new().entry(0, "WATER.CFG", b"zone=1").build();
let bundle = parse(&bytes).unwrap();
assert_eq!(bundle.count(), 1);
assert!(bundle.kernel().is_none());
}
#[test]
fn an_empty_file_is_valid() {
let bytes = Builder::new().entry(0, "EMPTY.TXT", b"").build();
assert_eq!(parse(&bytes).unwrap().iter().next().unwrap().data, b"");
}
#[test]
fn too_short() {
assert_eq!(error(&[]), Error::TooShort);
assert_eq!(error(&one_file()[..MIN_LEN - 1]), Error::TooShort);
}
#[test]
fn bad_magic() {
let mut bytes = one_file();
bytes[0] = b'X';
assert_eq!(error(&bytes), Error::BadMagic);
}
#[test]
fn bad_version() {
let mut builder = Builder::new().entry(0, "A.TXT", b"a");
builder.version = 1;
assert_eq!(error(&builder.build()), Error::BadVersion(1));
}
#[test]
fn bad_checksum() {
let mut bytes = one_file();
let last = bytes.len() - TRAILER_LEN - 1;
bytes[last] ^= 0xFF;
assert_eq!(error(&bytes), Error::BadChecksum);
}
#[test]
fn the_checksum_is_checked_before_the_entries() {
let mut builder = Builder::new().entry(0, "A.TXT", b"a");
builder.count = Some(0);
builder.corrupt = true;
assert_eq!(error(&builder.build()), Error::BadChecksum);
}
#[test]
fn bad_entry_count() {
let mut builder = Builder::new().entry(0, "A.TXT", b"a");
builder.count = Some(0);
assert_eq!(error(&builder.build()), Error::BadEntryCount(0));
let mut builder = Builder::new();
for index in 0..9 {
builder = builder.entry(0, &format!("F{index}.TXT"), b"x");
}
assert_eq!(error(&builder.build()), Error::BadEntryCount(9));
}
#[test]
fn an_entry_running_past_the_end_is_truncated() {
let mut builder = Builder::new().entry(0, "A.TXT", b"abc");
builder.count = Some(2);
assert_eq!(error(&builder.build()), Error::Truncated);
}
#[test]
fn slack_between_the_last_entry_and_the_checksum_is_truncated() {
let mut builder = Builder::new().entry(0, "A.TXT", b"abc");
builder.trailing = vec![0; 3];
assert_eq!(error(&builder.build()), Error::Truncated);
}
#[test]
fn bad_role() {
let bytes = Builder::new().entry(4, "A.TXT", b"a").build();
assert_eq!(error(&bytes), Error::BadRole(4));
}
#[test]
fn path_not_utf8() {
let bytes = Builder::new()
.raw_path_entry(0, &[0xFF, 0xFE], b"a")
.build();
assert_eq!(error(&bytes), Error::PathNotUtf8);
}
#[test]
fn unsafe_paths() {
for path in [
"",
"/ABS.TXT",
"../ESCAPE.TXT",
"WWW/../ESCAPE.TXT",
"./HERE.TXT",
"WWW//DOUBLE.TXT",
"TRAILING/",
"WWW\\INDEX.HTM",
] {
let bytes = Builder::new().entry(0, path, b"x").build();
assert_eq!(error(&bytes), Error::BadPath, "path {path:?} was accepted");
}
}
#[test]
fn nested_paths_are_fine() {
let bytes = Builder::new().entry(0, "CERTS/ROOTS/CA.PEM", b"x").build();
assert_eq!(parse(&bytes).unwrap().count(), 1);
}
#[test]
fn duplicate_path() {
let bytes = Builder::new()
.entry(0, "WWW/INDEX.HTM", b"one")
.entry(0, "WWW/INDEX.HTM", b"two")
.build();
assert_eq!(error(&bytes), Error::DuplicatePath);
}
#[test]
fn two_kernels() {
let bytes = Builder::new()
.entry(1, "KERNEL7.IMG", b"a")
.entry(1, "KERNEL8.IMG", b"b")
.build();
assert_eq!(error(&bytes), Error::DuplicateRole(Role::Kernel));
}
#[test]
fn two_configs() {
let bytes = Builder::new()
.entry(3, "CONFIG.TXT", b"a")
.entry(3, "BOOT/CONFIG.TXT", b"b")
.build();
assert_eq!(error(&bytes), Error::DuplicateRole(Role::Config));
}
#[test]
fn a_start_without_its_fixup_is_unpaired() {
let bytes = Builder::new().entry(2, "START.ELF", b"a").build();
assert_eq!(error(&bytes), Error::UnpairedFirmware);
}
#[test]
fn a_fixup_without_its_start_is_unpaired() {
let bytes = Builder::new().entry(2, "FIXUP.DAT", b"a").build();
assert_eq!(error(&bytes), Error::UnpairedFirmware);
}
#[test]
fn a_pair_with_mismatched_suffixes_is_unpaired() {
let bytes = Builder::new()
.entry(2, "START4.ELF", b"a")
.entry(2, "FIXUP.DAT", b"b")
.build();
assert_eq!(error(&bytes), Error::UnpairedFirmware);
}
#[test]
fn a_matched_pair_is_accepted_whatever_the_case_and_suffix() {
for (start, fixup) in [
("START.ELF", "FIXUP.DAT"),
("start.elf", "fixup.dat"),
("Start4.Elf", "fixup4.DAT"),
("BOOT/start_x.elf", "FIXUP_X.DAT"),
] {
let bytes = Builder::new()
.entry(2, start, b"a")
.entry(2, fixup, b"b")
.build();
assert!(parse(&bytes).is_ok(), "{start} + {fixup} was rejected");
}
}
#[test]
fn firmware_with_no_partner_needs_none() {
let bytes = Builder::new().entry(2, "BOOTCODE.BIN", b"a").build();
assert_eq!(parse(&bytes).unwrap().count(), 1);
}
#[test]
fn every_rejection_is_the_senders_fault() {
assert_eq!(Error::TooShort.http_status(), 400);
assert_eq!(Error::BadChecksum.http_status(), 400);
assert_eq!(Error::UnpairedFirmware.http_status(), 400);
}
#[test]
fn roles_round_trip_through_their_wire_bytes() {
for role in [Role::File, Role::Kernel, Role::Firmware, Role::Config] {
assert_eq!(Role::from_byte(role.as_byte()), Some(role));
}
assert_eq!(Role::from_byte(4), None);
}
const GOLDEN_ONE_FILE: &[u8] = &[
b'T', b'E', b'S', b'T', 2, 0, 1, 0, 0, 5, b'A', b'.', b'T', b'X', b'T', 3, 0, 0, 0, b'a', b'b', b'c', 0xE9, 0xB1, 0x6D, 0xB9, ];
const GOLDEN_FULL: &[u8] = &[
b'T', b'E', b'S', b'T', 2, 0, 3, 0, 1, 11, b'K', b'E', b'R', b'N', b'E', b'L', b'7', b'.', b'I', b'M', b'G', 4, 0, 0,
0, b'k', b'r', b'n', b'l', 0, 13, 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,
0, b'<', b'h', b't', b'm', b'l', b'>', 3, 10, b'C', b'O', b'N', b'F', b'I', b'G', b'.', b'T', b'X', b'T', 12, 0, 0, 0, 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,
0xA1, ];
const GOLDEN_FIRMWARE: &[u8] = &[
b'T', b'E', b'S', b'T', 2, 0, 2, 0, 2, 9, b'S', b'T', b'A', b'R', b'T', b'.', b'E', b'L', b'F', 2, 0, 0, 0, b's', b'e', 2, 9, b'F', b'I', b'X', b'U', b'P', b'.', b'D', b'A', b'T', 2, 0, 0, 0, b'f', b'd', 0x4F, 0xA4, 0x7B, 0xD1, ];
fn check_golden(bytes: &[u8], entries: &[Entry<'_>]) {
let bundle = parse(bytes).expect("the golden vector should parse");
assert_eq!(bundle.iter().collect::<Vec<_>>(), entries);
assert_eq!(encode(&FORMAT, entries).unwrap(), bytes);
}
#[test]
fn golden_one_file() {
check_golden(
GOLDEN_ONE_FILE,
&[Entry {
role: Role::File,
path: "A.TXT",
data: b"abc",
}],
);
}
#[test]
fn golden_full() {
check_golden(
GOLDEN_FULL,
&[
Entry {
role: Role::Kernel,
path: "KERNEL7.IMG",
data: b"krnl",
},
Entry {
role: Role::File,
path: "WWW/INDEX.HTM",
data: b"<html>",
},
Entry {
role: Role::Config,
path: "CONFIG.TXT",
data: b"arm_64bit=0\n",
},
],
);
}
#[test]
fn golden_firmware() {
check_golden(
GOLDEN_FIRMWARE,
&[
Entry {
role: Role::Firmware,
path: "START.ELF",
data: b"se",
},
Entry {
role: Role::Firmware,
path: "FIXUP.DAT",
data: b"fd",
},
],
);
}
#[test]
fn what_is_encoded_parses_back() {
let big = vec![0xA5; 100_000];
let entries = [
Entry {
role: Role::Kernel,
path: "KERNEL8.IMG",
data: &big,
},
Entry {
role: Role::File,
path: "CERTS/ROOTS/CA.PEM",
data: b"-----BEGIN",
},
Entry {
role: Role::File,
path: "EMPTY.BIN",
data: b"",
},
];
let bytes = encode(&FORMAT, &entries).unwrap();
let bundle = parse(&bytes).unwrap();
assert_eq!(bundle.iter().collect::<Vec<_>>(), entries);
assert_eq!(bundle.kernel().unwrap().data.len(), 100_000);
}
#[test]
fn encoding_reserves_exactly_what_it_writes() {
let entries = [Entry {
role: Role::File,
path: "A.TXT",
data: b"abc",
}];
let bytes = encode(&FORMAT, &entries).unwrap();
assert_eq!(bytes.len(), bytes.capacity());
}
#[test]
fn another_applications_bundle_is_rejected() {
let entries = [Entry {
role: Role::File,
path: "A.TXT",
data: b"abc",
}];
let bytes = encode(
&Format {
magic: *b"OTHR",
max_entries: 8,
},
&entries,
)
.unwrap();
assert_eq!(error(&bytes), Error::BadMagic);
}
#[test]
fn encoding_applies_every_rule_the_parser_would() {
let file = |path| Entry {
role: Role::File,
path,
data: b"x" as &[u8],
};
assert_eq!(
encode(&FORMAT, &[]).unwrap_err(),
Error::BadEntryCount(0),
"an empty bundle"
);
let many: Vec<_> = ["A", "B", "C", "D", "E", "F", "G", "H", "I"]
.iter()
.map(|path| file(path))
.collect();
assert_eq!(
encode(&FORMAT, &many).unwrap_err(),
Error::BadEntryCount(9),
"more entries than the format allows"
);
assert_eq!(
encode(&FORMAT, &[file("../ESCAPE.TXT")]).unwrap_err(),
Error::BadPath
);
assert_eq!(
encode(&FORMAT, &[file("A.TXT"), file("A.TXT")]).unwrap_err(),
Error::DuplicatePath
);
assert_eq!(
encode(
&FORMAT,
&[
Entry {
role: Role::Kernel,
path: "KERNEL7.IMG",
data: b"a"
},
Entry {
role: Role::Kernel,
path: "KERNEL8.IMG",
data: b"b"
},
]
)
.unwrap_err(),
Error::DuplicateRole(Role::Kernel)
);
assert_eq!(
encode(
&FORMAT,
&[Entry {
role: Role::Firmware,
path: "START.ELF",
data: b"a"
}]
)
.unwrap_err(),
Error::UnpairedFirmware
);
}
#[test]
fn a_path_longer_than_its_length_field() {
let long = "A".repeat(256);
assert_eq!(
encode(
&FORMAT,
&[Entry {
role: Role::File,
path: &long,
data: b"x"
}]
)
.unwrap_err(),
Error::PathTooLong
);
let limit = "A".repeat(255);
assert!(
encode(
&FORMAT,
&[Entry {
role: Role::File,
path: &limit,
data: b"x"
}]
)
.is_ok()
);
}
}