// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
#![deny(missing_docs)]
#![deny(warnings)]

use clap::ArgMatches;
use libc::VMADDR_CID_HOST;
#[cfg(test)]
use libc::VMADDR_CID_LOCAL;
use serde::{Deserialize, Serialize};
use std::fs::File;

use crate::common::{NitroCliErrorEnum, NitroCliFailure, NitroCliResult, VMADDR_CID_PARENT};
use crate::get_id_by_name;
use crate::new_nitro_cli_failure;
use crate::utils::PcrType;

/// The arguments used by the `run-enclave` command.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunEnclavesArgs {
    /// The path to the enclave image file.
    pub eif_path: String,
    /// The optional enclave CID
    pub enclave_cid: Option<u64>,
    /// The amount of memory that will be given to the enclave.
    pub memory_mib: u64,
    /// An optional list of CPU IDs that will be given to the enclave.
    pub cpu_ids: Option<Vec<u32>>,
    /// A flag indicating if the enclave will be started in debug mode.
    #[serde(default)]
    pub debug_mode: bool,
    /// Attach to the console immediately if using debug mode.
    #[serde(default)]
    pub attach_console: bool,
    /// The number of CPUs that the enclave will receive.
    pub cpu_count: Option<u32>,
    /// Enclave name set by the user.
    pub enclave_name: Option<String>,
}

impl RunEnclavesArgs {
    /// Construct a new `RunEnclavesArgs` instance from the given command-line arguments.
    pub fn new_with(args: &ArgMatches) -> NitroCliResult<Self> {
        if let Some(config_file) = args.value_of("config") {
            let file = File::open(config_file).map_err(|err| {
                new_nitro_cli_failure!(
                    &format!("Failed to open config file: {:?}", err),
                    NitroCliErrorEnum::FileOperationFailure
                )
                .add_info(vec![config_file, "Open"])
            })?;

            let mut json: RunEnclavesArgs = serde_json::from_reader(file).map_err(|err| {
                new_nitro_cli_failure!(
                    &format!("Invalid JSON format for config file: {:?}", err),
                    NitroCliErrorEnum::SerdeError
                )
            })?;
            if json.cpu_count.is_none() && json.cpu_ids.is_none() {
                return Err(new_nitro_cli_failure!(
                    "Missing both `cpu-count` and `cpu-ids`",
                    NitroCliErrorEnum::MissingArgument
                ));
            }
            if json.cpu_count.is_some() && json.cpu_ids.is_some() {
                return Err(new_nitro_cli_failure!(
                    "`cpu-count` and `cpu-ids` cannot be used together",
                    NitroCliErrorEnum::ConflictingArgument
                ));
            }

            // attach_console implies debug_mode
            json.debug_mode = json.debug_mode || json.attach_console;

            Ok(json)
        } else {
            Ok(RunEnclavesArgs {
                cpu_count: parse_cpu_count(args)
                    .map_err(|err| err.add_subaction("Parse CPU count".to_string()))?,
                eif_path: parse_eif_path(args)
                    .map_err(|err| err.add_subaction("Parse EIF path".to_string()))?,
                enclave_cid: parse_enclave_cid(args)
                    .map_err(|err| err.add_subaction("Parse enclave CID".to_string()))?,
                memory_mib: parse_memory(args)
                    .map_err(|err| err.add_subaction("Parse memory".to_string()))?,
                cpu_ids: parse_cpu_ids(args)
                    .map_err(|err| err.add_subaction("Parse CPU IDs".to_string()))?,
                debug_mode: debug_mode(args),
                attach_console: attach_console(args),
                enclave_name: parse_enclave_name(args)
                    .map_err(|err| err.add_subaction("Parse enclave name".to_string()))?,
            })
        }
    }
}

/// The arguments used by the `build-enclave` command.
#[derive(Debug, Clone)]
pub struct BuildEnclavesArgs {
    /// The URI to the Docker image.
    pub docker_uri: String,
    /// The directory containing the Docker image.
    pub docker_dir: Option<String>,
    /// The path where the enclave image file will be written to.
    pub output: String,
    /// The path to the signing certificate for signed enclaves.
    pub signing_certificate: Option<String>,
    /// The path to the private key for signed enclaves.
    pub private_key: Option<String>,
    /// The name of the enclave image.
    pub img_name: Option<String>,
    /// The version of the enclave image.
    pub img_version: Option<String>,
    /// The path to custom metadata JSON file
    pub metadata: Option<String>,
}

impl BuildEnclavesArgs {
    /// Construct a new `BuildEnclavesArgs` instance from the given command-line arguments.
    pub fn new_with(args: &ArgMatches) -> NitroCliResult<Self> {
        let signing_certificate = parse_signing_certificate(args);
        let private_key = parse_private_key(args);

        match (&signing_certificate, &private_key) {
            (Some(_), None) => {
                return Err(new_nitro_cli_failure!(
                    "`private-key` argument not found",
                    NitroCliErrorEnum::MissingArgument
                )
                .add_info(vec!["private-key"]))
            }
            (None, Some(_)) => {
                return Err(new_nitro_cli_failure!(
                    "`signing-certificate` argument not found",
                    NitroCliErrorEnum::MissingArgument
                )
                .add_info(vec!["signing-certificate"]))
            }
            _ => (),
        };

        Ok(BuildEnclavesArgs {
            docker_uri: parse_docker_tag(args).ok_or_else(|| {
                new_nitro_cli_failure!(
                    "`docker-uri` argument not found",
                    NitroCliErrorEnum::MissingArgument
                )
                .add_info(vec!["docker-uri"])
            })?,
            docker_dir: parse_docker_dir(args),
            output: parse_output(args).ok_or_else(|| {
                new_nitro_cli_failure!(
                    "`output` argument not found",
                    NitroCliErrorEnum::MissingArgument
                )
                .add_info(vec!["output"])
            })?,
            signing_certificate,
            private_key,
            img_name: parse_image_name(args),
            img_version: parse_image_version(args),
            metadata: parse_metadata(args),
        })
    }
}

/// The arguments used by the `terminate-enclave` command.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TerminateEnclavesArgs {
    /// The ID of the enclave that is to be terminated.
    pub enclave_id: String,
}

impl TerminateEnclavesArgs {
    /// Construct a new `TerminateEnclavesArgs` instance from the given command-line arguments.
    pub fn new_with(args: &ArgMatches) -> NitroCliResult<Self> {
        // If a name is given, find the corresponding EnclaveID
        match parse_enclave_name(args)
            .map_err(|e| e.add_subaction("Parse Enclave Name".to_string()))?
        {
            Some(name) => Ok(TerminateEnclavesArgs {
                enclave_id: get_id_by_name(name)
                    .map_err(|e| e.add_subaction("Get ID by Name".to_string()))?,
            }),
            None => Ok(TerminateEnclavesArgs {
                enclave_id: parse_enclave_id(args)
                    .map_err(|e| e.add_subaction("Parse enclave ID".to_string()))?,
            }),
        }
    }
}

/// The arguments used by the `console` command.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConsoleArgs {
    /// The ID of the enclave whose console is to be shown.
    pub enclave_id: String,
    /// The time in seconds after the console disconnects from the enclave.
    pub disconnect_timeout_sec: Option<u64>,
}

impl ConsoleArgs {
    /// Construct a new `ConsoleArgs` instance from the given command-line arguments.
    pub fn new_with(args: &ArgMatches) -> NitroCliResult<Self> {
        // If a name is given, find the corresponding EnclaveID
        let enclave_id = match parse_enclave_name(args)
            .map_err(|e| e.add_subaction("Parse Enclave Name".to_string()))?
        {
            Some(name) => {
                get_id_by_name(name).map_err(|e| e.add_subaction("Get ID by Name".to_string()))?
            }
            None => parse_enclave_id(args)
                .map_err(|e| e.add_subaction("Parse enclave ID".to_string()))?,
        };

        Ok(ConsoleArgs {
            enclave_id,
            disconnect_timeout_sec: parse_disconnect_timeout(args)
                .map_err(|e| e.add_subaction("Parse disconnect timeout".to_string()))?,
        })
    }
}

/// Empty set of arguments.
#[derive(Serialize, Deserialize)]
pub struct EmptyArgs {}

/// The arguments used by `describe-enclaves` command.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DescribeEnclavesArgs {
    /// True if metadata is requested.
    pub metadata: bool,
}

impl DescribeEnclavesArgs {
    /// Construct a new `DescribeEnclavesArgs` instance from the given command-line arguments.
    pub fn new_with(args: &ArgMatches) -> Self {
        DescribeEnclavesArgs {
            metadata: args.is_present("metadata"),
        }
    }
}

/// The arguments used by the `explain` command.
#[derive(Debug, Clone)]
pub struct ExplainArgs {
    /// The error code of the error to explain.
    pub error_code_str: String,
}

impl ExplainArgs {
    /// Construct a new `ExplainArgs` instance from the given command-line arguments.
    pub fn new_with(args: &ArgMatches) -> NitroCliResult<Self> {
        Ok(ExplainArgs {
            error_code_str: parse_error_code_str(args)
                .map_err(|e| e.add_subaction("Parse error code".to_string()))?,
        })
    }
}

/// The arguments used by `pcr` command
pub struct PcrArgs {
    /// Path to the file needed for hashing
    pub path: String,
    /// The type of file we need to hash
    pub pcr_type: PcrType,
}

impl PcrArgs {
    /// Construct a new `PcrArgs` instance from the given command-line arguments.
    pub fn new_with(args: &ArgMatches) -> NitroCliResult<Self> {
        let (val_name, pcr_type) = match args.is_present("signing-certificate") {
            true => ("signing-certificate", PcrType::SigningCertificate),
            false => ("input", PcrType::DefaultType),
        };
        let path = parse_file_path(args, val_name)
            .map_err(|e| e.add_subaction("Parse PCR file".to_string()))?;
        Ok(Self { path, pcr_type })
    }
}

/// Parse file path to hash from the command-line arguments.
fn parse_file_path(args: &ArgMatches, val_name: &str) -> NitroCliResult<String> {
    let path = args.value_of(val_name).ok_or_else(|| {
        new_nitro_cli_failure!(
            "`input` or `signing-certificate` argument not found",
            NitroCliErrorEnum::MissingArgument
        )
    })?;
    Ok(path.to_string())
}

/// Parse the requested amount of enclave memory from the command-line arguments.
fn parse_memory(args: &ArgMatches) -> NitroCliResult<u64> {
    let memory = args.value_of("memory").ok_or_else(|| {
        new_nitro_cli_failure!(
            "`memory` argument not found",
            NitroCliErrorEnum::MissingArgument
        )
    })?;
    memory.parse().map_err(|_| {
        new_nitro_cli_failure!(
            "`memory` is not a number",
            NitroCliErrorEnum::InvalidArgument
        )
        .add_info(vec!["memory", memory])
    })
}

/// Parse the Docker tag from the command-line arguments.
fn parse_docker_tag(args: &ArgMatches) -> Option<String> {
    args.value_of("docker-uri").map(|val| val.to_string())
}

/// Parse the Docker directory from the command-line arguments.
fn parse_docker_dir(args: &ArgMatches) -> Option<String> {
    args.value_of("docker-dir").map(|val| val.to_string())
}

/// Parse the enclave's required CID from the command-line arguments.
fn parse_enclave_cid(args: &ArgMatches) -> NitroCliResult<Option<u64>> {
    let enclave_cid = if let Some(enclave_cid) = args.value_of("enclave-cid") {
        let enclave_cid: u64 = enclave_cid.parse().map_err(|_| {
            new_nitro_cli_failure!(
                "`enclave-cid` is not a number",
                NitroCliErrorEnum::InvalidArgument
            )
            .add_info(vec!["enclave-cid", enclave_cid])
        })?;

        // Do not use well-known CID values - 0, 1, 2 - as the enclave CID.
        // VMADDR_CID_ANY = -1U
        // VMADDR_CID_HYPERVISOR = 0
        // VMADDR_CID_LOCAL = 1
        // VMADDR_CID_HOST = 2
        // Note: 0 is used as a placeholder to auto-generate a CID.
        // <http://man7.org/linux/man-pages/man7/vsock.7.html>
        if enclave_cid == 0 {
            eprintln!("The enclave CID will be auto-generated as the provided CID is 0");
        }

        if enclave_cid > 0 && enclave_cid <= VMADDR_CID_HOST as u64 {
            return Err(new_nitro_cli_failure!(
                &format!(
                    "CID {} is a well-known CID, not to be used for enclaves",
                    enclave_cid
                ),
                NitroCliErrorEnum::InvalidArgument
            ));
        }

        if enclave_cid == u32::max_value() as u64 {
            return Err(new_nitro_cli_failure!(
                &format!(
                    "CID {} is a well-known CID, not to be used for enclaves",
                    enclave_cid
                ),
                NitroCliErrorEnum::InvalidArgument
            ));
        }

        // Do not use the CID of the parent VM as the enclave CID.
        if enclave_cid == VMADDR_CID_PARENT as u64 {
            return Err(new_nitro_cli_failure!(
                &format!(
                    "CID {} is the CID of the parent VM, not to be used for enclaves",
                    enclave_cid
                ),
                NitroCliErrorEnum::InvalidArgument
            ));
        }

        // 64-bit CIDs are not yet supported for the vsock device.
        if enclave_cid > u32::max_value() as u64 {
            return Err(new_nitro_cli_failure!(
                &format!(
                    "CID {} is higher than the maximum supported (u32 max) for a vsock device",
                    enclave_cid
                ),
                NitroCliErrorEnum::InvalidArgument
            ));
        }

        Some(enclave_cid)
    } else {
        None
    };

    Ok(enclave_cid)
}

/// Parse the enclave image file path from the command-line arguments.
fn parse_eif_path(args: &ArgMatches) -> NitroCliResult<String> {
    let eif_path = args.value_of("eif-path").ok_or_else(|| {
        new_nitro_cli_failure!(
            "`eif-path` argument not found",
            NitroCliErrorEnum::MissingArgument
        )
    })?;
    Ok(eif_path.to_string())
}

/// Parse the enclave's ID from the command-line arguments.
fn parse_enclave_id(args: &ArgMatches) -> NitroCliResult<String> {
    let enclave_id = args.value_of("enclave-id").ok_or_else(|| {
        new_nitro_cli_failure!(
            "`enclave-id` argument not found",
            NitroCliErrorEnum::MissingArgument
        )
    })?;
    Ok(enclave_id.to_string())
}

/// Parse the disconnect timeout from the command-line arguments.
fn parse_disconnect_timeout(args: &ArgMatches) -> NitroCliResult<Option<u64>> {
    let disconnect_timeout = match args.value_of("disconnect-timeout") {
        Some(arg) => Some(arg.parse::<u64>().map_err(|_| {
            new_nitro_cli_failure!(
                "`disconnect-timeout` argument can't be parsed as a number",
                NitroCliErrorEnum::InvalidArgument
            )
            .add_info(vec![
                "disconnect-timeout",
                args.value_of("disconnect-timeout").unwrap(),
            ])
        })?),
        None => None,
    };
    Ok(disconnect_timeout)
}

/// Parse the list of requested CPU IDs from the command-line arguments.
fn parse_cpu_ids(args: &ArgMatches) -> NitroCliResult<Option<Vec<u32>>> {
    let cpu_ids_arg = args.values_of("cpu-ids");
    match cpu_ids_arg {
        Some(iterator) => {
            let mut cpu_ids = Vec::new();
            for cpu_id in iterator {
                cpu_ids.push(cpu_id.parse().map_err(|_| {
                    new_nitro_cli_failure!(
                        "`cpu-id` is not a number",
                        NitroCliErrorEnum::InvalidArgument
                    )
                    .add_info(vec!["cpu-id", cpu_id])
                })?);
            }
            Ok(Some(cpu_ids))
        }
        None => Ok(None),
    }
}

/// Parse the requested number of CPUs from the command-line arguments.
fn parse_cpu_count(args: &ArgMatches) -> NitroCliResult<Option<u32>> {
    let cpu_count = if let Some(cpu_count) = args.value_of("cpu-count") {
        let cpu_count: u32 = cpu_count.parse().map_err(|_| {
            new_nitro_cli_failure!(
                "`cpu-count` is not a number",
                NitroCliErrorEnum::InvalidArgument
            )
            .add_info(vec!["cpu-count", cpu_count])
        })?;
        Some(cpu_count)
    } else {
        None
    };
    Ok(cpu_count)
}

/// Parse the path of an output file from the command-line arguments.
fn parse_output(args: &ArgMatches) -> Option<String> {
    args.value_of("output-file").map(|val| val.to_string())
}

/// Parse the debug-mode flag from the command-line arguments.
fn debug_mode(args: &ArgMatches) -> bool {
    args.is_present("debug-mode") || args.is_present("attach-console")
}

/// Parse the attach-console flag from the command-line arguments.
fn attach_console(args: &ArgMatches) -> bool {
    args.is_present("attach-console")
}

/// Parse the enclave name from the command-line arguments.
fn parse_enclave_name(args: &ArgMatches) -> NitroCliResult<Option<String>> {
    Ok(args.value_of("enclave-name").map(|e| e.to_string()))
}

fn parse_signing_certificate(args: &ArgMatches) -> Option<String> {
    args.value_of("signing-certificate")
        .map(|val| val.to_string())
}

fn parse_private_key(args: &ArgMatches) -> Option<String> {
    args.value_of("private-key").map(|val| val.to_string())
}

fn parse_image_name(args: &ArgMatches) -> Option<String> {
    args.value_of("image_name").map(|val| val.to_string())
}

fn parse_image_version(args: &ArgMatches) -> Option<String> {
    args.value_of("image_version").map(|val| val.to_string())
}

fn parse_metadata(args: &ArgMatches) -> Option<String> {
    args.value_of("metadata").map(|val| val.to_string())
}

fn parse_error_code_str(args: &ArgMatches) -> NitroCliResult<String> {
    let error_code_str = args.value_of("error-code").ok_or_else(|| {
        new_nitro_cli_failure!(
            "`error-code` argument not found",
            NitroCliErrorEnum::MissingArgument
        )
    })?;
    Ok(error_code_str.to_string())
}

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

    use crate::common::construct_error_message;
    use crate::create_app;

    use clap::{App, AppSettings, Arg, SubCommand};

    /// Parse the path of the JSON config file
    fn parse_config_file(args: &ArgMatches) -> NitroCliResult<String> {
        let config_file = args.value_of("config").ok_or(new_nitro_cli_failure!(
            "`config` argument not found",
            NitroCliErrorEnum::MissingArgument
        ))?;
        Ok(config_file.to_string())
    }

    #[test]
    fn test_parse_memory() {
        let app = create_app!();
        let args = vec![
            "nitro-cli",
            "run-enclave",
            "--memory",
            "256_mb",
            "--cpu-count",
            "2",
            "--eif-path",
            "non_existing_eif.eif",
        ];

        let matches = app.get_matches_from_safe(args);
        assert!(matches.is_ok());

        let result = parse_memory(
            matches
                .as_ref()
                .unwrap()
                .subcommand_matches("run-enclave")
                .unwrap(),
        );
        assert!(result.is_err());
        if let Err(err_info) = result {
            let err_str = construct_error_message(&err_info);
            assert!(err_str.contains("Invalid argument provided"))
        }
    }

    #[test]
    fn test_parse_docker_tag() {
        let app = create_app!();
        let args = vec![
            "nitro-cli",
            "build-enclave",
            "--docker-uri",
            "mytag",
            "--docker-dir",
            "/home/user/non_existing_dir",
            "--output-file",
            "sample_eif.eif",
        ];
        let matches = app.get_matches_from_safe(args);
        assert!(matches.is_ok());

        let result = parse_docker_tag(
            matches
                .as_ref()
                .unwrap()
                .subcommand_matches("build-enclave")
                .unwrap(),
        );
        assert!(result.is_some());
        assert_eq!(result.unwrap(), "mytag");
    }

    #[test]
    fn test_parse_docker_dir() {
        let app = create_app!();
        let args = vec![
            "nitro-cli",
            "build-enclave",
            "--docker-uri",
            "mytag",
            "--docker-dir",
            "/home/user/non_existing_dir",
            "--output-file",
            "sample_eif.eif",
        ];
        let matches = app.get_matches_from_safe(args);
        assert!(matches.is_ok());

        let result = parse_docker_dir(
            matches
                .as_ref()
                .unwrap()
                .subcommand_matches("build-enclave")
                .unwrap(),
        );
        assert!(result.is_some());
        assert_eq!(result.unwrap(), "/home/user/non_existing_dir");
    }

    #[test]
    fn test_parse_enclave_cid_correct() {
        let app = create_app!();
        let args = vec![
            "nitro-cli",
            "run-enclave",
            "--memory",
            "256",
            "--cpu-count",
            "2",
            "--eif-path",
            "non_existing_eif.eif",
            "--enclave-cid",
            "10",
        ];
        let matches = app.get_matches_from_safe(args);
        assert!(matches.is_ok());

        let result = parse_enclave_cid(
            matches
                .as_ref()
                .unwrap()
                .subcommand_matches("run-enclave")
                .unwrap(),
        );
        assert!(result.is_ok());

        if let Some(parsed_cid) = result.unwrap() {
            assert_eq!(parsed_cid, 10);
        }
    }

    #[test]
    fn test_parse_enclave_cid_to_be_autogenerated() {
        let app = create_app!();
        let args = vec![
            "nitro-cli",
            "run-enclave",
            "--memory",
            "256",
            "--cpu-count",
            "2",
            "--eif-path",
            "non_existing_eif.eif",
            "--enclave-cid",
            "0",
        ];
        let matches = app.get_matches_from_safe(args);
        assert!(matches.is_ok());

        let result = parse_enclave_cid(
            matches
                .as_ref()
                .unwrap()
                .subcommand_matches("run-enclave")
                .unwrap(),
        );
        assert!(result.is_ok());

        if let Some(parsed_cid) = result.unwrap() {
            assert_eq!(parsed_cid, 0);
        }
    }

    #[test]
    fn test_parse_enclave_cid_str() {
        let app = create_app!();
        let args = vec![
            "nitro-cli",
            "run-enclave",
            "--memory",
            "256",
            "--cpu-count",
            "2",
            "--eif-path",
            "non_existing_eif.eif",
            "--enclave-cid",
            "0x1g",
        ];
        let matches = app.get_matches_from_safe(args);
        assert!(matches.is_ok());

        let result = parse_enclave_cid(
            matches
                .as_ref()
                .unwrap()
                .subcommand_matches("run-enclave")
                .unwrap(),
        );
        assert!(result.is_err());
        if let Err(err_info) = result {
            let err_str = construct_error_message(&err_info);
            assert!(err_str.contains("Invalid argument provided"))
        }
    }

    #[test]
    fn test_parse_enclave_cid_well_known_cid_local() {
        let app = create_app!();
        let cid_local = VMADDR_CID_LOCAL.to_string();
        let args = vec![
            "nitro-cli",
            "run-enclave",
            "--memory",
            "256",
            "--cpu-count",
            "2",
            "--eif-path",
            "non_existing_eif.eif",
            "--enclave-cid",
            &cid_local,
        ];
        let matches = app.get_matches_from_safe(args);
        assert!(matches.is_ok());

        let result = parse_enclave_cid(
            matches
                .as_ref()
                .unwrap()
                .subcommand_matches("run-enclave")
                .unwrap(),
        );
        assert!(result.is_err());
        if let Err(err_info) = result {
            let err_str = construct_error_message(&err_info);
            assert!(err_str.contains("Invalid argument provided"));
        }
    }

    #[test]
    fn test_parse_enclave_cid_well_known_cid_host() {
        let app = create_app!();
        let cid_host = VMADDR_CID_HOST.to_string();
        let args = vec![
            "nitro-cli",
            "run-enclave",
            "--memory",
            "256",
            "--cpu-count",
            "2",
            "--eif-path",
            "non_existing_eif.eif",
            "--enclave-cid",
            &cid_host,
        ];
        let matches = app.get_matches_from_safe(args);
        assert!(matches.is_ok());

        let result = parse_enclave_cid(
            matches
                .as_ref()
                .unwrap()
                .subcommand_matches("run-enclave")
                .unwrap(),
        );
        assert!(result.is_err());
        if let Err(err_info) = result {
            let err_str = construct_error_message(&err_info);
            assert!(err_str.contains("Invalid argument provided"));
        }
    }

    #[test]
    fn test_parse_enclave_cid_parent_vm() {
        let app = create_app!();
        let parent_vm_cid = VMADDR_CID_PARENT.to_string();
        let args = vec![
            "nitro-cli",
            "run-enclave",
            "--memory",
            "256",
            "--cpu-count",
            "2",
            "--eif-path",
            "non_existing_eif.eif",
            "--enclave-cid",
            &parent_vm_cid,
        ];
        let matches = app.get_matches_from_safe(args);
        assert!(matches.is_ok());

        let result = parse_enclave_cid(
            matches
                .as_ref()
                .unwrap()
                .subcommand_matches("run-enclave")
                .unwrap(),
        );
        assert!(result.is_err());
        if let Err(err_info) = result {
            let err_str = construct_error_message(&err_info);
            assert!(err_str.contains("Invalid argument provided"));
        }
    }

    #[test]
    fn test_parse_enclave_cid_negative() {
        let app = create_app!();
        let args = vec![
            "nitro-cli",
            "run-enclave",
            "--memory",
            "256",
            "--cpu-count",
            "2",
            "--eif-path",
            "non_existing_eif.eif",
            "--enclave-cid",
            "-18",
        ];
        let matches = app.get_matches_from_safe(args);
        // Error (got unexpected value ["-1"])
        assert!(matches.is_err());
    }

    #[test]
    fn test_parse_eif_path() {
        let app = create_app!();
        let args = vec![
            "nitro-cli",
            "run-enclave",
            "--memory",
            "256",
            "--cpu-count",
            "2",
            "--eif-path",
            "non_existing_eif.eif",
        ];
        let matches = app.get_matches_from_safe(args);
        assert!(matches.is_ok());

        let result = parse_eif_path(
            matches
                .as_ref()
                .unwrap()
                .subcommand_matches("run-enclave")
                .unwrap(),
        );
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), "non_existing_eif.eif");
    }

    #[test]
    fn test_parse_enclave_id() {
        let app = create_app!();
        let args = vec![
            "nitro-cli",
            "terminate-enclave",
            "--enclave-id",
            "i-0000-enc-1234",
        ];
        let matches = app.get_matches_from_safe(args);
        assert!(matches.is_ok());

        let result = parse_enclave_id(
            matches
                .as_ref()
                .unwrap()
                .subcommand_matches("terminate-enclave")
                .unwrap(),
        );
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), "i-0000-enc-1234");
    }

    #[test]
    fn test_parse_cpu_ids_correct() {
        let app = create_app!();
        let args = vec![
            "nitro-cli",
            "run-enclave",
            "--cpu-ids",
            "1",
            "3",
            "--eif-path",
            "non_existing_eif.eif",
            "--memory",
            "64",
        ];
        let matches = app.get_matches_from_safe(args);
        assert!(matches.is_ok());

        let result = parse_cpu_ids(
            matches
                .as_ref()
                .unwrap()
                .subcommand_matches("run-enclave")
                .unwrap(),
        );
        assert!(result.is_ok());

        if let Some(parsed_cpu_ids) = result.unwrap() {
            assert_eq!(parsed_cpu_ids.len(), 2);
            assert_eq!(parsed_cpu_ids[0], 1);
            assert_eq!(parsed_cpu_ids[1], 3);
        }
    }

    #[test]
    fn test_parse_cpu_ids_negative() {
        let app = create_app!();
        let args = vec![
            "nitro-cli",
            "run-enclave",
            "--cpu-ids",
            "1",
            "-5",
            "--eif-path",
            "non_existing_eif.eif",
            "--memory 64",
        ];
        let matches = app.get_matches_from_safe(args);
        // Error (got unexpected value ["-5"])
        assert!(matches.is_err());
    }

    #[test]
    fn test_parse_cpu_ids_str() {
        let app = create_app!();
        let args = vec![
            "nitro-cli",
            "run-enclave",
            "--cpu-ids",
            "1",
            "three",
            "--eif-path",
            "non_existing_eif.eif",
            "--memory",
            "64",
        ];
        let matches = app.get_matches_from_safe(args);
        assert!(matches.is_ok());

        let result = parse_cpu_ids(
            matches
                .as_ref()
                .unwrap()
                .subcommand_matches("run-enclave")
                .unwrap(),
        );
        assert!(result.is_err());
        if let Err(err_info) = result {
            let err_str = construct_error_message(&err_info);
            assert!(err_str.contains("Invalid argument provided"));
        }
    }

    #[test]
    fn test_parse_cpu_count_correct() {
        let app = create_app!();
        let args = vec![
            "nitro-cli",
            "run-enclave",
            "--cpu-count",
            "2",
            "--eif-path",
            "non_existing_eif.eif",
            "--memory",
            "64",
        ];
        let matches = app.get_matches_from_safe(args);
        assert!(matches.is_ok());

        let result = parse_cpu_count(
            matches
                .as_ref()
                .unwrap()
                .subcommand_matches("run-enclave")
                .unwrap(),
        );
        assert!(result.is_ok());

        if let Some(parsed_cpu_count) = result.unwrap() {
            assert_eq!(parsed_cpu_count, 2);
        }
    }

    #[test]
    fn test_parse_cpu_count_str() {
        let app = create_app!();
        let args = vec![
            "nitro-cli",
            "run-enclave",
            "--cpu-count",
            "2n",
            "--eif-path",
            "non_existing_eif.eif",
            "--memory",
            "64",
        ];
        let matches = app.get_matches_from_safe(args);
        assert!(matches.is_ok());

        let result = parse_cpu_count(
            matches
                .as_ref()
                .unwrap()
                .subcommand_matches("run-enclave")
                .unwrap(),
        );
        assert!(result.is_err());
        if let Err(err_info) = result {
            let err_str = construct_error_message(&err_info);
            assert!(err_str.contains("Invalid argument provided"));
        }
    }

    #[test]
    fn test_parse_output() {
        let app = create_app!();
        let args = vec![
            "nitro-cli",
            "build-enclave",
            "--docker-uri",
            "mytag",
            "--docker-dir",
            "/home/user/non_existing_dir",
            "--output-file",
            "sample_eif.eif",
        ];
        let matches = app.get_matches_from_safe(args);
        assert!(matches.is_ok());

        let result = parse_output(
            matches
                .as_ref()
                .unwrap()
                .subcommand_matches("build-enclave")
                .unwrap(),
        );
        assert!(result.is_some());
        assert_eq!(result.unwrap(), "sample_eif.eif");
    }

    #[test]
    fn test_parse_output_not_supplied() {
        let app = create_app!();
        let args = vec![
            "nitro-cli",
            "build-enclave",
            "--docker-uri",
            "mytag",
            "--docker-dir",
            "/home/user/non_existing_dir",
        ];
        let matches = app.get_matches_from_safe(args);
        // Error (the following required argument were not supplied)
        assert!(matches.is_err());
    }

    #[test]
    fn test_debug_mode_supplied() {
        let app = create_app!();
        let args = vec![
            "nitro-cli",
            "run-enclave",
            "--memory",
            "64",
            "--cpu-count",
            "2",
            "--eif-path",
            "non_existing_eif.eif",
            "--debug-mode",
        ];
        let matches = app.get_matches_from_safe(args);
        assert!(matches.is_ok());

        let result = debug_mode(
            matches
                .as_ref()
                .unwrap()
                .subcommand_matches("run-enclave")
                .unwrap(),
        );
        assert!(result);
    }

    #[test]
    fn test_debug_mode_not_supplied() {
        let app = create_app!();
        let args = vec![
            "nitro-cli",
            "run-enclave",
            "--memory",
            "64",
            "--cpu-count",
            "2",
            "--eif-path",
            "non_existing_eif.eif",
        ];
        let matches = app.get_matches_from_safe(args);
        assert!(matches.is_ok());

        let result = debug_mode(
            matches
                .as_ref()
                .unwrap()
                .subcommand_matches("run-enclave")
                .unwrap(),
        );
        assert!(!result);
    }

    #[test]
    fn test_attach_console_supplied() {
        let app = create_app!();
        let args = vec![
            "nitro-cli",
            "run-enclave",
            "--attach-console",
            "--memory",
            "64",
            "--cpu-count",
            "2",
            "--eif-path",
            "non_existing_eif.eif",
        ];
        let matches = app.get_matches_from_safe(args);
        assert!(matches.is_ok());

        let matches = matches
            .as_ref()
            .unwrap()
            .subcommand_matches("run-enclave")
            .unwrap();
        let attach_console = attach_console(matches);
        let debug_mode = debug_mode(matches);
        assert!(attach_console);
        assert!(debug_mode);
    }

    #[test]
    fn test_parse_json_config() {
        let app = create_app!();
        let args = vec![
            "nitro-cli",
            "run-enclave",
            "--config",
            "non_existing_config.json",
        ];
        let matches = app.get_matches_from_safe(args);
        assert!(matches.is_ok());

        let result = parse_config_file(
            matches
                .as_ref()
                .unwrap()
                .subcommand_matches("run-enclave")
                .unwrap(),
        );
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), "non_existing_config.json");
    }
}


use lazy_static::lazy_static;

use std::collections::HashMap;

use crate::common::{NitroCliErrorEnum, NitroCliFailure};

lazy_static! {
    /// Structure mapping enum Errors to a specific error code.
    pub static ref ERROR_CODES: HashMap<NitroCliErrorEnum, &'static str> =
        [
            (NitroCliErrorEnum::UnspecifiedError, "E00"),
            (NitroCliErrorEnum::MissingArgument, "E01"),
            (NitroCliErrorEnum::ConflictingArgument, "E02"),
            (NitroCliErrorEnum::InvalidArgument, "E03"),
            (NitroCliErrorEnum::SocketPairCreationFailure, "E04"),
            (NitroCliErrorEnum::ProcessSpawnFailure, "E05"),
            (NitroCliErrorEnum::DaemonizeProcessFailure, "E06"),
            (NitroCliErrorEnum::ReadFromDiskFailure, "E07"),
            (NitroCliErrorEnum::UnusableConnectionError, "E08"),
            (NitroCliErrorEnum::SocketCloseError, "E09"),
            (NitroCliErrorEnum::SocketConnectTimeoutError, "E10"),
            (NitroCliErrorEnum::SocketError, "E11"),
            (NitroCliErrorEnum::EpollError, "E12"),
            (NitroCliErrorEnum::InotifyError, "E13"),
            (NitroCliErrorEnum::InvalidCommand, "E14"),
            (NitroCliErrorEnum::LockAcquireFailure, "E15"),
            (NitroCliErrorEnum::ThreadJoinFailure, "E16"),
            (NitroCliErrorEnum::SerdeError, "E17"),
            (NitroCliErrorEnum::FilePermissionsError, "E18"),
            (NitroCliErrorEnum::FileOperationFailure, "E19"),
            (NitroCliErrorEnum::InvalidCpuConfiguration, "E20"),
            (NitroCliErrorEnum::NoSuchCpuAvailableInPool, "E21"),
            (NitroCliErrorEnum::InsufficientCpus, "E22"),
            (NitroCliErrorEnum::MalformedCpuId, "E23"),
            (NitroCliErrorEnum::CpuError, "E24"),
            (NitroCliErrorEnum::NoSuchHugepageFlag, "E25"),
            (NitroCliErrorEnum::InsufficientMemoryRequested, "E26"),
            (NitroCliErrorEnum::InsufficientMemoryAvailable, "E27"),
            (NitroCliErrorEnum::InvalidEnclaveFd, "E28"),
            (NitroCliErrorEnum::IoctlFailure, "E29"),
            (NitroCliErrorEnum::IoctlImageLoadInfoFailure, "E30"),
            (NitroCliErrorEnum::IoctlSetMemoryRegionFailure, "E31"),
            (NitroCliErrorEnum::IoctlAddVcpuFailure, "E32"),
            (NitroCliErrorEnum::IoctlEnclaveStartFailure, "E33"),
            (NitroCliErrorEnum::MemoryOverflow, "E34"),
            (NitroCliErrorEnum::EifParsingError, "E35"),
            (NitroCliErrorEnum::EnclaveBootFailure, "E36"),
            (NitroCliErrorEnum::EnclaveEventWaitError, "E37"),
            (NitroCliErrorEnum::EnclaveProcessCommandNotExecuted, "E38"),
            (NitroCliErrorEnum::EnclaveProcessConnectionFailure, "E39"),
            (NitroCliErrorEnum::SocketPathNotFound, "E40"),
            (NitroCliErrorEnum::EnclaveProcessSendReplyFailure, "E41"),
            (NitroCliErrorEnum::EnclaveMmapError, "E42"),
            (NitroCliErrorEnum::EnclaveMunmapError, "E43"),
            (NitroCliErrorEnum::EnclaveConsoleConnectionFailure, "E44"),
            (NitroCliErrorEnum::EnclaveConsoleReadError, "E45"),
            (NitroCliErrorEnum::EnclaveConsoleWriteOutputError, "E46"),
            (NitroCliErrorEnum::IntegerParsingError, "E47"),
            (NitroCliErrorEnum::EifBuildingError, "E48"),
            (NitroCliErrorEnum::DockerImageBuildError, "E49"),
            (NitroCliErrorEnum::DockerImagePullError, "E50"),
            (NitroCliErrorEnum::ArtifactsPathNotSet, "E51"),
            (NitroCliErrorEnum::BlobsPathNotSet, "E52"),
            (NitroCliErrorEnum::ClockSkewError, "E53"),
            (NitroCliErrorEnum::SignalMaskingError, "E54"),
            (NitroCliErrorEnum::SignalUnmaskingError, "E55"),
            (NitroCliErrorEnum::LoggerError, "E56"),
            (NitroCliErrorEnum::HasherError, "E57"),
            (NitroCliErrorEnum::EnclaveNamingError, "E58"),
            (NitroCliErrorEnum::EIFSignatureCheckerError, "E59"),
        ].iter().cloned().collect();
}

/// Returns detailed error information based on supplied arguments.
pub fn get_detailed_info(error_code_str: String, additional_info: &[String]) -> String {
    let mut ret = format!("[ {} ] ", error_code_str);
    let info_placeholder = "MISSING_INFO".to_string();

    match error_code_str.as_str() {
        "E00" => {
            ret.push_str("Unspecified error. This is used as a catch-all error and should not be used in the code.");
        }
        "E01" => {
            ret.push_str(
                format!(
                    "Missing mandatory argument. User did not provide the `{}` argument.",
                    additional_info.get(0).unwrap_or(&info_placeholder)
                )
                .as_str(),
            );
        }
        "E02" => {
            ret.push_str(
                format!(
                    "Conflicting arguments. User provided both `{}` and `{}`.",
                    additional_info.get(0).unwrap_or(&info_placeholder),
                    additional_info.get(1).unwrap_or(&info_placeholder)
                )
                .as_str(),
            );
        }
        "E03" => {
            ret.push_str(
                format!(
                    "Invalid argument provided. The parameter `{}` is not a valid integer (`{}`)",
                    additional_info.get(0).unwrap_or(&info_placeholder),
                    additional_info.get(1).unwrap_or(&info_placeholder)
                )
                .as_str(),
            );
        }
        "E04" => {
            ret.push_str("Socket pair creation failure. Such error appears when the Nitro CLI process attempts to open a stream pair in order to send a command to the enclave process but the stream initialization fails.");
        }
        "E05" => {
            ret.push_str("Process spawn failure. Such error appears when the main Nitro CLI process fails to spawn the enclave process, in order to complete a `run-enclave` command.");
        }
        "E06" => {
            ret.push_str("Daemonize process failure. Such error appears when the system fails to daemonize a newly spawned enclave process.");
        }
        "E07" => {
            ret.push_str("Read from disk failure. Such error appears when the Nitro CLI process fails to read the content of the enclave sockets directory (usually '/run/nitro_enclaves/') in order to perform a `describe-enclave` operation. Check that the directory exists and it has proper permissions, or run the Nitro Enclaves configuration script in order to (re)configure the environment.");
        }
        "E08" => {
            ret.push_str("Unusable connection error. Such error appears when the Nitro CLI process attempts to open a connection to a non-existing or previously closed enclave descriptor");
        }
        "E09" => {
            ret.push_str("Socket close error. Such error appears when the system fails to successfully close a communication channel.");
        }
        "E10" => {
            ret.push_str("Socket connect set timeout error. Such error appears when the system fails to configure a specific timeout for a given socket. May arise when trying to connect to an enclave's console.");
        }
        "E11" => {
            ret.push_str("Socket error. This is used as an error for catching any other socket operation errors not covered by previous custom errors.");
        }
        "E12" => {
            ret.push_str("Epoll error. Such error appears, for instance, when the system fails to register a specific enclave descriptor with epoll in order to monitor events for it.");
        }
        "E13" => {
            ret.push_str("Inotify error. Such error appears when the system fails to configure a socket for monitorization.");
        }
        "E14" => {
            ret.push_str("Invalid command. Such error appears when an unknown command and / or unknown arguments are sent through a socket.");
        }
        "E15" => {
            ret.push_str("Lock acquire failure. Such error appears when the system fails to obtain the lock for an object with concurrent access, such as a structure containing information about a running enclave.");
        }
        "E16" => {
            ret.push_str("Thread join failure. Such error appears when the system fails to successfully join a thread, after it finished executing.");
        }
        "E17" => {
            ret.push_str("Serde error. Such error appears when serializing / deserializing a command or response fails.");
        }
        "E18" => {
            ret.push_str("File permissions error. Such error appears when a user other than the owner of the logging file (usually '/var/log/nitro_enclaves/nitro_enclaves.log') attempts to change the file permissions");
        }
        "E19" => {
            ret.push_str("File operation failure. Such error appears when the system fails to perform the requested file operations, such as opening the EIF file when launching an enclave, or seeking to a specific offset in the EIF file, or writing to the log file.");
            if additional_info.len() >= 2 {
                ret.push_str(
                    format!(
                        "\nFile: '{}', failing operation: '{}'.",
                        additional_info.get(0).unwrap_or(&info_placeholder),
                        additional_info.get(1).unwrap_or(&info_placeholder),
                    )
                    .as_str(),
                );
            }
        }
        "E20" => {
            ret.push_str(
                format!(
                    "Invalid CPU configuration. User provided `{}` contains same CPU(s) (CPU(s) {}) multiple times.",
                    additional_info.get(0).unwrap_or(&info_placeholder),
                    additional_info.get(1).unwrap_or(&info_placeholder),
                )
                .as_str(),
            );
        }
        "E21" => {
            ret.push_str(
                format!(
                    "No such CPU available in the pool. User provided `{}` contains CPU {}, which is not available in the pool.\nYou can add a specific CPU to the CPU pool by editing the `cpu_pool` value from '/etc/nitro_enclaves/allocator.yaml' and then enable the nitro-enclaves-allocator.service.",
                    additional_info.get(0).unwrap_or(&info_placeholder),
                    additional_info.get(1).unwrap_or(&info_placeholder),
                ).as_str(),
            );
        }
        "E22" => {
            ret.push_str(
                format!(
                    "Insufficient CPUs available in the pool. User provided `{}` is {}, which is more than the configured CPU pool size.\nYou can increase the CPU pool size by editing the `cpu_count` value from '/etc/nitro_enclaves/allocator.yaml' and then enable the nitro-enclaves-allocator.service.",
                    additional_info.get(0).unwrap_or(&info_placeholder),
                    additional_info.get(1).unwrap_or(&info_placeholder),
                ).as_str(),
            );
        }
        "E23" => {
            ret.push_str("Malformed CPU ID error. Such error appears when a `lscpu` line is malformed and reports an invalid online CPUs list.");
        }
        "E24" => {
            ret.push_str("CPU error. Such error appears when a CPU line interval is invalid (for instance, 0-3-7)");
        }
        "E25" => {
            ret.push_str("No such hugepage flag error. Such error appears when the enclave process attempts to use an invalid hugepage size (size other than the known hugepage sizes) for initializing the enclave memory.");
        }
        "E26" => {
            if additional_info.len() >= 3 {
                ret.push_str(
                    format!(
                        "Insufficient memory requested. User provided `{}` is {} MB, but based on the EIF file size, the minimum memory should be {} MB",
                        additional_info.get(0).unwrap_or(&info_placeholder),
                        additional_info.get(1).unwrap_or(&info_placeholder),
                        additional_info.get(2).unwrap_or(&info_placeholder)
                    ).as_str(),
                );
            } else {
                ret.push_str(
                    format!(
                        "Insufficient memory requested. User provided `{}` is {} MB, and memory should be greater than 0 MB.",
                        additional_info.get(0).unwrap_or(&info_placeholder),
                        additional_info.get(1).unwrap_or(&info_placeholder)
                    ).as_str(),
                );
            }
        }
        "E27" => {
            ret.push_str(
                format!(
                    "Insufficient memory available. User provided `{}` is {} MB, which is more than the available hugepage memory.\nYou can increase the available memory by editing the `memory_mib` value from '/etc/nitro_enclaves/allocator.yaml' and then restart the nitro-enclaves-allocator.service.",
                    additional_info.get(0).unwrap_or(&info_placeholder),
                    additional_info.get(1).unwrap_or(&info_placeholder)
                ).as_str(),
            );
        }
        "E28" => {
            ret.push_str("Invalid enclave descriptor. Such error appears when the NE_CREATE_VM ioctl returns with an error.");
        }
        "E29" => {
            ret.push_str("Ioctl failure. Such error is used as a general ioctl error and appears whenever an ioctl fails. In this case, the error backtrace provides detailed information on what specifically failed during the ioctl.");
        }
        "E30" => {
            ret.push_str("Ioctl image get load info failure. Such error appears when the ioctl used for getting the memory load information fails. In this case, the error backtrace provides detailed information on what specifically failed during the ioctl.");
        }
        "E31" => {
            ret.push_str("Ioctl set memory region failure. Such error appears when the ioctl used for setting a given memory region fails. In this case, the error backtrace provides detailed information on what specifically failed during the ioctl.");
        }
        "E32" => {
            ret.push_str("Ioctl add vCPU failure. Such error appears when the ioctl used for adding a vCPU fails. In this case, the error backtrace provides detailed information on what specifically failed during the ioctl.");
        }
        "E33" => {
            ret.push_str("Ioctl start enclave failure. Such error appears when the ioctl used for starting an enclave fails. In this case, the error backtrace provides details information on what specifically failed during the ioctl.");
        }
        "E34" => {
            ret.push_str("Memory overflow. Such error may appear during loading the EIF in the memory regions which will be conceded to the future enclave, if the regions offset plus the EIF file size exceeds the maximum address of the target platform.");
        }
        "E35" => {
            ret.push_str("EIF file parsing error. Such errors appear when attempting to fill a memory region with a section of the EIF file, but reading the entire section fails. This might indicate that the required hugepages are not available.");
        }
        "E36" => {
            ret.push_str("Enclave boot failure. Such error appears when attempting to receive the `ready` signal from a freshly booted enclave. It arises in several contexts, for instance, when the enclave is booted from an invalid EIF file and the enclave process immediately exits, failing to submit the `ready` signal. In this case, the error backtrace provides detailed information on what specifically failed during the enclave boot process.");
        }
        "E37" => {
            ret.push_str("Enclave event wait error. Such error appears when monitoring an enclave descriptor for events fails.");
        }
        "E38" => {
            ret.push_str("Enclave process command not executed error. Such error appears when at least one enclave fails to provide the description information.");
        }
        "E39" => {
            ret.push_str("Enclave process connection failure. Such error appears when the enclave manager fails to connect to at least one enclave process for retrieving the description information.");
        }
        "E40" => {
            ret.push_str("Socket path not found. Such error appears when the Nitro CLI process fails to build the corresponding socket path starting from a given enclave ID.");
        }
        "E41" => {
            ret.push_str("Enclave process send reply failure. Such error appears when the enclave process fails to submit the status code to the Nitro CLI process after performing a run / describe / terminate command.");
        }
        "E42" => {
            ret.push_str(
                "Enclave mmap error. Such error appears when allocating the enclave memory fails.",
            );
        }
        "E43" => {
            ret.push_str(
                "Enclave munmap error. Such error appears when unmapping the enclave memory fails.",
            );
        }
        "E44" => {
            ret.push_str("Enclave console connection failure. Such error appears when the Nitro CLI process fails to establish a connection to a running enclave's console.");
        }
        "E45" => {
            ret.push_str("Enclave console read error. Such error appears when reading from a running enclave's console fails.");
        }
        "E46" => {
            ret.push_str("Enclave console write output error. Such error appears when writing the information retrieved from a running enclave's console (to a given stream) fails.");
        }
        "E47" => {
            ret.push_str("Integer parsing error. Such error appears when trying to connect to a running enclave's console, but the enclave CID cannot be parsed correctly.");
        }
        "E48" => {
            ret.push_str("EIF building error. Such error appears when trying to build an EIF file. In this case, the error backtrace provides detailed information on the failure reason.");
        }
        "E49" => {
            ret.push_str("Docker image build error. Such error appears when trying to build and EIF file, but building the corresponding docker image fails. In this case, the error backtrace provides detailed information on the failure reason.");
        }
        "E50" => {
            ret.push_str("Docker image pull error. Such error appears when trying to build an EIF file, but pulling the corresponding docker image fails. In this case, the error backtrace provides detailed informatino on the failure reason.");
        }
        "E51" => {
            ret.push_str("Artifacts path environment variable not set. Such error appears when trying to build an EIF file, but the artifacts path environment variable is not set.");
        }
        "E52" => {
            ret.push_str("Blobs path environment variable not set. Such error appears when trying to build an EIF file, but the blobs path environment variable is not set.");
        }
        "E53" => {
            ret.push_str("Clock skew error. Such error appears when continuously reading from a running enclave's console, but measuring the time elapsed between consecutive reads failed.");
        }
        "E54" => {
            ret.push_str("Signal masking error. Such error appears if attempting to mask specific signals before creating an enclave process fails.");
        }
        "E55" => {
            ret.push_str("Signal unmasking error. Such error appears if attempting to unmask specific signals after creating an enclave process fails.");
        }
        "E56" => {
            ret.push_str("Logger error. Such error appears when attempting to initialize the underlying logging system fails.");
        }
        "E57" => {
            ret.push_str("Hasher error. Such error appears when trying to initialize a hasher or write bytes to it, resulting in a IO error.");
        }
        "E58" => {
            ret.push_str("Naming error. Such error appears when trying to perform an enclave operation using the enclave name and the name is invalid.");
        }
        "E59" => {
            ret.push_str("EIF signature checker error. Such error appears when validation of the signing certificate fails.");
        }
        _ => {
            ret.push_str(format!("No such error code {}", error_code_str).as_str());
        }
    }

    ret
}

/// Returns a link with more detailed information regarding a specific error.
pub fn construct_help_link(error_code_str: String) -> String {
    format!(
        "https://docs.aws.amazon.com/enclaves/latest/user/cli-errors.html#{}",
        error_code_str
    )
}

/// Returns a string containing the backtrace recorded during propagating an error message
pub fn construct_backtrace(failure_info: &NitroCliFailure) -> String {
    let version = env!("CARGO_PKG_VERSION").to_string();

    format!("  Action: {}\n  Subactions:{}\n  Root error file: {}\n  Root error line: {}\n  Version: {}",
        failure_info.action,
        failure_info.subactions.iter().rev().fold("".to_string(), |acc, x| {
            format!("{}\n    {}", acc, x)
        }),
        failure_info.file,
        failure_info.line,
        version)
}

/// Detailed information based on user-provided error code.
pub fn explain_error(error_code_str: String) {
    match error_code_str.as_str() {
        "E00" => {
            eprintln!("Unspecified error. This is used as a catch-all error and should not be used in the code.");
        }
        "E01" => {
            eprintln!("Missing mandatory argument. Such error appears when the Nitro CLI is requested to perform an operation, but not all of the mandatory arguments were supplied.\n\tExample: `nitro-cli run-enclave --cpu-count 2 --eif-path /path/to/my/eif`. Note that in this case, the mandatory parameter `--memory` is missing a value.");
        }
        "E02" => {
            eprintln!("CLI conflicting arguments. Such error appears when the Nitro CLI is supplied two contradicting arguments at the same time, such as `--cpu-count` and `--cpu-ids`.\nIn this case, only one of the parameters should be supplied.");
        }
        "E03" => {
            eprintln!("Invalid argument provided. Such error appears when the type of at least one of the arguments provided to the Nitro CLI does not match the expected type of that parameter.\n\tExample: `nitro-cli run-enclave --cpu-count 1z --memory 80 --eif-path /path/to/my/eif`. In this case, `cpu-count` is not a valid integer value." );
        }
        "E04" => {
            eprintln!("Socket pair creation failure. Such error apears when the Nitro CLI process attempts to open a stream pair in order to send a command to the enclave process, but the stream initialization fails.");
        }
        "E05" => {
            eprintln!("Process spawn failure. Such error appears when the main Nitro CLI process failed to spawn the enclave process, in order to complete a `run-enclave` command.");
        }
        "E06" => {
            eprintln!("Daemonize process failure. Such error appears when the system fails to daemonize the newly spawned enclave process.")
        }
        "E07" => {
            eprintln!("Read from disk failure. Such error appears when the Nitro CLI process fails to read the content of the enclave sockets directory (usually '/run/nitro_enclaves/') in order to perform a `describe-enclave` operation. Check that the directory exists and it has proper permissions, or run the Nitro Enclaves configuration script in order to (re)configure the environment.");
        }
        "E08" => {
            eprintln!("Unusable connection error. Such error appears when the Nitro CLI process attempts to open a connection to a non-existing or previously closed enclave descriptor");
        }
        "E09" => {
            eprintln!("Socket close error. Such error appears when the system fails to successfully close a communication channel.");
        }
        "E10" => {
            eprintln!("Socket connect set timeout error. Such error appears when the system fails to configure a specific timeout for a given socket. May arise when trying to connect to an enclave's console.");
        }
        "E11" => {
            eprintln!("Socket error. This is used as an error for catching any other socket operation errors not covered by previous custom errors.");
        }
        "E12" => {
            eprintln!("Epoll error. Such error appears, for instance, when the system fails to register a specific enclave descriptor with epoll in order to monitor events for it.");
        }
        "E13" => {
            eprintln!("Inotify error. Such error appears when the system fails to configure a socket for monitorization.");
        }
        "E14" => {
            eprintln!("Invalid command. Such error appears when an unknown command and / or unknown arguments are sent through a socket.");
        }
        "E15" => {
            eprintln!("Lock acquire failure. Such error appears when the system fails to obtain the lock for an object with concurrent access, such as a structure containing information about a running enclave.");
        }
        "E16" => {
            eprintln!("Thread join failure. Such error appears when the system fails to successfully join a thread, after it finished executing.");
        }
        "E17" => {
            eprintln!("Serde error. Such error appears when serializing / deserializing a command or response fails.");
        }
        "E18" => {
            eprintln!("File permissions error. Such error appears when a user other than the owner of the logging file (usually '/var/log/nitro_enclaves/nitro_enclaves.log') attempts to change the file permissions");
        }
        "E19" => {
            eprintln!("File operation failure. Such error appears when the system fails to perform the requested file operations, such as opening the EIF file when launching an enclave, or seeking to a specific offset in the EIF file, or writing to the log file.");
        }
        "E20" => {
            eprintln!("Invalid CPU configuration. Such error appears when the user supplies the same CPU ID multiple times.\n\tExample: `nitro-cli run-enclave --cpu-ids 1 1 --memory 80 --eif-path /path/to/my/eif`. In this case, CPU ID `1` has been selected twice.");
        }
        "E21" => {
            eprintln!("No such CPU available in the pool. Such error appears when the user requests to run an enclave with at least one CPU ID which does not exist in the CPU pool.\n\tExample: (configured CPU pool: [1,9]) `nitro-cli run-enclave --cpu-ids 1 2 --memory 80 --eif-path /path/to/my/eif`. In this case, CPU 2 is not in the configured CPU pool.");
        }
        "E22" => {
            eprintln!("Insufficient CPUs available in the pool. Such error appears when the user requests to run an enclave with more CPUs that available in the CPU pool.\n\tExample: (configured CPU pool: [1,9]) `nitro-cli run-enclave --cpu-count 4 --memory 80 --eif-path /path/to/my/eif`. In this case, the user requested 4 CPUs, but the CPU pool contains only 2.");
        }
        "E23" => {
            eprintln!("Malformed CPU ID error. Such error appears when a `lscpu` line is malformed and reports an invalid online CPUs list.");
        }
        "E24" => {
            eprintln!(
                "CPU error. Such error appears when a CPU line interval is invalid (as in 0-3-7)"
            );
        }
        "E25" => {
            eprintln!("No such hugepage flag error. Such error appears when the enclave process attempts to use an invalid hugepage size (size other than the known hugepage sizes) for initializing the enclave memory.");
        }
        "E26" => {
            eprintln!("Insufficient memory requested. Such error appears when the user requests to launch an enclave with not enough memory. The enclave memory should be at least equal to the size of the EIF file used for launching the enclave.\n\tExample: (EIF file size: 11MB) `nitro-cli run-enclave --cpu-count 2 --memory 5 --eif-path /path/to/my/eif`. In this case, the user requested to run an enclave with only 5MB of memory, whereas the EIF file alone requires 11MB.");
        }
        "E27" => {
            eprintln!("Insufficient memory available. Such error appears when the user requests to launch an enclave with more memory than available. The enclave memory should be at most equal to the size of the configured hugepage memory.\n\tExample: (previously configured 80MB of hugepage memory) `nitro-cli run-enclave --cpu-count 2 --memory 100 --eif-path /path/to/my/eif`. In this case, the user requested to run an enclave with 100MB of memory, whereas the system has only 80MB available for enclaves. As a solution, (re)configure the Nitro Enclaves environment, specifying a higher value for the available memory.");
        }
        "E28" => {
            eprintln!("Invalid enclave descriptor. Such error appears when the NE_CREATE_VM ioctl returns with an error.");
        }
        "E29" => {
            eprintln!("Ioctl failure. Such error is used as a general ioctl error and appears whenever an ioctl fails. In this case, the error backtrace provides detailed information on what specifically failed during the ioctl.");
        }
        "E30" => {
            eprintln!("Ioctl image get load info failure. Such error appears when the ioctl used for getting the memory load information fails. In this case, the error backtrace provides detailed information on what specifically failed during the ioctl.");
        }
        "E31" => {
            eprintln!("Ioctl set memory region failure. Such error appears when the ioctl used for setting a given memory region fails. In this case, the error backtrace provides detailed information on what specifically failed during the ioctl.");
        }
        "E32" => {
            eprintln!("Ioctl add vCPU failure. Such error appears when the ioctl used for adding a vCPU fails. In this case, the error backtrace provides detailed information on what specifically failed during the ioctl.");
        }
        "E33" => {
            eprintln!("Ioctl start enclave failure. Such error appears when the ioctl used for starting an enclave fails. In this case, the error backtrace provides details information on what specifically failed during the ioctl.");
        }
        "E34" => {
            eprintln!("Memory overflow. Such error may appear during loading the EIF in the memory regions which will be conceded to the future enclave, if the regions offset plus the EIF file size exceeds the maximum address of the target platform.");
        }
        "E35" => {
            eprintln!("EIF file parsing error. Such errors appear when attempting to fill a memory region with a section of the EIF file, but reading the entire section fails. This might indicate that the required hugepages are not available.");
        }
        "E36" => {
            eprintln!("Enclave boot failure. Such error appears when attempting to receive the `ready` signal from a freshly booted enclave. It arises in several contexts, for instance, when the enclave is booted from an invalid EIF file and the enclave process immediately exits, failing to submit the `ready` signal. In this case, the error backtrace provides detailed information on what specifically failed during the enclave boot process.");
        }
        "E37" => {
            eprintln!("Enclave event wait error. Such error appears when monitoring an enclave descriptor for events fails.");
        }
        "E38" => {
            eprintln!("Enclave process command not executed error. Such error appears when at least one enclave fails to provide the description information.");
        }
        "E39" => {
            eprintln!("Enclave process connection failure. Such error appears when the enclave manager fails to connect to at least one enclave process for retrieving the description information.");
        }
        "E40" => {
            eprintln!("Socket path not found. Such error appears when the Nitro CLI process fails to build the corresponding socket path starting from a given enclave ID.");
        }
        "E41" => {
            eprintln!("Enclave process send reply failure. Such error appears when the enclave process fails to submit the status code to the Nitro CLI process after performing a run / describe / terminate command.");
        }
        "E42" => {
            eprintln!(
                "Enclave mmap error. Such error appears when allocating the enclave memory fails."
            );
        }
        "E43" => {
            eprintln!(
                "Enclave munmap error. Such error appears when unmapping the enclave memory fails."
            );
        }
        "E44" => {
            eprintln!("Enclave console connection failure. Such error appears when the Nitro CLI process fails to establish a connection to a running enclave's console.");
        }
        "E45" => {
            eprintln!("Enclave console read error. Such error appears when reading from a running enclave's console fails.");
        }
        "E46" => {
            eprintln!("Enclave console write output error. Such error appears when writing the information retrieved from a running enclave's console (to a given stream) fails.");
        }
        "E47" => {
            eprintln!("Integer parsing error. Such error appears when trying to connect to a running enclave's console, but the enclave CID cannot be parsed correctly.");
        }
        "E48" => {
            eprintln!("EIF building error. Such error appears when trying to build an EIF file. In this case, the error backtrace provides detailed information on the failure reason.");
        }
        "E49" => {
            eprintln!("Docker image build error. Such error appears when trying to build and EIF file, but building the corresponding docker image fails. In this case, the error backtrace provides detailed information on the failure reason.");
        }
        "E50" => {
            eprintln!("Docker image pull error. Such error appears when trying to build an EIF file, but pulling the corresponding docker image fails. In this case, the error backtrace provides detailed informatino on the failure reason.");
        }
        "E51" => {
            eprintln!("Artifacts path environment variable not set. Such error appears when trying to build an EIF file, but the artifacts path environment variable is not set.");
        }
        "E52" => {
            eprintln!("Blobs path environment variable not set. Such error appears when trying to build an EIF file, but the blobs path environment variable is not set.");
        }
        "E53" => {
            eprintln!("Clock skew error. Such error appears when continuously reading from a running enclave's console, but measuring the time elapsed between consecutive reads failed.");
        }
        "E54" => {
            eprintln!("Signal masking error. Such error appears if attempting to mask specific signals before creating an enclave process fails.");
        }
        "E55" => {
            eprintln!("Signal unmasking error. Such error appears if attempting to unmask specific signals after creating an enclave process fails.");
        }
        "E56" => {
            eprintln!("Logger error. Such error appears when attempting to initialize the underlying logging system fails.");
        }
        _ => {
            eprintln!("No such error code {}", error_code_str);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::NitroCliFailure;
    use crate::common::document_errors::construct_backtrace;

    #[test]
    fn test_construct_backtrace() {
        let failure = NitroCliFailure::new()
            .set_action(String::from("ABCD"))
            .add_subaction(String::from("EFGH"))
            .add_subaction(String::from("IJKL"))
            .set_file_and_line("/path/file.txt", 1234);
        // "  Action: ABCD
        //    Subactions:
        //      IJKL
        //      EFGH
        //    Root error file: /path/file.txt
        //    Root error line: 1234
        //    Version: X.Y.Z"
        let expected = format!("  Action: ABCD\n  Subactions:\n    IJKL\n    EFGH\n  Root error file: /path/file.txt\n  Root error line: 1234\n  Version: {}",
            env!("CARGO_PKG_VERSION"));
        assert_eq!(expected, construct_backtrace(&failure));
    }
}


// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
#![deny(missing_docs)]
#![deny(warnings)]
#![allow(clippy::too_many_arguments)]

use aws_nitro_enclaves_image_format::defs::EifIdentityInfo;
use aws_nitro_enclaves_image_format::utils::eif_reader::SignCertificateInfo;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;

/// The information to be provided for a `describe-enclaves` request.
#[derive(Clone, Serialize, Deserialize)]
pub struct EnclaveDescribeInfo {
    /// Enclave name assigned by the user
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(rename = "EnclaveName")]
    pub enclave_name: Option<String>,
    #[serde(rename = "EnclaveID")]
    /// The full ID of the enclave.
    pub enclave_id: String,
    #[serde(rename = "ProcessID")]
    /// The PID of the enclave process which manages the enclave.
    pub process_id: u32,
    #[serde(rename = "EnclaveCID")]
    /// The enclave's CID.
    pub enclave_cid: u64,
    #[serde(rename = "NumberOfCPUs")]
    /// The number of CPUs used by the enclave.
    pub cpu_count: u64,
    #[serde(rename = "CPUIDs")]
    /// The IDs of the CPUs used by the enclave.
    pub cpu_ids: Vec<u32>,
    #[serde(rename = "MemoryMiB")]
    /// The memory provided to the enclave (in MiB).
    pub memory_mib: u64,
    #[serde(rename = "State")]
    /// The current state of the enclave.
    pub state: String,
    #[serde(rename = "Flags")]
    /// The bit-mask which provides the enclave's launch flags.
    pub flags: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(flatten)]
    /// Build measurements containing PCRs
    pub build_info: Option<EnclaveBuildInfo>,
    /// Assigned or default EIF name
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(rename = "ImageName")]
    pub img_name: Option<String>,
    #[serde(rename = "ImageVersion")]
    /// Assigned or default EIF version
    #[serde(skip_serializing_if = "Option::is_none")]
    pub img_version: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(rename = "Metadata")]
    /// EIF metadata
    pub metadata: Option<MetadataDescribeInfo>,
}

impl EnclaveDescribeInfo {
    /// Create a new `EnclaveDescribeInfo` instance from the given enclave information.
    pub fn new(
        enclave_name: Option<String>,
        enclave_id: String,
        enclave_cid: u64,
        cpu_count: u64,
        cpu_ids: Vec<u32>,
        memory_mib: u64,
        state: String,
        flags: String,
        build_info: Option<EnclaveBuildInfo>,
        img_name: Option<String>,
        img_version: Option<String>,
        metadata: Option<MetadataDescribeInfo>,
    ) -> Self {
        EnclaveDescribeInfo {
            enclave_name,
            enclave_id,
            process_id: std::process::id(),
            enclave_cid,
            cpu_count,
            cpu_ids,
            memory_mib,
            state,
            flags,
            build_info,
            img_name,
            img_version,
            metadata,
        }
    }
}

/// The information to be provided for a `run-enclave` request.
#[derive(Clone, Serialize, Deserialize)]
pub struct EnclaveRunInfo {
    #[serde(rename = "EnclaveName")]
    /// The name of the enclave.
    pub enclave_name: String,
    #[serde(rename = "EnclaveID")]
    /// The full ID of the enclave.
    pub enclave_id: String,
    #[serde(rename = "ProcessID")]
    /// The PID of the enclave process which manages the enclave.
    pub process_id: u32,
    #[serde(rename = "EnclaveCID")]
    /// The enclave's CID.
    pub enclave_cid: u64,
    #[serde(rename = "NumberOfCPUs")]
    /// The number of CPUs used by the enclave.
    pub cpu_count: usize,
    #[serde(rename = "CPUIDs")]
    /// The IDs of the CPUs used by the enclave.
    pub cpu_ids: Vec<u32>,
    #[serde(rename = "MemoryMiB")]
    /// The memory provided to the enclave (in MiB).
    pub memory_mib: u64,
}

impl EnclaveRunInfo {
    /// Create a new `EnclaveRunInfo` instance from the given enclave information.
    pub fn new(
        enclave_name: String,
        enclave_id: String,
        enclave_cid: u64,
        cpu_count: usize,
        cpu_ids: Vec<u32>,
        memory_mib: u64,
    ) -> Self {
        EnclaveRunInfo {
            enclave_name,
            enclave_id,
            process_id: std::process::id(),
            enclave_cid,
            cpu_count,
            cpu_ids,
            memory_mib,
        }
    }
}

/// The information to be provided for a `terminate-enclave` request.
#[derive(Clone, Serialize, Deserialize)]
pub struct EnclaveTerminateInfo {
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(rename = "EnclaveName")]
    /// The name of the enclave. Optional for older versions.
    pub enclave_name: Option<String>,
    #[serde(rename = "EnclaveID")]
    /// The full ID of the enclave.
    pub enclave_id: String,
    #[serde(rename = "Terminated")]
    /// A flag indicating if the enclave has terminated.
    pub terminated: bool,
}

impl EnclaveTerminateInfo {
    /// Create a new `EnclaveTerminateInfo` instance from the given enclave information.
    pub fn new(enclave_name: Option<String>, enclave_id: String, terminated: bool) -> Self {
        EnclaveTerminateInfo {
            enclave_name,
            enclave_id,
            terminated,
        }
    }
}

/// The information to be provided for a `build-enclave` request.
#[derive(Serialize, Clone, Deserialize, Debug)]
pub struct EnclaveBuildInfo {
    #[serde(rename = "Measurements")]
    /// The measurement results (hashes) of various enclave properties.
    pub measurements: BTreeMap<String, String>,
}

impl EnclaveBuildInfo {
    /// Create a new `EnclaveBuildInfo` instance from the given measurements.
    pub fn new(measurements: BTreeMap<String, String>) -> Self {
        EnclaveBuildInfo { measurements }
    }
}

/// The information to be provided for a `describe-eif` request.
#[derive(Clone, Serialize, Deserialize)]
pub struct EifDescribeInfo {
    #[serde(rename = "EifVersion")]
    /// EIF version.
    pub version: u16,
    #[serde(flatten)]
    /// Contains the PCR values.
    pub build_info: EnclaveBuildInfo,
    #[serde(rename = "IsSigned")]
    /// Specifies if the image is signed or not.
    pub is_signed: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(rename = "SigningCertificate")]
    /// Certificate's signature algorithm
    pub cert_info: Option<SignCertificateInfo>,
    #[serde(rename = "CheckCRC")]
    /// Specifies if the CRC check passed.
    pub crc_check: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(rename = "SignatureCheck")]
    /// Specifies if the EIF signature check passed.
    pub sign_check: Option<bool>,
    #[serde(rename = "ImageName")]
    /// Assigned or default EIF name
    #[serde(skip_serializing_if = "Option::is_none")]
    pub img_name: Option<String>,
    #[serde(rename = "ImageVersion")]
    /// Assigned or default EIF version
    #[serde(skip_serializing_if = "Option::is_none")]
    pub img_version: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(rename = "Metadata")]
    /// EIF metadata
    pub metadata: Option<MetadataDescribeInfo>,
}

/// Metadata to be included in the describe output
#[derive(Clone, Serialize, Deserialize)]
pub struct MetadataDescribeInfo {
    #[serde(rename = "BuildTime")]
    /// Time of the build
    pub build_time: String,
    #[serde(rename = "BuildTool")]
    /// Tool used for EIF build
    pub build_tool: String,
    #[serde(rename = "BuildToolVersion")]
    /// Version of the build tool
    pub tool_version: String,
    #[serde(rename = "OperatingSystem")]
    /// Enclave OS
    pub operating_system: String,
    #[serde(rename = "KernelVersion")]
    /// Enclave kernel version
    pub kernel_version: String,
    #[serde(rename = "DockerInfo")]
    /// Docker image information
    pub docker_info: serde_json::Value,
    #[serde(skip_serializing_if = "serde_json::Value::is_null")]
    #[serde(rename = "CustomMetadata")]
    #[serde(flatten)]
    /// Metadata added by the user as JSON
    pub custom_metadata: serde_json::Value,
}

impl MetadataDescribeInfo {
    /// Construct metadata output struct
    pub fn new(eif_info: EifIdentityInfo) -> Self {
        MetadataDescribeInfo {
            build_time: eif_info.build_info.build_time,
            build_tool: eif_info.build_info.build_tool,
            tool_version: eif_info.build_info.build_tool_version,
            operating_system: eif_info.build_info.img_os,
            kernel_version: eif_info.build_info.img_kernel,
            docker_info: eif_info.docker_info,
            custom_metadata: eif_info.custom_info,
        }
    }
}


// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
#![deny(missing_docs)]
#![deny(warnings)]

use chrono::offset::{Local, Utc};
use chrono::DateTime;
use flexi_logger::writers::LogWriter;
use flexi_logger::{DeferredNow, Record};
use nix::unistd::Uid;
use std::env;
use std::fs::{File, OpenOptions, Permissions};
use std::io::{Error, ErrorKind, Result, Write};
use std::ops::{Deref, DerefMut};
use std::os::unix::fs::{MetadataExt, PermissionsExt};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};

use crate::common::{NitroCliErrorEnum, NitroCliFailure, NitroCliResult};
use crate::new_nitro_cli_failure;

/// The default logging level used by the logger.
const DEFAULT_LOG_LEVEL: &str = "info";

/// The environment variable which holds the path to the logging directory.
const LOGS_DIR_PATH_ENV_VAR: &str = "NITRO_CLI_LOGS_PATH";

/// The default path to the logging directory.
const LOGS_DIR_PATH: &str = "/var/log/nitro_enclaves";

/// The name of the output log file.
const LOG_FILE_NAME: &str = "nitro_enclaves.log";

/// A log writer which outputs its messages to a custom file. It also
/// allows the updating of its ID, in order to indicate which process
/// is actually logging a message. This implementation will also enable
/// synchronized logging to a centralized file for multiple enclaves.
#[derive(Clone)]
pub struct EnclaveProcLogWriter {
    out_file: Arc<Mutex<File>>,
    logger_id: Arc<Mutex<String>>,
}

impl EnclaveProcLogWriter {
    /// Create a new log writer.
    pub fn new() -> NitroCliResult<Self> {
        // All logging shall be directed to a centralized file.
        Ok(EnclaveProcLogWriter {
            out_file: Arc::new(Mutex::new(
                open_log_file(&get_log_file_path())
                    .map_err(|e| e.add_subaction("Failed to open log file".to_string()))?,
            )),
            logger_id: Arc::new(Mutex::new(String::new())),
        })
    }

    /// Check if the log file is present and if it is not, (re)open it.
    fn safe_open_log_file(&self) -> NitroCliResult<()> {
        let log_path = &get_log_file_path();
        if !log_path.exists() {
            let new_file = open_log_file(log_path)
                .map_err(|e| e.add_subaction(String::from("Failed to open log file")))?;
            let mut file_ref = self.out_file.lock().map_err(|e| {
                new_nitro_cli_failure!(
                    &format!("Failed to acquire lock: {:?}", e),
                    NitroCliErrorEnum::LockAcquireFailure
                )
            })?;
            *file_ref.deref_mut() = new_file;
        }

        Ok(())
    }

    /// Update the logger ID (correlated with the process which is doing logging).
    pub fn update_logger_id(&self, new_id: &str) -> NitroCliResult<()> {
        let mut old_id = self.logger_id.lock().map_err(|e| {
            new_nitro_cli_failure!(
                &format!("Failed to acquire logger ID lock: {:?}", e),
                NitroCliErrorEnum::LockAcquireFailure
            )
        })?;
        old_id.deref_mut().clear();
        old_id.deref_mut().push_str(new_id);

        Ok(())
    }

    /// Generate a single message string.
    fn create_msg(&self, now: &DateTime<Local>, record: &Record) -> NitroCliResult<String> {
        // UTC timestamp according to RFC 2822
        let timestamp = DateTime::<Utc>::from_utc(now.naive_utc(), Utc)
            .to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
        let logger_id = self.logger_id.lock().map_err(|e| {
            new_nitro_cli_failure!(
                &format!("Failed to acquire logger ID lock: {:?}", e),
                NitroCliErrorEnum::LockAcquireFailure
            )
        })?;
        Ok(format!(
            "[{}][{}][{}][{}:{}] {}\n",
            logger_id.deref(),
            record.level(),
            timestamp,
            record.file().unwrap_or("?"),
            record.line().unwrap_or(0),
            &record.args()
        ))
    }
}

impl LogWriter for EnclaveProcLogWriter {
    fn write(&self, now: &mut DeferredNow, record: &Record) -> Result<()> {
        if self.safe_open_log_file().is_err() {
            return Err(Error::new(
                ErrorKind::Other,
                "Failed to safely open log file for writing",
            ));
        }

        if let Ok(record_str) = self.create_msg(now.now(), record) {
            if let Ok(mut out_file) = self.out_file.lock() {
                out_file.deref_mut().write_all(record_str.as_bytes())?;

                return Ok(());
            }

            return Err(Error::new(ErrorKind::Other, "Failed to lock log file"));
        }

        Err(Error::new(
            ErrorKind::Other,
            "Failed to create logger message",
        ))
    }

    fn flush(&self) -> Result<()> {
        Ok(())
    }

    fn max_log_level(&self) -> log::LevelFilter {
        // The log level is either given in RUST_LOG or defaults to a specified value.
        let level = std::env::var("RUST_LOG").unwrap_or_else(|_| DEFAULT_LOG_LEVEL.to_string());

        match level.to_lowercase().as_ref() {
            "info" => log::LevelFilter::Info,
            "debug" => log::LevelFilter::Debug,
            "warn" => log::LevelFilter::Warn,
            "error" => log::LevelFilter::Error,
            "trace" => log::LevelFilter::Trace,
            _ => log::LevelFilter::Info,
        }
    }
}

/// Get the directory containing Nitro CLI related log files.
pub fn get_log_file_base_path() -> String {
    match env::var(LOGS_DIR_PATH_ENV_VAR) {
        Ok(env_path) => env_path,
        Err(_) => LOGS_DIR_PATH.to_string(),
    }
}

/// Get the path to the log file.
fn get_log_file_path() -> PathBuf {
    Path::new(&get_log_file_base_path()).join(LOG_FILE_NAME)
}

/// Open a file at a given location for writing and appending.
fn open_log_file(file_path: &Path) -> NitroCliResult<File> {
    let file = OpenOptions::new()
        .create(true)
        .append(true)
        .read(false)
        .open(file_path)
        .map_err(|e| {
            new_nitro_cli_failure!(
                &format!("Failed to open log file: {:?}", e),
                NitroCliErrorEnum::FileOperationFailure
            )
            .add_info(vec![
                file_path
                    .to_str()
                    .unwrap_or("Invalid unicode log file name"),
                "Open",
            ])
        })?;

    let log_file_uid = Uid::from_raw(
        file.metadata()
            .map_err(|e| {
                new_nitro_cli_failure!(
                    &format!("Failed to get log file metadata: {:?}", e),
                    NitroCliErrorEnum::FileOperationFailure
                )
                .add_info(vec![
                    file_path
                        .to_str()
                        .unwrap_or("Invalid unicode log file name"),
                    "Get metadata",
                ])
            })?
            .uid(),
    );

    // The log file should be write-accessible to any user, since
    // any user may launch a CLI instance. Only the file's owner
    // may change its permissions.
    if log_file_uid == Uid::current() {
        let perms = Permissions::from_mode(0o766);
        file.set_permissions(perms).map_err(|e| {
            new_nitro_cli_failure!(
                &format!("Failed to change log file permissions: {:?}", e),
                NitroCliErrorEnum::FilePermissionsError
            )
        })?;
    }

    Ok(file)
}

/// Initialize logging.
pub fn init_logger() -> NitroCliResult<EnclaveProcLogWriter> {
    // The log file is "nitro-cli.log" and is stored in the NPE resources directory.
    let log_writer = EnclaveProcLogWriter::new()?;

    // Initialize logging with the new log writer.
    flexi_logger::Logger::try_with_env_or_str(DEFAULT_LOG_LEVEL)
        .map_err(|e| {
            new_nitro_cli_failure!(
                &format!("Failed to initialize enclave process logger: {:?}", e),
                NitroCliErrorEnum::LoggerError
            )
        })?
        .log_to_writer(Box::new(log_writer.clone()))
        .start()
        .map_err(|e| {
            new_nitro_cli_failure!(
                &format!("Failed to initialize enclave process logger: {:?}", e),
                NitroCliErrorEnum::LoggerError
            )
        })?;

    // The log writer is provided for sharing between CLI-related processes.
    Ok(log_writer)
}

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

    use std::fs;
    use std::os::unix::fs::PermissionsExt;

    use tempfile::NamedTempFile;

    /// Tests that `open_log_file()` creates a file
    /// with the expected permissions.
    #[test]
    fn test_open_log_file() {
        let file0 = NamedTempFile::new();

        if let Ok(file0) = file0 {
            let test_file_path = file0.path();

            let f = open_log_file(test_file_path).unwrap();
            let metadata = f.metadata();
            assert!(metadata.is_ok());

            if let Ok(metadata) = metadata {
                assert!(metadata.is_file());
                let permissions = metadata.permissions();
                let mode = permissions.mode();

                assert_eq!(mode & 0o777, 0o766);
            }
        }
    }

    /// Tests that the logger id is initially empty ("").
    #[test]
    fn test_init_logger() {
        let tmp_log_dir: &str = "./.tmp_logs_init_logger";

        // Get old environment variable value
        let old_log_path = env::var(LOGS_DIR_PATH_ENV_VAR);
        let path_existed = Path::new(tmp_log_dir).exists();

        // Update environment variable value
        env::set_var(LOGS_DIR_PATH_ENV_VAR, tmp_log_dir);
        let _ = fs::create_dir(tmp_log_dir);

        let log_writer = EnclaveProcLogWriter::new().unwrap();
        let lock_result = log_writer.logger_id.lock();

        assert!(lock_result.unwrap().is_empty());

        if !path_existed {
            // Remove whole `tmp_log_dir` if necessary
            let _ = fs::remove_dir_all(tmp_log_dir);
        } else {
            // Only remove the log file
            let _ = fs::remove_file(&format!("{}/{}", tmp_log_dir, &LOG_FILE_NAME));
        }

        // Reset old environment variable value if necessary
        if let Ok(old_log_path) = old_log_path {
            env::set_var(LOGS_DIR_PATH_ENV_VAR, old_log_path);
        }
    }

    /// Tests that the logger id is altered after issuing a
    /// call to `update_logger_id()`.
    #[test]
    fn test_update_logger_id() {
        let tmp_log_dir: &str = "./.tmp_logs_update_logger_id";

        // Get old environment variable value
        let old_log_path = env::var(LOGS_DIR_PATH_ENV_VAR);
        let path_existed = Path::new(tmp_log_dir).exists();

        // Update environment variable value
        env::set_var(LOGS_DIR_PATH_ENV_VAR, tmp_log_dir);
        let _ = fs::create_dir(tmp_log_dir);

        let log_writer = EnclaveProcLogWriter::new().unwrap();
        let _ = log_writer.update_logger_id("new-logger-id").unwrap();
        let lock_result = log_writer.logger_id.lock();

        assert!(lock_result.unwrap().eq("new-logger-id"));

        let _ = log_writer.update_logger_id("").unwrap();

        if !path_existed {
            // Remove whole `tmp_log_dir` if necessary
            let _ = fs::remove_dir_all(tmp_log_dir);
        } else {
            // Only remove the log file
            let _ = fs::remove_file(&format!("{}/{}", tmp_log_dir, &LOG_FILE_NAME));
        }

        // Reset old environment variable value if necessary
        if let Ok(old_log_path) = old_log_path {
            env::set_var(LOGS_DIR_PATH_ENV_VAR, old_log_path);
        }
    }
}