use byteorder::{ByteOrder, BE};
use log::warn;
use std::fmt::{Display, LowerHex, Write};
use std::io::{Read, Seek, SeekFrom};
use thiserror::Error;
pub fn parse(file: &mut (impl Read + Seek)) -> Result<SuffixInfo, Error> {
const MIN_SUFFIX_LEN: u8 = 0x10;
const MIN_DFU_BCD: u16 = 0x0100;
let file_len = file.seek(SeekFrom::End(0))?;
if file_len < MIN_SUFFIX_LEN as _ {
return Err(SuffixError::FileTooShort {
minimum: MIN_SUFFIX_LEN as u64,
}
.into());
}
let mut suffix = [0u8; MIN_SUFFIX_LEN as usize];
file.seek(SeekFrom::End(-(MIN_SUFFIX_LEN as i64)))?;
file.read_exact(&mut suffix)?;
suffix.reverse();
if &suffix[5..=7] != b"DFU" {
return Err(SuffixError::BadSignature.into());
}
let suffix_len = suffix[4];
#[allow(clippy::comparison_chain)]
if suffix_len < MIN_SUFFIX_LEN {
return Err(SuffixError::SuffixTooShort {
minimum: MIN_SUFFIX_LEN as _,
actual: suffix_len,
}
.into());
} else if suffix_len > MIN_SUFFIX_LEN {
warn!(
"Got {} extra bytes in DFU suffix; continuing",
suffix_len - MIN_SUFFIX_LEN
);
}
let payload_length = match file_len.checked_sub(suffix_len as _) {
Some(i) => i,
None => {
return Err(SuffixError::SuffixTooLong {
suffix_len,
file_len,
}
.into())
}
};
let bcd_dfu = BE::read_u16(&suffix[8..10]);
if bcd_dfu < MIN_DFU_BCD {
return Err(SuffixError::TooOld {
minimum: MIN_DFU_BCD,
actual: bcd_dfu,
}
.into());
}
file.seek(SeekFrom::Start(0))?;
let actual_crc = compute_crc(&mut file.take(file_len - 4))?;
let expected_crc = BE::read_u32(&suffix[0..4]);
file.seek(SeekFrom::Start(0))?;
Ok(SuffixInfo {
vendor_id: BE::read_u16(&suffix[10..12]).into(),
product_id: BE::read_u16(&suffix[12..14]).into(),
release_number: BE::read_u16(&suffix[14..16]).into(),
expected_crc,
actual_crc,
payload_length,
})
}
fn compute_crc(file: &mut impl Read) -> std::io::Result<u32> {
const CHUNK_SIZE: usize = 4096;
let mut hasher = crc32fast::Hasher::new();
let mut buf = [0u8; CHUNK_SIZE];
loop {
let len = file.read(&mut buf)?;
if len == 0 {
break;
}
hasher.update(&buf[0..len]);
}
Ok(!hasher.finalize()) }
#[derive(Debug)]
pub struct SuffixInfo {
pub vendor_id: OptionalId,
pub product_id: OptionalId,
pub release_number: OptionalId,
pub expected_crc: u32,
pub actual_crc: u32,
pub payload_length: u64,
}
impl SuffixInfo {
pub fn has_valid_crc(&self) -> bool {
self.actual_crc == self.expected_crc
}
pub fn ensure_valid_crc(&self) -> Result<(), SuffixError> {
match self.has_valid_crc() {
true => Ok(()),
false => Err(SuffixError::BadCRC {
expected: self.expected_crc,
actual: self.actual_crc,
}),
}
}
}
#[derive(Debug)]
pub struct OptionalId(pub Option<u16>);
impl OptionalId {
pub fn matches(&self, cmp: u16) -> bool {
match self.0 {
None => true,
Some(id) => id == cmp,
}
}
fn fmt_helper<F>(&self, f: &mut std::fmt::Formatter, delegate: F) -> std::fmt::Result
where
F: FnOnce(&u16, &mut std::fmt::Formatter) -> std::fmt::Result,
{
match self.0 {
Some(id) => delegate(&id, f),
None => {
for _ in 0..f.width().unwrap_or(3) {
f.write_char('?')?
}
Ok(())
}
}
}
}
impl Display for OptionalId {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
self.fmt_helper(f, Display::fmt)
}
}
impl LowerHex for OptionalId {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
self.fmt_helper(f, LowerHex::fmt)
}
}
impl From<u16> for OptionalId {
fn from(val: u16) -> Self {
OptionalId(match val {
0xffff => None,
i => Some(i),
})
}
}
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum Error {
#[error("invalid firmware file")]
SuffixError(#[from] SuffixError),
#[error("I/O error")]
IoError(#[from] std::io::Error),
}
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum SuffixError {
#[error("DFU signature is not present; are you sure this is a DFU file?")]
BadSignature,
#[error(
"DFU specification version is too old: expected at least {}.{}, got {}.{}",
.minimum >> 8, .minimum & 0xff,
.actual >> 8, .actual & 0xff,
)]
TooOld { minimum: u16, actual: u16 },
#[error("file is shorter than DFU suffix: expected at least {minimum} bytes")]
FileTooShort { minimum: u64 },
#[error("DFU suffix is shorter than allowed: expected at least {minimum} bytes, got {actual}")]
SuffixTooShort { minimum: u8, actual: u8 },
#[error("DFU suffix is longer than file: suffix is {suffix_len} bytes, file is {file_len}")]
SuffixTooLong { suffix_len: u8, file_len: u64 },
#[error("bad CRC32 checksum: expected {expected:#010x}, got {actual:#010x}")]
BadCRC { expected: u32, actual: u32 },
}