esp-hal-ota 0.6.2

OTA library for esp-hal
extern crate std;

use super::*;
use std::{vec, vec::Vec};

const OTADATA: u32 = 0xD000;
const OTA_0: u32 = 0x10000;
const OTA_1: u32 = 0x20000;
const OTA_SIZE: u32 = 0x10000;

struct MemFlash(Vec<u8>);

impl ReadStorage for MemFlash {
    type Error = ();

    fn read(&mut self, offset: u32, bytes: &mut [u8]) -> core::result::Result<(), ()> {
        let src = self
            .0
            .get(offset as usize..offset as usize + bytes.len())
            .ok_or(())?;
        bytes.copy_from_slice(src);
        Ok(())
    }

    fn capacity(&self) -> usize {
        self.0.len()
    }
}

impl Storage for MemFlash {
    fn write(&mut self, offset: u32, bytes: &[u8]) -> core::result::Result<(), ()> {
        let dst = self
            .0
            .get_mut(offset as usize..offset as usize + bytes.len())
            .ok_or(())?;
        dst.copy_from_slice(bytes);
        Ok(())
    }
}

fn part_entry(p_type: u8, subtype: u8, offset: u32, size: u32) -> [u8; 32] {
    let mut e = [0u8; 32];
    e[0..4].copy_from_slice(&[0xAA, 0x50, p_type, subtype]);
    e[4..8].copy_from_slice(&offset.to_le_bytes());
    e[8..12].copy_from_slice(&size.to_le_bytes());
    e
}

fn flash_with(entries: &[[u8; 32]]) -> MemFlash {
    let mut flash = vec![0xFF; 0x30000];
    for (i, e) in entries.iter().enumerate() {
        let at = PART_OFFSET as usize + i * 32;
        flash[at..at + 32].copy_from_slice(e);
    }
    MemFlash(flash)
}

fn default_flash() -> MemFlash {
    flash_with(&[
        part_entry(1, 2, 0x9000, 0x4000), // nvs
        part_entry(1, 0, OTADATA, 0x2000),
        part_entry(0, 0x10, OTA_0, OTA_SIZE),
        part_entry(0, 0x11, OTA_1, OTA_SIZE),
        part_entry(0, 0x20, 0x2_0000, 0x1000), // test app, not an OTA slot
    ])
}

fn entry_at(ota: &mut Ota<MemFlash>, slot: u8) -> EspOtaSelectEntry {
    ota.read_boot_entry(slot).unwrap()
}

#[test]
fn crc_matches_esp_idf() {
    assert_eq!(crc32_le(0, b"123456789"), 0xCBF43926);
    // values from ESP-IDF otatool: binascii.crc32(struct.pack('<I', seq), 0xFFFFFFFF)
    assert!(helpers::is_crc_seq_correct(1, 0x4743989A));
    assert!(helpers::is_crc_seq_correct(2, 0x55F63774));
    assert!(!helpers::is_crc_seq_correct(2, 0x4743989A));
    // erased otadata is never valid
    assert!(!helpers::is_crc_seq_correct(
        u32::MAX,
        crc32_le(u32::MAX, &[0xFF; 4])
    ));
}

#[test]
fn helpers_do_not_panic_on_bad_input() {
    assert_eq!(helpers::seq_to_part(5, 0), 0);
    assert_eq!(helpers::seq_to_part(0, 2), 0);
    assert_eq!(helpers::seq_to_part(4, 2), 1);
    assert_eq!(helpers::seq_or_default(&[1, 0, 0], 0x4743989A, 7), 7);
    assert_eq!(helpers::seq_or_default(&[1, 0, 0, 0], 0x4743989A, 7), 1);
}

#[test]
fn entry_parsing_accepts_any_state_value() {
    let mut bytes = [0xAB; 32];
    bytes[24..28].copy_from_slice(&7u32.to_le_bytes());
    let entry = EspOtaSelectEntry::from_bytes(&bytes);
    assert_eq!(entry.ota_state, OtaImgState::EspOtaImgUndefined);
    assert_eq!(entry.seq_label, [0xAB; 20]);

    let mut entry = entry;
    entry.ota_state = OtaImgState::EspOtaImgValid;
    let parsed = EspOtaSelectEntry::from_bytes(&entry.to_bytes());
    assert_eq!(parsed.ota_state, OtaImgState::EspOtaImgValid);
    assert_eq!(parsed.seq, entry.seq);
}

#[test]
fn partition_table_parsing() {
    let ota = Ota::new(default_flash()).unwrap();
    assert_eq!(
        ota.get_partitions(),
        &[(OTA_0, OTA_SIZE), (OTA_1, OTA_SIZE)]
    );
    assert_eq!(ota.pinfo.otadata_offset, OTADATA);

    let no_otadata = flash_with(&[
        part_entry(0, 0x10, OTA_0, OTA_SIZE),
        part_entry(0, 0x11, OTA_1, OTA_SIZE),
    ]);
    assert_eq!(
        Ota::new(no_otadata).err(),
        Some(OtaError::NotEnoughPartitions)
    );

    let out_of_order = flash_with(&[
        part_entry(1, 0, OTADATA, 0x2000),
        part_entry(0, 0x11, OTA_1, OTA_SIZE),
        part_entry(0, 0x10, OTA_0, OTA_SIZE),
    ]);
    assert_eq!(
        Ota::new(out_of_order).err(),
        Some(OtaError::WrongOTAPArtitionOrder)
    );
}

#[test]
fn full_update_writes_otadata() {
    let image: Vec<u8> = (0..5000u32).map(|i| i as u8).collect();
    let crc = crc32_le(0, &image);

    let mut ota = Ota::new(default_flash()).unwrap();
    assert_eq!(ota.ota_write_chunk(&image), Err(OtaError::OtaNotStarted));
    assert_eq!(
        ota.ota_begin(OTA_SIZE + 1, crc),
        Err(OtaError::OtaPartitionTooSmall)
    );

    ota.ota_begin(image.len() as u32, crc).unwrap();
    assert_eq!(ota.ota_write_chunk(&image[..1000]), Ok(false));
    assert_eq!(ota.ota_flush(false, true), Err(OtaError::WrongCRC)); // incomplete

    // last chunk with trailing garbage, which is ignored
    let mut rest = image[1000..].to_vec();
    rest.extend_from_slice(&[0x55; 100]);
    assert_eq!(ota.ota_write_chunk(&rest), Ok(true));
    assert_eq!(ota.get_ota_progress(), 1.0);
    assert_eq!(ota.ota_verify(), Ok(true));
    ota.ota_flush(true, true).unwrap();

    assert_eq!(&ota.flash.0[OTA_0 as usize..][..image.len()], &image[..]);
    // fresh otadata: entry 2 gets seq 1 (= ota_0)
    let entry = entry_at(&mut ota, 2);
    assert_eq!(entry.seq, 1);
    assert_eq!(entry.ota_state, OtaImgState::EspOtaImgNew);
    assert_eq!(entry_at(&mut ota, 1).seq, 0);
}

#[test]
fn wrong_crc_does_not_touch_otadata() {
    let mut ota = Ota::new(default_flash()).unwrap();
    ota.ota_begin(4, 1234).unwrap();
    assert_eq!(ota.ota_write_chunk(&[1, 2, 3, 4]), Ok(true));
    assert_eq!(ota.ota_flush(false, false), Err(OtaError::WrongCRC));
    assert_eq!(ota.ota_flush(true, false), Err(OtaError::OtaVerifyError));
    assert!(
        ota.flash.0[OTADATA as usize..][..0x2000]
            .iter()
            .all(|&b| b == 0xFF)
    );
}

#[test]
fn boot_seq_follows_esp_idf() {
    let mut ota = Ota::new(default_flash()).unwrap();

    ota.write_boot_partition(0, OtaImgState::EspOtaImgNew)
        .unwrap();
    assert_eq!(
        (entry_at(&mut ota, 1).seq, entry_at(&mut ota, 2).seq),
        (0, 1)
    );

    ota.write_boot_partition(1, OtaImgState::EspOtaImgNew)
        .unwrap();
    assert_eq!(
        (entry_at(&mut ota, 1).seq, entry_at(&mut ota, 2).seq),
        (2, 1)
    );

    ota.write_boot_partition(0, OtaImgState::EspOtaImgNew)
        .unwrap();
    assert_eq!(
        (entry_at(&mut ota, 1).seq, entry_at(&mut ota, 2).seq),
        (2, 3)
    );

    // same partition again keeps the seq, written to the inactive entry
    ota.write_boot_partition(0, OtaImgState::EspOtaImgNew)
        .unwrap();
    assert_eq!(
        (entry_at(&mut ota, 1).seq, entry_at(&mut ota, 2).seq),
        (3, 3)
    );

    assert_eq!(
        ota.write_boot_partition(2, OtaImgState::EspOtaImgNew),
        Err(OtaError::NotEnoughPartitions)
    );
}

#[test]
fn resume_validates_input() {
    let image = [7u8; 300];
    let crc = crc32_le(0, &image);

    let mut ota = Ota::new(default_flash()).unwrap();
    assert_eq!(
        ota.ota_resume(10, 20, 0, 0, false),
        Err(OtaError::OtaPartitionTooSmall)
    );

    ota.ota_begin(300, crc).unwrap();
    ota.ota_write_chunk(&image[..100]).unwrap();
    let details = ota.get_progress_details().unwrap();

    let mut ota = Ota::new(MemFlash(ota.flash.0)).unwrap();
    assert_eq!(
        ota.ota_resume(300, details.remaining, crc, details.last_crc ^ 1, true),
        Err(OtaError::WrongCRC)
    );
    ota.ota_resume(300, details.remaining, crc, details.last_crc, true)
        .unwrap();
    assert_eq!(ota.ota_write_chunk(&image[100..]), Ok(true));
    ota.ota_flush(true, false).unwrap();
}