use crate::Version;
use serde::Deserialize;
use sha2::{Digest, Sha256};
use std::fmt::Write as _;
use std::sync::OnceLock;
const TEXT: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/config/firmware.toml"));
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Manifest {
pub base_url: String,
#[serde(default)]
pub prefix: String,
pub size: u64,
#[serde(default)]
pub image: Vec<Image>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Image {
pub name: String,
pub version: Version,
pub pcb: Option<String>,
pub kind: String,
#[serde(default)]
pub chips: Vec<String>,
pub sha256: String,
}
impl Manifest {
#[must_use]
pub fn path(&self, image: &Image) -> String {
let file: String = image
.name
.chars()
.map(|c| if c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-') { c } else { '_' })
.collect();
let mut key = String::with_capacity(file.len());
for c in file.chars() {
if c == '_' && key.ends_with('_') {
continue;
}
key.push(c);
}
format!("{}/{key}", self.prefix.trim_end_matches('/'))
}
}
impl Image {
pub fn verify(&self, bytes: &[u8]) -> Result<(), String> {
let want = manifest().size;
if bytes.len() as u64 != want {
return Err(format!("{}: size {} bytes, manifest says {want}", self.name, bytes.len()));
}
let got = sha256_hex(bytes);
if got != self.sha256 {
return Err(format!(
"{}: sha256 {got}, manifest says {}",
self.name, self.sha256
));
}
Ok(())
}
}
#[must_use]
pub fn sha256_hex(bytes: &[u8]) -> String {
let digest = Sha256::digest(bytes);
let mut s = String::with_capacity(64);
for b in digest {
let _ = write!(s, "{b:02x}");
}
s
}
fn parse(text: &str) -> Result<Manifest, String> {
let m: Manifest = toml::from_str(text).map_err(|e| format!("config/firmware.toml: {e}"))?;
for (i, img) in m.image.iter().enumerate() {
if img.sha256.len() != 64 || !img.sha256.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f')) {
return Err(format!("config/firmware.toml: {}: sha256 is not 64 lowercase hex digits", img.name));
}
if m.image[..i].iter().any(|o| o.name == img.name) {
return Err(format!("config/firmware.toml: {}: listed twice", img.name));
}
}
Ok(m)
}
pub fn manifest() -> &'static Manifest {
static MANIFEST: OnceLock<Manifest> = OnceLock::new();
MANIFEST.get_or_init(|| parse(TEXT).unwrap_or_else(|e| panic!("{e}")))
}
#[must_use]
pub fn image(name: &str) -> Option<&'static Image> {
manifest().image.iter().find(|i| i.name == name)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
fn repo(rel: &str) -> std::path::PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../..").join(rel)
}
#[test]
fn the_manifest_lists_every_archived_image_and_the_hashes_match() {
let m = manifest();
assert!(m.base_url.is_empty() || m.base_url.starts_with("https://"));
let dir = repo("third-party/firmware");
let Ok(entries) = std::fs::read_dir(&dir) else {
eprintln!("skipped: no local firmware archive at {}", dir.display());
return;
};
let on_disk: Vec<String> = entries
.flatten()
.map(|e| e.file_name().to_string_lossy().into_owned())
.filter(|n| Path::new(n).extension().is_some_and(|x| x == "hex"))
.collect();
for name in &on_disk {
let img = image(name).unwrap_or_else(|| panic!("{name}: not in the manifest"));
let bytes = std::fs::read(dir.join(name)).expect("read image");
assert_eq!(img.verify(&bytes), Ok(()), "{name}");
}
}
#[test]
fn names_resolve_exactly() {
let img = image("E320_PWM_FPGA16.53_20231227_SM16386S_SM16269SH.hex").expect("16.53 listed");
assert_eq!(img.version, Version(16, 53));
assert_eq!(img.kind, "PWM");
assert_eq!(img.chips, ["SM16386S", "SM16269SH"]);
assert_eq!(img.pcb, None);
assert_eq!(
image("E320_PCB6.1_LS0allDA_FPGA6.69_20220907.hex").map(|i| i.pcb.as_deref()),
Some(Some("6.1"))
);
assert!(image("e320_pwm_fpga16.53_20231227_sm16386s_sm16269sh.hex").is_none());
assert!(image("third-party/firmware/E320_PWM_FPGA16.53_20231227_SM16386S_SM16269SH.hex").is_none());
}
#[test]
fn a_wrong_hash_or_size_is_refused() {
let bytes = vec![0xA5u8; manifest().size as usize];
let img = Image {
name: "synthetic.hex".into(),
version: Version(1, 0),
pcb: None,
kind: "PWM".into(),
chips: Vec::new(),
sha256: sha256_hex(&bytes),
};
assert_eq!(img.verify(&bytes), Ok(()));
let mut other = bytes.clone();
other[0] = 0x5A;
assert!(img.verify(&other).unwrap_err().contains("sha256"));
assert!(img.verify(&bytes[..bytes.len() - 1]).unwrap_err().contains("size"));
}
#[test]
fn a_malformed_manifest_is_refused() {
let bad = "base_url = \"\"\nprefix = \"f\"\nsize = 1\nimage = [ { name = \"x.hex\", version = \"1.0\", kind = \"PWM\", sha256 = \"abc\" } ]\n";
assert!(parse(bad).unwrap_err().contains("64 lowercase hex"));
let twice = TEXT.trim_end().trim_end_matches(']').to_string()
+ " { name = \"E320_PCB6.0_PWM_FPGA9.53_20221031.hex\", version = \"9.53\", kind = \"PWM\", sha256 = \"cb7c264231d7123bbf3fba4a9ec964a410b20e284db5715e46f50da0eeaffa19\" },\n]\n";
assert!(parse(&twice).unwrap_err().contains("listed twice"));
}
}