bose_dfu/
dfu_file.rs

1use byteorder::{ByteOrder, BE};
2use log::warn;
3use std::fmt::{Display, LowerHex, Write};
4use std::io::{Read, Seek, SeekFrom};
5use thiserror::Error;
6
7/// Parse the suffix of a DFU file and calculate the data's real checksum, storing the results in a
8/// [SuffixInfo] struct. When this returns, `file`'s cursor is at the beginning of the payload.
9pub fn parse(file: &mut (impl Read + Seek)) -> Result<SuffixInfo, Error> {
10    const MIN_SUFFIX_LEN: u8 = 0x10;
11    const MIN_DFU_BCD: u16 = 0x0100;
12
13    let file_len = file.seek(SeekFrom::End(0))?;
14    if file_len < MIN_SUFFIX_LEN as _ {
15        return Err(SuffixError::FileTooShort {
16            minimum: MIN_SUFFIX_LEN as u64,
17        }
18        .into());
19    }
20
21    let mut suffix = [0u8; MIN_SUFFIX_LEN as usize];
22    file.seek(SeekFrom::End(-(MIN_SUFFIX_LEN as i64)))?;
23    file.read_exact(&mut suffix)?;
24    suffix.reverse(); // Entire suffix is byte-swapped
25
26    if &suffix[5..=7] != b"DFU" {
27        return Err(SuffixError::BadSignature.into());
28    }
29
30    let suffix_len = suffix[4];
31    #[allow(clippy::comparison_chain)]
32    if suffix_len < MIN_SUFFIX_LEN {
33        return Err(SuffixError::SuffixTooShort {
34            minimum: MIN_SUFFIX_LEN as _,
35            actual: suffix_len,
36        }
37        .into());
38    } else if suffix_len > MIN_SUFFIX_LEN {
39        warn!(
40            "Got {} extra bytes in DFU suffix; continuing",
41            suffix_len - MIN_SUFFIX_LEN
42        );
43    }
44
45    let payload_length = match file_len.checked_sub(suffix_len as _) {
46        Some(i) => i,
47        None => {
48            return Err(SuffixError::SuffixTooLong {
49                suffix_len,
50                file_len,
51            }
52            .into())
53        }
54    };
55
56    let bcd_dfu = BE::read_u16(&suffix[8..10]);
57    if bcd_dfu < MIN_DFU_BCD {
58        return Err(SuffixError::TooOld {
59            minimum: MIN_DFU_BCD,
60            actual: bcd_dfu,
61        }
62        .into());
63    }
64
65    // CRC is over all but the last 4 bytes, which hold the expected CRC.
66    file.seek(SeekFrom::Start(0))?;
67    let actual_crc = compute_crc(&mut file.take(file_len - 4))?;
68    let expected_crc = BE::read_u32(&suffix[0..4]);
69
70    // Reset cursor so caller can read the file's data.
71    file.seek(SeekFrom::Start(0))?;
72
73    Ok(SuffixInfo {
74        vendor_id: BE::read_u16(&suffix[10..12]).into(),
75        product_id: BE::read_u16(&suffix[12..14]).into(),
76        release_number: BE::read_u16(&suffix[14..16]).into(),
77        expected_crc,
78        actual_crc,
79        payload_length,
80    })
81}
82
83/// Compute the CRC used by USB DFU 1.1 over all bytes in the given file. Does not strip CRC field
84/// from suffix automatically.
85fn compute_crc(file: &mut impl Read) -> std::io::Result<u32> {
86    const CHUNK_SIZE: usize = 4096; // Chosen fairly arbitrarily
87
88    let mut hasher = crc32fast::Hasher::new();
89    let mut buf = [0u8; CHUNK_SIZE];
90    loop {
91        let len = file.read(&mut buf)?;
92        if len == 0 {
93            break;
94        }
95        hasher.update(&buf[0..len]);
96    }
97    Ok(!hasher.finalize()) // DFU's CRC algorithm is a bitwise NOT of IEEE's.
98}
99
100/// Metadata about a file containing a DFU suffix.
101#[derive(Debug)]
102pub struct SuffixInfo {
103    pub vendor_id: OptionalId,
104    pub product_id: OptionalId,
105    pub release_number: OptionalId,
106    pub expected_crc: u32,
107    pub actual_crc: u32,
108    pub payload_length: u64,
109}
110
111impl SuffixInfo {
112    pub fn has_valid_crc(&self) -> bool {
113        self.actual_crc == self.expected_crc
114    }
115
116    pub fn ensure_valid_crc(&self) -> Result<(), SuffixError> {
117        match self.has_valid_crc() {
118            true => Ok(()),
119            false => Err(SuffixError::BadCRC {
120                expected: self.expected_crc,
121                actual: self.actual_crc,
122            }),
123        }
124    }
125}
126
127/// A 16-bit ID that may be unset. Has functions for pretty-printing and wildcard matching.
128#[derive(Debug)]
129pub struct OptionalId(pub Option<u16>);
130
131impl OptionalId {
132    pub fn matches(&self, cmp: u16) -> bool {
133        match self.0 {
134            None => true,
135            Some(id) => id == cmp,
136        }
137    }
138
139    fn fmt_helper<F>(&self, f: &mut std::fmt::Formatter, delegate: F) -> std::fmt::Result
140    where
141        F: FnOnce(&u16, &mut std::fmt::Formatter) -> std::fmt::Result,
142    {
143        match self.0 {
144            Some(id) => delegate(&id, f),
145            None => {
146                for _ in 0..f.width().unwrap_or(3) {
147                    f.write_char('?')?
148                }
149                Ok(())
150            }
151        }
152    }
153}
154
155impl Display for OptionalId {
156    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
157        self.fmt_helper(f, Display::fmt)
158    }
159}
160
161impl LowerHex for OptionalId {
162    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
163        self.fmt_helper(f, LowerHex::fmt)
164    }
165}
166
167/// Convert from an ID field in a DFU suffix.
168impl From<u16> for OptionalId {
169    fn from(val: u16) -> Self {
170        OptionalId(match val {
171            0xffff => None,
172            i => Some(i),
173        })
174    }
175}
176
177/// All errors (parse and I/O) that can happen while reading a DFU file.
178#[derive(Error, Debug)]
179#[non_exhaustive]
180pub enum Error {
181    #[error("invalid firmware file")]
182    SuffixError(#[from] SuffixError),
183
184    #[error("I/O error")]
185    IoError(#[from] std::io::Error),
186}
187
188/// Parse errors for a DFU suffix.
189#[derive(Error, Debug)]
190#[non_exhaustive]
191pub enum SuffixError {
192    #[error("DFU signature is not present; are you sure this is a DFU file?")]
193    BadSignature,
194
195    #[error(
196        "DFU specification version is too old: expected at least {}.{}, got {}.{}",
197        .minimum >> 8, .minimum & 0xff,
198        .actual >> 8, .actual & 0xff,
199    )]
200    TooOld { minimum: u16, actual: u16 },
201
202    #[error("file is shorter than DFU suffix: expected at least {minimum} bytes")]
203    FileTooShort { minimum: u64 },
204
205    #[error("DFU suffix is shorter than allowed: expected at least {minimum} bytes, got {actual}")]
206    SuffixTooShort { minimum: u8, actual: u8 },
207
208    #[error("DFU suffix is longer than file: suffix is {suffix_len} bytes, file is {file_len}")]
209    SuffixTooLong { suffix_len: u8, file_len: u64 },
210
211    #[error("bad CRC32 checksum: expected {expected:#010x}, got {actual:#010x}")]
212    BadCRC { expected: u32, actual: u32 },
213}