use resident_fat::{BlockDevice, FileSystem};
use crate::bundle::{self, Bundle, Checksum, Entry, Format, Role};
const SCRATCH: usize = 64 * 1024;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Report {
pub kernel_len: Option<u32>,
pub written: u32,
pub skipped: u32,
}
pub trait Progress {
fn starting(&mut self, entry: Entry<'_>) {
let _ = entry;
}
fn wrote(&mut self, entry: Entry<'_>) {
let _ = entry;
}
fn verified(&mut self, entry: Entry<'_>) {
let _ = entry;
}
fn skipped(&mut self, entry: Entry<'_>) {
let _ = entry;
}
}
impl Progress for () {}
#[derive(Debug)]
pub enum Error<E> {
Bundle(bundle::Error),
Storage(resident_fat::Error<E>),
VerifyFailed,
}
impl<E> Error<E> {
pub fn http_status(&self) -> u16 {
match self {
Error::Bundle(error) => error.http_status(),
Error::Storage(_) | Error::VerifyFailed => 500,
}
}
}
impl<E> From<bundle::Error> for Error<E> {
fn from(error: bundle::Error) -> Error<E> {
Error::Bundle(error)
}
}
impl<E> From<resident_fat::Error<E>> for Error<E> {
fn from(error: resident_fat::Error<E>) -> Error<E> {
Error::Storage(error)
}
}
impl<E: core::fmt::Debug> core::fmt::Display for Error<E> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Error::Bundle(error) => write!(f, "{error}"),
Error::Storage(error) => write!(f, "{error}"),
Error::VerifyFailed => f.write_str("write verification failed"),
}
}
}
impl<E: core::fmt::Debug> core::error::Error for Error<E> {}
pub fn apply<D, P>(
volume: &mut FileSystem<D>,
format: &Format,
bundle: &[u8],
progress: &mut P,
) -> Result<Report, Error<D::Error>>
where
D: BlockDevice,
P: Progress,
{
let parsed = Bundle::parse(format, bundle)?;
let installed = install(volume, &parsed, progress);
let synced = volume.sync();
installed.and_then(|report| synced.map(|()| report).map_err(Error::from))
}
fn install<D, P>(
volume: &mut FileSystem<D>,
bundle: &Bundle<'_>,
progress: &mut P,
) -> Result<Report, Error<D::Error>>
where
D: BlockDevice,
P: Progress,
{
let mut scratch = alloc::vec![0u8; SCRATCH];
let mut report = Report::default();
for role in [Role::File, Role::Firmware, Role::Config] {
for entry in bundle.iter().filter(|entry| entry.role == role) {
record(
&mut report,
install_entry(volume, entry, progress, &mut scratch)?,
);
}
}
if let Some(entry) = bundle.kernel() {
report.kernel_len = Some(entry.data.len() as u32);
record(
&mut report,
install_entry(volume, entry, progress, &mut scratch)?,
);
}
Ok(report)
}
fn record(report: &mut Report, written: bool) {
if written {
report.written += 1;
} else {
report.skipped += 1;
}
}
fn install_entry<D, P>(
volume: &mut FileSystem<D>,
entry: Entry<'_>,
progress: &mut P,
scratch: &mut [u8],
) -> Result<bool, Error<D::Error>>
where
D: BlockDevice,
P: Progress,
{
progress.starting(entry);
if on_card(volume, entry.path, entry.data, scratch)? {
progress.skipped(entry);
return Ok(false);
}
create_parents(volume, entry.path)?;
volume.write_file(entry.path, entry.data)?;
progress.wrote(entry);
if !on_card(volume, entry.path, entry.data, scratch)? {
return Err(Error::VerifyFailed);
}
progress.verified(entry);
Ok(true)
}
fn on_card<D>(
volume: &mut FileSystem<D>,
path: &str,
data: &[u8],
scratch: &mut [u8],
) -> Result<bool, Error<D::Error>>
where
D: BlockDevice,
{
let file = match volume.open(path) {
Ok(file) => file,
Err(resident_fat::Error::NotFound { .. }) => return Ok(false),
Err(error) => return Err(Error::Storage(error)),
};
if file.len() as usize != data.len() {
return Ok(false);
}
let mut checksum = Checksum::new();
let mut at = 0u64;
while at < file.len() as u64 {
let read = volume.read_at(&file, at, scratch)?;
if read == 0 {
return Ok(false);
}
checksum.update(&scratch[..read]);
at += read as u64;
}
Ok(checksum.finish() == bundle::checksum(data))
}
fn create_parents<D>(volume: &mut FileSystem<D>, path: &str) -> Result<(), Error<D::Error>>
where
D: BlockDevice,
{
let mut at = 0;
while let Some(offset) = path[at..].find('/') {
let end = at + offset;
let directory = &path[..end];
if volume.open_dir(directory).is_err() {
volume.create_dir(directory)?;
}
at = end + 1;
}
Ok(())
}