// Copyright 2019 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

//! The root level module that includes the config and aggregate of the submodules for running said
//! configs.

pub mod argument;
pub mod error;
#[cfg(all(target_arch = "x86_64", feature = "gdb"))]
pub mod gdb;
#[path = "linux.rs"]
pub mod platform;
#[cfg(feature = "plugin")]
pub mod plugin;

use std::collections::BTreeMap;
use std::net;
use std::os::unix::io::RawFd;
use std::path::{Path, PathBuf};
use std::str::FromStr;

use arch::{Pstore, VcpuAffinity};
use devices::serial_device::{SerialHardware, SerialParameters};
#[cfg(feature = "audio_cras")]
use devices::virtio::cras_backend::Parameters as CrasSndParameters;
use devices::virtio::fs::passthrough;
#[cfg(feature = "gpu")]
use devices::virtio::gpu::GpuParameters;
#[cfg(feature = "audio")]
use devices::Ac97Parameters;
use devices::ProtectionType;
use libc::{getegid, geteuid};
use vm_control::BatteryType;

static KVM_PATH: &str = "/dev/kvm";
static VHOST_VSOCK_PATH: &str = "/dev/vhost-vsock";
static VHOST_NET_PATH: &str = "/dev/vhost-net";
static SECCOMP_POLICY_DIR: &str = "/usr/share/policy/crosvm";

/// Indicates the location and kind of executable kernel for a VM.
#[derive(Debug)]
pub enum Executable {
    /// An executable intended to be run as a BIOS directly.
    Bios(PathBuf),
    /// A elf linux kernel, loaded and executed by crosvm.
    Kernel(PathBuf),
    /// Path to a plugin executable that is forked by crosvm.
    Plugin(PathBuf),
}

/// Maximum length of a `DiskOption` identifier.
///
/// This is based on the virtio-block ID length limit.
pub const DISK_ID_LEN: usize = 20;

pub struct DiskOption {
    pub path: PathBuf,
    pub read_only: bool,
    pub sparse: bool,
    pub o_direct: bool,
    pub block_size: u32,
    pub id: Option<[u8; DISK_ID_LEN]>,
}

pub struct VhostUserOption {
    pub socket: PathBuf,
}

pub struct VhostUserFsOption {
    pub socket: PathBuf,
    pub tag: String,
}

pub struct VhostUserWlOption {
    pub socket: PathBuf,
    pub vm_tube: PathBuf,
}

/// A bind mount for directories in the plugin process.
pub struct BindMount {
    pub src: PathBuf,
    pub dst: PathBuf,
    pub writable: bool,
}

/// A mapping of linux group IDs for the plugin process.
pub struct GidMap {
    pub inner: libc::gid_t,
    pub outer: libc::gid_t,
    pub count: u32,
}

/// Direct IO forwarding options
#[cfg(feature = "direct")]
pub struct DirectIoOption {
    pub path: PathBuf,
    pub ranges: Vec<(u64, u64)>,
}

pub const DEFAULT_TOUCH_DEVICE_HEIGHT: u32 = 1024;
pub const DEFAULT_TOUCH_DEVICE_WIDTH: u32 = 1280;

pub struct TouchDeviceOption {
    path: PathBuf,
    width: Option<u32>,
    height: Option<u32>,
    default_width: u32,
    default_height: u32,
}

impl TouchDeviceOption {
    pub fn new(path: PathBuf) -> TouchDeviceOption {
        TouchDeviceOption {
            path,
            width: None,
            height: None,
            default_width: DEFAULT_TOUCH_DEVICE_WIDTH,
            default_height: DEFAULT_TOUCH_DEVICE_HEIGHT,
        }
    }

    /// Getter for the path to the input event streams.
    pub fn get_path(&self) -> &Path {
        self.path.as_path()
    }

    /// When a user specifies the parameters for a touch device, width and height are optional.
    /// If the width and height are missing, default values are used. Default values can be set
    /// dynamically, for example from the display sizes specified by the gpu argument.
    pub fn set_default_size(&mut self, width: u32, height: u32) {
        self.default_width = width;
        self.default_height = height;
    }

    /// Setter for the width specified by the user.
    pub fn set_width(&mut self, width: u32) {
        self.width.replace(width);
    }

    /// Setter for the height specified by the user.
    pub fn set_height(&mut self, height: u32) {
        self.height.replace(height);
    }

    /// If the user specifies the size, use it. Otherwise, use the default values.
    pub fn get_size(&self) -> (u32, u32) {
        (
            self.width.unwrap_or(self.default_width),
            self.height.unwrap_or(self.default_height),
        )
    }
}

#[derive(Eq, PartialEq)]
pub enum SharedDirKind {
    FS,
    P9,
}

impl FromStr for SharedDirKind {
    type Err = &'static str;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        use SharedDirKind::*;
        match s {
            "fs" | "FS" => Ok(FS),
            "9p" | "9P" | "p9" | "P9" => Ok(P9),
            _ => Err("invalid file system type"),
        }
    }
}

impl Default for SharedDirKind {
    fn default() -> SharedDirKind {
        SharedDirKind::P9
    }
}

pub struct SharedDir {
    pub src: PathBuf,
    pub tag: String,
    pub kind: SharedDirKind,
    pub uid_map: String,
    pub gid_map: String,
    pub fs_cfg: passthrough::Config,
    pub p9_cfg: p9::Config,
}

impl Default for SharedDir {
    fn default() -> SharedDir {
        SharedDir {
            src: Default::default(),
            tag: Default::default(),
            kind: Default::default(),
            uid_map: format!("0 {} 1", unsafe { geteuid() }),
            gid_map: format!("0 {} 1", unsafe { getegid() }),
            fs_cfg: Default::default(),
            p9_cfg: Default::default(),
        }
    }
}

/// Vfio device type, recognized based on command line option.
#[derive(Eq, PartialEq, Clone, Copy)]
pub enum VfioType {
    Pci,
    Platform,
}

impl FromStr for VfioType {
    type Err = &'static str;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        use VfioType::*;
        match s {
            "vfio" => Ok(Pci),
            "vfio-platform" => Ok(Platform),
            _ => Err("invalid vfio device type, must be 'vfio|vfio-platform'"),
        }
    }
}

/// VFIO device structure for creating a new instance based on command line options.
pub struct VfioCommand {
    vfio_path: PathBuf,
    dev_type: VfioType,
    params: BTreeMap<String, String>,
}

impl VfioCommand {
    pub fn new(dev_type: VfioType, path: &str) -> argument::Result<VfioCommand> {
        let mut param = path.split(',');
        let vfio_path =
            PathBuf::from(param.next().ok_or_else(|| argument::Error::InvalidValue {
                value: path.to_owned(),
                expected: String::from("missing vfio path"),
            })?);

        if !vfio_path.exists() {
            return Err(argument::Error::InvalidValue {
                value: path.to_owned(),
                expected: String::from("the vfio path does not exist"),
            });
        }
        if !vfio_path.is_dir() {
            return Err(argument::Error::InvalidValue {
                value: path.to_owned(),
                expected: String::from("the vfio path should be directory"),
            });
        }

        let mut params = BTreeMap::new();
        for p in param {
            let mut kv = p.splitn(2, '=');
            if let (Some(kind), Some(value)) = (kv.next(), kv.next()) {
                Self::validate_params(kind, value)?;
                params.insert(kind.to_owned(), value.to_owned());
            };
        }
        Ok(VfioCommand {
            vfio_path,
            params,
            dev_type,
        })
    }

    fn validate_params(kind: &str, value: &str) -> Result<(), argument::Error> {
        match kind {
            "iommu" => match value {
                "on" | "off" => Ok(()),
                _ => {
                    return Err(argument::Error::InvalidValue {
                        value: format!("{}={}", kind.to_owned(), value.to_owned()),
                        expected: String::from("option must be `iommu=on|off`"),
                    })
                }
            },
            _ => Err(argument::Error::InvalidValue {
                value: format!("{}={}", kind.to_owned(), value.to_owned()),
                expected: String::from("option must be `iommu=<val>`"),
            }),
        }
    }

    pub fn get_type(&self) -> VfioType {
        self.dev_type
    }

    pub fn iommu_enabled(&self) -> bool {
        matches!(self.params.get("iommu"), Some(value) if value.as_str() == "on")
    }
}

/// Aggregate of all configurable options for a running VM.
pub struct Config {
    pub kvm_device_path: PathBuf,
    pub vhost_vsock_device_path: PathBuf,
    pub vhost_net_device_path: PathBuf,
    pub vcpu_count: Option<usize>,
    pub rt_cpus: Vec<usize>,
    pub vcpu_affinity: Option<VcpuAffinity>,
    pub cpu_clusters: Vec<Vec<usize>>,
    pub cpu_capacity: BTreeMap<usize, u32>, // CPU index -> capacity
    pub per_vm_core_scheduling: bool,
    #[cfg(feature = "audio_cras")]
    pub cras_snd: Option<CrasSndParameters>,
    pub delay_rt: bool,
    pub no_smt: bool,
    pub memory: Option<u64>,
    pub swiotlb: Option<u64>,
    pub hugepages: bool,
    pub memory_file: Option<PathBuf>,
    pub executable_path: Option<Executable>,
    pub android_fstab: Option<PathBuf>,
    pub initrd_path: Option<PathBuf>,
    pub params: Vec<String>,
    pub socket_path: Option<PathBuf>,
    pub plugin_root: Option<PathBuf>,
    pub plugin_mounts: Vec<BindMount>,
    pub plugin_gid_maps: Vec<GidMap>,
    pub disks: Vec<DiskOption>,
    pub pmem_devices: Vec<DiskOption>,
    pub pstore: Option<Pstore>,
    pub host_ip: Option<net::Ipv4Addr>,
    pub netmask: Option<net::Ipv4Addr>,
    pub mac_address: Option<net_util::MacAddress>,
    pub net_vq_pairs: Option<u16>,
    pub vhost_net: bool,
    pub tap_fd: Vec<RawFd>,
    pub cid: Option<u64>,
    pub wayland_socket_paths: BTreeMap<String, PathBuf>,
    pub x_display: Option<String>,
    pub shared_dirs: Vec<SharedDir>,
    pub sandbox: bool,
    pub seccomp_policy_dir: PathBuf,
    pub seccomp_log_failures: bool,
    #[cfg(feature = "gpu")]
    pub gpu_parameters: Option<GpuParameters>,
    pub software_tpm: bool,
    pub display_window_keyboard: bool,
    pub display_window_mouse: bool,
    #[cfg(feature = "audio")]
    pub ac97_parameters: Vec<Ac97Parameters>,
    #[cfg(feature = "audio")]
    pub sound: Option<PathBuf>,
    pub serial_parameters: BTreeMap<(SerialHardware, u8), SerialParameters>,
    pub syslog_tag: Option<String>,
    pub virtio_single_touch: Vec<TouchDeviceOption>,
    pub virtio_multi_touch: Vec<TouchDeviceOption>,
    pub virtio_trackpad: Vec<TouchDeviceOption>,
    pub virtio_mice: Vec<PathBuf>,
    pub virtio_keyboard: Vec<PathBuf>,
    pub virtio_switches: Vec<PathBuf>,
    pub virtio_input_evdevs: Vec<PathBuf>,
    pub split_irqchip: bool,
    pub vfio: Vec<VfioCommand>,
    pub video_dec: bool,
    pub video_enc: bool,
    pub acpi_tables: Vec<PathBuf>,
    pub protected_vm: ProtectionType,
    pub battery_type: Option<BatteryType>,
    #[cfg(all(target_arch = "x86_64", feature = "gdb"))]
    pub gdb: Option<u32>,
    pub balloon_bias: i64,
    pub vhost_user_blk: Vec<VhostUserOption>,
    pub vhost_user_console: Vec<VhostUserOption>,
    pub vhost_user_fs: Vec<VhostUserFsOption>,
    pub vhost_user_gpu: Vec<VhostUserOption>,
    pub vhost_user_mac80211_hwsim: Option<VhostUserOption>,
    pub vhost_user_net: Vec<VhostUserOption>,
    pub vhost_user_vsock: Vec<VhostUserOption>,
    pub vhost_user_wl: Vec<VhostUserWlOption>,
    #[cfg(feature = "direct")]
    pub direct_pmio: Option<DirectIoOption>,
    #[cfg(feature = "direct")]
    pub direct_level_irq: Vec<u32>,
    #[cfg(feature = "direct")]
    pub direct_edge_irq: Vec<u32>,
    pub dmi_path: Option<PathBuf>,
    pub no_legacy: bool,
}

impl Default for Config {
    fn default() -> Config {
        Config {
            kvm_device_path: PathBuf::from(KVM_PATH),
            vhost_vsock_device_path: PathBuf::from(VHOST_VSOCK_PATH),
            vhost_net_device_path: PathBuf::from(VHOST_NET_PATH),
            vcpu_count: None,
            rt_cpus: Vec::new(),
            vcpu_affinity: None,
            cpu_clusters: Vec::new(),
            cpu_capacity: BTreeMap::new(),
            per_vm_core_scheduling: false,
            #[cfg(feature = "audio_cras")]
            cras_snd: None,
            delay_rt: false,
            no_smt: false,
            memory: None,
            swiotlb: None,
            hugepages: false,
            memory_file: None,
            executable_path: None,
            android_fstab: None,
            initrd_path: None,
            params: Vec::new(),
            socket_path: None,
            plugin_root: None,
            plugin_mounts: Vec::new(),
            plugin_gid_maps: Vec::new(),
            disks: Vec::new(),
            pmem_devices: Vec::new(),
            pstore: None,
            host_ip: None,
            netmask: None,
            mac_address: None,
            net_vq_pairs: None,
            vhost_net: false,
            tap_fd: Vec::new(),
            cid: None,
            #[cfg(feature = "gpu")]
            gpu_parameters: None,
            software_tpm: false,
            wayland_socket_paths: BTreeMap::new(),
            x_display: None,
            display_window_keyboard: false,
            display_window_mouse: false,
            shared_dirs: Vec::new(),
            sandbox: !cfg!(feature = "default-no-sandbox"),
            seccomp_policy_dir: PathBuf::from(SECCOMP_POLICY_DIR),
            seccomp_log_failures: false,
            #[cfg(feature = "audio")]
            ac97_parameters: Vec::new(),
            #[cfg(feature = "audio")]
            sound: None,
            serial_parameters: BTreeMap::new(),
            syslog_tag: None,
            virtio_single_touch: Vec::new(),
            virtio_multi_touch: Vec::new(),
            virtio_trackpad: Vec::new(),
            virtio_mice: Vec::new(),
            virtio_keyboard: Vec::new(),
            virtio_switches: Vec::new(),
            virtio_input_evdevs: Vec::new(),
            split_irqchip: false,
            vfio: Vec::new(),
            video_dec: false,
            video_enc: false,
            acpi_tables: Vec::new(),
            protected_vm: ProtectionType::Unprotected,
            battery_type: None,
            #[cfg(all(target_arch = "x86_64", feature = "gdb"))]
            gdb: None,
            balloon_bias: 0,
            vhost_user_blk: Vec::new(),
            vhost_user_console: Vec::new(),
            vhost_user_gpu: Vec::new(),
            vhost_user_fs: Vec::new(),
            vhost_user_mac80211_hwsim: None,
            vhost_user_net: Vec::new(),
            vhost_user_vsock: Vec::new(),
            vhost_user_wl: Vec::new(),
            #[cfg(feature = "direct")]
            direct_pmio: None,
            #[cfg(feature = "direct")]
            direct_level_irq: Vec::new(),
            #[cfg(feature = "direct")]
            direct_edge_irq: Vec::new(),
            dmi_path: None,
            no_legacy: false,
        }
    }
}
