libbmfw/
lib.rs

1use binrw::{binrw, helpers::until_eof};
2use flate2::read::ZlibDecoder;
3use std::io::SeekFrom;
4
5mod checksum;
6mod error;
7
8pub use crate::{
9    checksum::{FirmwareChecksum, SECRET_VALUE},
10    error::Error,
11};
12pub type Result<T> = std::result::Result<T, Error>;
13
14/// ATEM firmware image file
15///
16/// ## File format
17///
18/// * `0x20` bytes: firmware image checksum
19/// * repeated until eof: [Resource]
20///
21/// ## Checksum
22///
23/// The image checksum is SHA-256 of the rest of the file, but has been
24/// initialised with a "secret value":
25/// `e6141730dd0a0c465941255d110f030545504239`.
26///
27/// ## Firmware location
28///
29/// Firmware files named `data-[0-9a-f]{4}.bin`.
30///
31/// They can be found in the `AdminUtility/PlugIns/{product_name}/Resources/`
32/// subfolder of the application's install path:
33///
34/// * macOS: `/Library/Application Supports/Blackmagic Design/*/`
35/// * Windows: `Program Files/Blackmagic Design/*/`
36#[binrw]
37#[derive(Debug, Default, Clone)]
38#[brw(big, stream = r, map_stream = FirmwareChecksum::new)]
39pub struct FirmwareFile {
40    #[brw(pad_before = 32)]
41    #[br(parse_with = until_eof)]
42    pub resources: Vec<Resource>,
43
44    #[brw(seek_before = SeekFrom::Start(0))]
45    #[br(temp, assert(checksum == r.check(), "bad checksum: {:x?} != {:x?}", checksum, r.check()))]
46    #[bw(calc(r.check()))]
47    pub checksum: [u8; 0x20],
48}
49
50/// Firmware resource
51///
52/// ## Data format
53///
54/// Resource headers are at minimum 24 bytes:
55///
56/// * `u16`: magic value: `0xBDBD`
57/// * `u16`: format version? always 1
58/// * `u32`: ?
59/// * `u32`: total resource size in bytes, including headers
60/// * `u16`: header length
61/// * `bool`: compression enabled (zlib)
62/// * `u8`: type
63/// * `u32`: unpacked length
64/// * `u32`: maybe CRC?
65/// * `header_length - 24` bytes: additional headers
66///
67/// The payload (`resource_size - header_length`) follows.
68#[binrw]
69#[derive(Default, PartialEq, Clone)]
70#[br(assert(usize::from(header_length) >= Self::MIN_LENGTH))]
71#[brw(big, magic = 0xBDBD0001u32)]
72pub struct Resource {
73    unknown1: u32,
74    #[bw(try_calc(u32::try_from(payload.len() + header.len() + Self::MIN_LENGTH)))]
75    length: u32,
76    #[bw(try_calc(u16::try_from(header.len() + Self::MIN_LENGTH)))]
77    header_length: u16,
78
79    /// Compression enabled; 1 = zlib
80    pub compression: u8,
81    pub typ: u8,
82    unpacked_length: u32,
83    unknown7: u32,
84
85    #[br(count = header_length - 24)]
86    pub header: Vec<u8>,
87
88    #[br(count = length - u32::from(header_length))]
89    pub payload: Vec<u8>,
90}
91
92impl Resource {
93    pub const MIN_LENGTH: usize = 24;
94
95    pub fn decompress_payload(&self) -> ZlibDecoder<&[u8]> {
96        ZlibDecoder::new(self.payload.as_ref())
97    }
98}
99
100impl std::fmt::Debug for Resource {
101    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102        f.debug_struct("Resource")
103            .field("unknown1", &self.unknown1)
104            .field("compression", &self.compression)
105            .field("typ", &self.typ)
106            .field("unpacked_length", &self.unpacked_length)
107            .field("unknown7", &self.unknown7)
108            .field("header", &hex::encode(&self.header))
109            .field("payload", &format!("{} bytes", self.payload.len()))
110            .finish()
111    }
112}
113
114#[cfg(test)]
115mod test {
116    use super::*;
117    use binrw::{BinRead, BinWrite};
118    use std::io::Cursor;
119
120    #[test]
121    fn firmware_atem_mini() -> Result<()> {
122        let mut cmd = hex::decode("BDBD000100000000007328300030000000732800F0B8512A0001001800000000000000011EDBBE490000000000000000")?;
123        cmd.resize(cmd.len() + 7546880, 0);
124
125        let expected = Resource {
126            unknown1: 0,
127            compression: 0,
128            typ: 0,
129            unpacked_length: 7546880,
130            unknown7: 0xF0B8512A,
131            header: hex::decode("0001001800000000000000011EDBBE490000000000000000")?,
132            payload: vec![0u8; 7546880],
133        };
134        let r = Resource::read(&mut Cursor::new(&cmd))?;
135        assert_eq!(expected, r);
136
137        let mut out = Cursor::new(Vec::with_capacity(cmd.len() + 32));
138        let fw = FirmwareFile {
139            resources: vec![expected],
140        };
141        fw.write(&mut out)?;
142
143        let out = out.into_inner();
144        // Only compare the first 512 bytes, otherwise errors are hard to read
145        assert_eq!(&cmd[..512], &out[32..544]);
146        assert_eq!(
147            hex::decode("05a5716ebe99b9fbbc06b0e9add2f12cc8362f2541ba7ee19b8f5593375ab209")?,
148            &out[..32],
149        );
150
151        // Reading back the firmware should work with that checksum
152        let _ = FirmwareFile::read(&mut Cursor::new(&out))?;
153        Ok(())
154    }
155
156    #[test]
157    fn firmware_camera_control_panel() -> Result<()> {
158        let mut cmd = hex::decode("BDBD00010000000000002F4600300100000058D4337D01C40001001800000000000300011EDBBE0E0000000000000000")?;
159        cmd.resize(cmd.len() + 12054, 0);
160
161        let expected = Resource {
162            unknown1: 0,
163            compression: 1,
164            typ: 0,
165            unpacked_length: 22740,
166            unknown7: 0x337D01C4,
167            header: hex::decode("0001001800000000000300011EDBBE0E0000000000000000")?,
168            payload: vec![0u8; 12054],
169        };
170
171        let r = Resource::read(&mut Cursor::new(&cmd))?;
172        assert_eq!(expected, r);
173
174        let mut out = Cursor::new(Vec::with_capacity(cmd.len() + 32));
175        let fw = FirmwareFile {
176            resources: vec![expected],
177        };
178        fw.write(&mut out)?;
179
180        let out = out.into_inner();
181        // Only compare the first 512 bytes, otherwise errors are hard to read
182        assert_eq!(&cmd[..512], &out[32..544]);
183        assert_eq!(
184            hex::decode("76d7bb1d94be3022d096a844236797e35575f111b0cd1efe3a8ee2d06096cf60")?,
185            &out[..32],
186        );
187
188        // Reading back the firmware should work with that checksum
189        let _ = FirmwareFile::read(&mut Cursor::new(&out))?;
190
191        let mut cmd = hex::decode("BDBD00010000000000006D840030010400040000F2C9BEE80001001800000000FF0000011EDBBE0E0000000000000000")?;
192        cmd.resize(cmd.len() + 27988, 0);
193
194        let expected = Resource {
195            unknown1: 0,
196            compression: 1,
197            typ: 4,
198            unpacked_length: 262144,
199            unknown7: 0xf2c9bee8,
200            header: hex::decode("0001001800000000FF0000011EDBBE0E0000000000000000")?,
201            payload: vec![0u8; 27988],
202        };
203        let r = Resource::read(&mut Cursor::new(&cmd))?;
204        assert_eq!(expected, r);
205
206        let mut out = Cursor::new(Vec::with_capacity(cmd.len() + 32));
207        let fw = FirmwareFile {
208            resources: vec![expected],
209        };
210        fw.write(&mut out)?;
211
212        let out = out.into_inner();
213        // Only compare the first 512 bytes, otherwise errors are hard to read
214        assert_eq!(&cmd[..512], &out[32..544]);
215        assert_eq!(
216            hex::decode("af21c869f1396f1fcb24b382002ac800b00e0d2986e6e80bd899f78d1c5e6078")?,
217            &out[..32],
218        );
219
220        // Reading back the firmware should work with that checksum
221        let _ = FirmwareFile::read(&mut Cursor::new(&out))?;
222        Ok(())
223    }
224}