1use byteorder::{ByteOrder, BE};
2use log::warn;
3use std::fmt::{Display, LowerHex, Write};
4use std::io::{Read, Seek, SeekFrom};
5use thiserror::Error;
6
7pub 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(); 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 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 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
83fn compute_crc(file: &mut impl Read) -> std::io::Result<u32> {
86 const CHUNK_SIZE: usize = 4096; 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()) }
99
100#[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#[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
167impl 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#[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#[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}