// Copyright (c) 2017-2021 Linaro LTD
// Copyright (c) 2017-2019 JUUL Labs
// Copyright (c) 2019-2023 Arm Limited
//
// SPDX-License-Identifier: Apache-2.0

//! Interface wrappers to C API entering to the bootloader

use crate::area::AreaDesc;
use simflash::SimMultiFlash;
use crate::api;

#[allow(unused)]
use std::sync::Once;

use std::borrow::Borrow;

/// The result of an invocation of `boot_go`.  This is intentionally opaque so that we can provide
/// accessors for everything we need from this.
#[derive(Debug)]
pub enum BootGoResult {
    /// This run was stopped by the flash simulation mechanism.
    Stopped,
    /// The bootloader ran to completion with the following data.
    Normal {
        result: i32,
        asserts: u8,

        resp: api::BootRsp,
    },
}

impl BootGoResult {
    /// Was this run interrupted.
    pub fn interrupted(&self) -> bool {
        matches!(self, BootGoResult::Stopped)
    }

    /// Was this boot run successful (returned 0)
    pub fn success(&self) -> bool {
        matches!(self, BootGoResult::Normal { result: 0, .. })
    }

    /// Success, but also no asserts.
    pub fn success_no_asserts(&self) -> bool {
        matches!(self, BootGoResult::Normal {
            result: 0,
            asserts: 0,
            ..
        })
    }

    /// Get the asserts count.  An interrupted run will be considered to have no asserts.
    pub fn asserts(&self) -> u8 {
        match self {
            BootGoResult::Normal { asserts, .. } => *asserts,
            _ => 0,
        }
    }

    /// Retrieve the 'resp' field that is filled in.
    pub fn resp(&self) -> Option<&api::BootRsp> {
        match self {
            BootGoResult::Normal { resp, .. } => Some(resp),
            _ => None,
        }
    }
}

/// Invoke the bootloader on this flash device.
pub fn boot_go(multiflash: &mut SimMultiFlash, areadesc: &AreaDesc,
               counter: Option<&mut i32>, image_index: Option<i32>,
               catch_asserts: bool) -> BootGoResult {
    init_crypto();

    for (&dev_id, flash) in multiflash.iter_mut() {
        api::set_flash(dev_id, flash);
    }
    let mut sim_ctx = api::CSimContext {
        flash_counter: match counter {
            None => 0,
            Some(ref c) => **c as libc::c_int
        },
        c_catch_asserts: if catch_asserts { 1 } else { 0 },
        .. Default::default()
    };
    let mut rsp = api::BootRsp {
        br_hdr: std::ptr::null(),
        flash_dev_id: 0,
        image_off: 0,
    };
    let result: i32 = unsafe {
        let adesc = areadesc.get_c();
        match image_index {
            None => raw::invoke_boot_go(&mut sim_ctx as *mut _,
                                        adesc.borrow() as *const _,
                                        &mut rsp as *mut _, -1) as i32,
            Some(i) => raw::invoke_boot_go(&mut sim_ctx as *mut _,
                                           adesc.borrow() as *const _,
                                           &mut rsp as *mut _,
                                           i as i32) as i32
        }
    };
    let asserts = sim_ctx.c_asserts;
    if let Some(c) = counter {
        *c = sim_ctx.flash_counter;
    }
    for &dev_id in multiflash.keys() {
        api::clear_flash(dev_id);
    }
    if result == -0x13579 {
        BootGoResult::Stopped
    } else {
        BootGoResult::Normal { result, asserts, resp: rsp }
    }
}

pub fn boot_trailer_sz(align: u32) -> u32 {
    unsafe { raw::boot_trailer_sz(align) }
}

pub fn boot_status_sz(align: u32) -> u32 {
    unsafe { raw::boot_status_sz(align) }
}

pub fn boot_magic_sz() -> usize {
    unsafe { raw::boot_magic_sz() as usize }
}

pub fn boot_max_align() -> usize {
    unsafe { raw::boot_max_align() as usize }
}

pub fn rsa_oaep_encrypt(pubkey: &[u8], seckey: &[u8]) -> Result<[u8; 256], &'static str> {
    unsafe {
        let mut encbuf: [u8; 256] = [0; 256];
        if raw::rsa_oaep_encrypt_(pubkey.as_ptr(), pubkey.len() as u32,
                                  seckey.as_ptr(), seckey.len() as u32,
                                  encbuf.as_mut_ptr()) == 0 {
            return Ok(encbuf);
        }
        Err("Failed to encrypt buffer")
    }
}

pub fn kw_encrypt(kek: &[u8], seckey: &[u8], keylen: u32) -> Result<Vec<u8>, &'static str> {
    unsafe {
        let mut encbuf = vec![0u8; 24];
        if keylen == 32 {
            encbuf = vec![0u8; 40];
        }
        if raw::kw_encrypt_(kek.as_ptr(), seckey.as_ptr(), encbuf.as_mut_ptr()) == 0 {
            return Ok(encbuf);
        }
        Err("Failed to encrypt buffer")
    }
}

pub fn set_security_counter(image_index: u32, security_counter_value: u32) {
    api::sim_set_nv_counter_for_image(image_index, security_counter_value);
}

pub fn get_security_counter(image_index: u32) -> u32 {
    let mut counter_val: u32 = 0;
    api::sim_get_nv_counter_for_image(image_index, &mut counter_val as *mut u32);
    return counter_val;
}

mod raw {
    use crate::area::CAreaDesc;
    use crate::api::{BootRsp, CSimContext};

    extern "C" {
        // This generates a warning about `CAreaDesc` not being foreign safe.  There doesn't appear to
        // be any way to get rid of this warning.  See https://github.com/rust-lang/rust/issues/34798
        // for information and tracking.
        pub fn invoke_boot_go(sim_ctx: *mut CSimContext, areadesc: *const CAreaDesc,
            rsp: *mut BootRsp, image_index: libc::c_int) -> libc::c_int;

        pub fn boot_trailer_sz(min_write_sz: u32) -> u32;
        pub fn boot_status_sz(min_write_sz: u32) -> u32;

        pub fn boot_magic_sz() -> u32;
        pub fn boot_max_align() -> u32;

        pub fn rsa_oaep_encrypt_(pubkey: *const u8, pubkey_len: libc::c_uint,
                                 seckey: *const u8, seckey_len: libc::c_uint,
                                 encbuf: *mut u8) -> libc::c_int;

        pub fn kw_encrypt_(kek: *const u8, seckey: *const u8,
                           encbuf: *mut u8) -> libc::c_int;

        #[allow(unused)]
        pub fn psa_crypto_init() -> u32;

        #[allow(unused)]
        pub fn mbedtls_test_enable_insecure_external_rng();
    }
}

#[allow(unused)]
static PSA_INIT_SYNC: Once = Once::new();

#[allow(unused)]
static MBEDTLS_EXTERNAL_RNG_ENABLE_SYNC: Once = Once::new();

#[cfg(feature = "psa-crypto-api")]
fn init_crypto() {
    PSA_INIT_SYNC.call_once(|| {
        assert_eq!(unsafe { raw::psa_crypto_init() }, 0);
    });

    /* The PSA APIs require properly initialisation of the entropy subsystem
     * The configuration adds the option MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG when the
     * psa-crypto-api feature is enabled. As a result the tests use the implementation
     * of the test external rng that needs to be initialised before being able to use it
     */
    MBEDTLS_EXTERNAL_RNG_ENABLE_SYNC.call_once(|| {
        unsafe { raw::mbedtls_test_enable_insecure_external_rng() }
    });
}

#[cfg(not(feature = "psa-crypto-api"))]
fn init_crypto() {
   // When the feature is not enabled, the init is just empty
}
