// Copyright 2022, The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Error types used in libavb.
//!
//! There are a few advantages of providing these custom types rather than exposing the raw bindgen
//! enums directly:
//! * More idiomatic error handling
//!   * C code defines a "status" enum that can contain either OK or an error, whereas Rust prefers
//!     error-only enums to use with `Result<>` e.g. `Result<(), IoError>`. An "OK" status doesn't
//!     make sense when used with `Result<>`.
//! * Better naming e.g. `IoError::Oom` vs the redundant `AvbIoResult::AVB_IO_RESULT_ERROR_OOM`
//! * We can implement traits such as `Display` for added convenience.

// The naming scheme can be a bit confusing due to the re-use of "result" in a few places:
// * `Avb*Result`: raw libavb enums generated by bindgen, containing errors and "OK". Internal-only;
//                 library users should never have to use these types.
// * `*Error`: `Avb*Result` wrappers which only contain error conditions, not "OK". Should be
//             wrapped in a Rust `Result<>` in public API.
// * `Result<T, *Error>`: top-level `Result<>` type used in this library's public API.

use avb_bindgen::{AvbIOResult, AvbSlotVerifyResult};

use core::{fmt, str::Utf8Error};

/// `AvbSlotVerifyResult` error wrapper.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SlotVerifyError {
    /// `AVB_SLOT_VERIFY_RESULT_ERROR_INVALID_ARGUMENT`
    InvalidArgument,
    /// `AVB_SLOT_VERIFY_RESULT_ERROR_INVALID_METADATA`
    InvalidMetadata,
    /// `AVB_SLOT_VERIFY_RESULT_ERROR_IO`
    Io,
    /// `AVB_SLOT_VERIFY_RESULT_ERROR_OOM`
    Oom,
    /// `AVB_SLOT_VERIFY_RESULT_ERROR_PUBLIC_KEY_REJECTED`
    PublicKeyRejected,
    /// `AVB_SLOT_VERIFY_RESULT_ERROR_ROLLBACK_INDEX`
    RollbackIndex,
    /// `AVB_SLOT_VERIFY_RESULT_ERROR_UNSUPPORTED_VERSION`
    UnsupportedVersion,
    /// `AVB_SLOT_VERIFY_RESULT_ERROR_VERIFICATION`
    Verification,
}

impl fmt::Display for SlotVerifyError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Self::InvalidArgument => write!(f, "Invalid parameters"),
            Self::InvalidMetadata => write!(f, "Invalid metadata"),
            Self::Io => write!(f, "I/O error"),
            Self::Oom => write!(f, "Unable to allocate memory"),
            Self::PublicKeyRejected => write!(f, "Public key rejected or data not signed"),
            Self::RollbackIndex => write!(f, "Rollback index violation"),
            Self::UnsupportedVersion => write!(f, "Unsupported vbmeta version"),
            Self::Verification => write!(f, "Verification failure"),
        }
    }
}

// Converts a bindgen `AvbSlotVerifyResult` enum to a `Result<>`, mapping
// `AVB_SLOT_VERIFY_RESULT_OK` to the Rust equivalent `Ok(())` and errors to the corresponding
// `Err(SlotVerifyError)`.
//
// This function is also important to serve as a compile-time check that we're handling all the
// libavb enums; if a new one is added to (or removed from) the C code, this will fail to compile
// until it is updated to match.
//
// TODO(b/290110273): this can be limited to pub(crate) once we've moved the full libavb wrapper
// here.
pub fn slot_verify_enum_to_result(result: AvbSlotVerifyResult) -> Result<(), SlotVerifyError> {
    match result {
        AvbSlotVerifyResult::AVB_SLOT_VERIFY_RESULT_OK => Ok(()),
        AvbSlotVerifyResult::AVB_SLOT_VERIFY_RESULT_ERROR_INVALID_ARGUMENT => {
            Err(SlotVerifyError::InvalidArgument)
        }
        AvbSlotVerifyResult::AVB_SLOT_VERIFY_RESULT_ERROR_INVALID_METADATA => {
            Err(SlotVerifyError::InvalidMetadata)
        }
        AvbSlotVerifyResult::AVB_SLOT_VERIFY_RESULT_ERROR_IO => Err(SlotVerifyError::Io),
        AvbSlotVerifyResult::AVB_SLOT_VERIFY_RESULT_ERROR_OOM => Err(SlotVerifyError::Oom),
        AvbSlotVerifyResult::AVB_SLOT_VERIFY_RESULT_ERROR_PUBLIC_KEY_REJECTED => {
            Err(SlotVerifyError::PublicKeyRejected)
        }
        AvbSlotVerifyResult::AVB_SLOT_VERIFY_RESULT_ERROR_ROLLBACK_INDEX => {
            Err(SlotVerifyError::RollbackIndex)
        }
        AvbSlotVerifyResult::AVB_SLOT_VERIFY_RESULT_ERROR_UNSUPPORTED_VERSION => {
            Err(SlotVerifyError::UnsupportedVersion)
        }
        AvbSlotVerifyResult::AVB_SLOT_VERIFY_RESULT_ERROR_VERIFICATION => {
            Err(SlotVerifyError::Verification)
        }
    }
}

/// `AvbIOResult` error wrapper.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum IoError {
    /// `AVB_IO_RESULT_ERROR_OOM`
    Oom,
    /// `AVB_IO_RESULT_ERROR_IO`
    Io,
    /// `AVB_IO_RESULT_ERROR_NO_SUCH_PARTITION`
    NoSuchPartition,
    /// `AVB_IO_RESULT_ERROR_RANGE_OUTSIDE_PARTITION`
    RangeOutsidePartition,
    /// `AVB_IO_RESULT_ERROR_NO_SUCH_VALUE`
    NoSuchValue,
    /// `AVB_IO_RESULT_ERROR_INVALID_VALUE_SIZE`
    InvalidValueSize,
    /// `AVB_IO_RESULT_ERROR_INSUFFICIENT_SPACE`
    InsufficientSpace,
    /// Custom error code to indicate that an optional callback method has not been implemented.
    /// If this is returned from a required callback method, it will bubble up as an `Io` error.
    NotImplemented,
}

impl fmt::Display for IoError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Self::Oom => write!(f, "Unable to allocate memory"),
            Self::Io => write!(f, "I/O error"),
            Self::NoSuchPartition => write!(f, "No such partition exists"),
            Self::RangeOutsidePartition => write!(f, "Range is outside the partition"),
            Self::NoSuchValue => write!(f, "No such named persistent value"),
            Self::InvalidValueSize => write!(f, "Invalid named persistent value size"),
            Self::InsufficientSpace => write!(f, "Buffer is too small"),
            Self::NotImplemented => write!(f, "Function not implemented"),
        }
    }
}

impl From<Utf8Error> for IoError {
    fn from(_: Utf8Error) -> Self {
        Self::Io
    }
}

// Converts a bindgen `AvbIOResult` enum to a `Result<>`, mapping `AVB_IO_RESULT_OK` to the Rust
// equivalent `Ok(())` and errors to the corresponding `Err(IoError)`.
//
// This function is also important to serve as a compile-time check that we're handling all the
// libavb enums; if a new one is added to (or removed from) the C code, this will fail to compile
// until it is updated to match.
#[allow(dead_code)]
pub(crate) fn io_enum_to_result(result: AvbIOResult) -> Result<(), IoError> {
    match result {
        AvbIOResult::AVB_IO_RESULT_OK => Ok(()),
        AvbIOResult::AVB_IO_RESULT_ERROR_OOM => Err(IoError::Oom),
        AvbIOResult::AVB_IO_RESULT_ERROR_IO => Err(IoError::Io),
        AvbIOResult::AVB_IO_RESULT_ERROR_NO_SUCH_PARTITION => Err(IoError::NoSuchPartition),
        AvbIOResult::AVB_IO_RESULT_ERROR_RANGE_OUTSIDE_PARTITION => {
            Err(IoError::RangeOutsidePartition)
        }
        AvbIOResult::AVB_IO_RESULT_ERROR_NO_SUCH_VALUE => Err(IoError::NoSuchValue),
        AvbIOResult::AVB_IO_RESULT_ERROR_INVALID_VALUE_SIZE => Err(IoError::InvalidValueSize),
        AvbIOResult::AVB_IO_RESULT_ERROR_INSUFFICIENT_SPACE => Err(IoError::InsufficientSpace),
    }
}

// Converts our `IoError` to the bindgen `AvbIOResult` enum.
//
// Unlike `SlotVerifyError` which gets generated by libavb and passed to the caller, `IoError` is
// created by the user callbacks and passed back into libavb so we need to be able to convert in
// this direction as well.
impl From<IoError> for AvbIOResult {
    fn from(error: IoError) -> Self {
        match error {
            IoError::Oom => AvbIOResult::AVB_IO_RESULT_ERROR_OOM,
            IoError::Io => AvbIOResult::AVB_IO_RESULT_ERROR_IO,
            IoError::NoSuchPartition => AvbIOResult::AVB_IO_RESULT_ERROR_NO_SUCH_PARTITION,
            IoError::RangeOutsidePartition => {
                AvbIOResult::AVB_IO_RESULT_ERROR_RANGE_OUTSIDE_PARTITION
            }
            IoError::NoSuchValue => AvbIOResult::AVB_IO_RESULT_ERROR_NO_SUCH_VALUE,
            IoError::InvalidValueSize => AvbIOResult::AVB_IO_RESULT_ERROR_INVALID_VALUE_SIZE,
            IoError::InsufficientSpace => AvbIOResult::AVB_IO_RESULT_ERROR_INSUFFICIENT_SPACE,
            // `NotImplemented` is internal to this library and doesn't have a libavb equivalent,
            // convert it to the default I/O error.
            IoError::NotImplemented => AvbIOResult::AVB_IO_RESULT_ERROR_IO,
        }
    }
}

// Converts a `Result<>` to the bindgen `AvbIOResult` enum.
//
// TODO(b/290110273): this can be limited to pub(crate) once we've moved the full libavb wrapper
// here.
pub fn result_to_io_enum(result: Result<(), IoError>) -> AvbIOResult {
    result.map_or_else(|e| e.into(), |_| AvbIOResult::AVB_IO_RESULT_OK)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_SlotVerifyError_display() {
        // The actual error message can change as needed, the point of the test is just to make sure
        // the fmt::Display trait is properly implemented.
        assert_eq!(
            format!("{}", SlotVerifyError::Verification),
            "Verification failure"
        );
    }

    #[test]
    fn test_SlotVerifyError_from_raw() {
        assert_eq!(
            slot_verify_enum_to_result(AvbSlotVerifyResult::AVB_SLOT_VERIFY_RESULT_OK),
            Ok(())
        );
        assert_eq!(
            slot_verify_enum_to_result(AvbSlotVerifyResult::AVB_SLOT_VERIFY_RESULT_ERROR_IO),
            Err(SlotVerifyError::Io)
        );
    }

    #[test]
    fn test_IoError_display() {
        // The actual error message can change as needed, the point of the test is just to make sure
        // the fmt::Display trait is properly implemented.
        assert_eq!(
            format!("{}", IoError::NoSuchPartition),
            "No such partition exists"
        );
    }

    #[test]
    fn test_IoError_from_raw() {
        assert_eq!(io_enum_to_result(AvbIOResult::AVB_IO_RESULT_OK), Ok(()));
        assert_eq!(
            io_enum_to_result(AvbIOResult::AVB_IO_RESULT_ERROR_IO),
            Err(IoError::Io)
        );
    }

    #[test]
    fn test_IoError_to_raw() {
        assert_eq!(result_to_io_enum(Ok(())), AvbIOResult::AVB_IO_RESULT_OK);
        assert_eq!(
            result_to_io_enum(Err(IoError::Io)),
            AvbIOResult::AVB_IO_RESULT_ERROR_IO
        );
    }
}
