//! [OCI runtime spec](https://github.com/opencontainers/runtime-spec) types and definitions.
//!
//! [`Spec`] represents the root object from the specification.

use derive_builder::Builder;
use getset::{Getters, MutGetters, Setters};
use serde::{Deserialize, Serialize};
use std::{
    collections::HashMap,
    fs,
    io::{BufReader, BufWriter, Write},
    path::{Path, PathBuf},
};

use crate::error::{oci_error, OciSpecError, Result};

mod capability;
mod features;
mod hooks;
mod linux;
mod miscellaneous;
mod process;
mod solaris;
mod state;
mod test;
mod version;
mod vm;
mod windows;
mod zos;

// re-export for ease of use
pub use capability::*;
pub use features::*;
pub use hooks::*;
pub use linux::*;
pub use miscellaneous::*;
pub use process::*;
pub use solaris::*;
pub use state::*;
pub use version::*;
pub use vm::*;
pub use windows::*;
pub use zos::*;

/// `config.json` file root object.
#[derive(
    Builder, Clone, Debug, Deserialize, Getters, MutGetters, Setters, PartialEq, Eq, Serialize,
)]
#[serde(rename_all = "camelCase")]
#[builder(
    default,
    pattern = "owned",
    setter(into, strip_option),
    build_fn(error = "OciSpecError")
)]
#[getset(get_mut = "pub", get = "pub", set = "pub")]
pub struct Spec {
    #[serde(default, rename = "ociVersion")]
    ///  MUST be in SemVer v2.0.0 format and specifies the version of the
    /// Open Container Initiative  Runtime Specification with which
    /// the bundle complies. The Open Container Initiative
    ///  Runtime Specification follows semantic versioning and retains
    /// forward and backward  compatibility within major versions.
    /// For example, if a configuration is compliant with
    ///  version 1.1 of this specification, it is compatible with all
    /// runtimes that support any 1.1  or later release of this
    /// specification, but is not compatible with a runtime that supports
    ///  1.0 and not 1.1.
    version: String,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    /// Specifies the container's root filesystem. On Windows, for Windows
    /// Server Containers, this field is REQUIRED. For Hyper-V
    /// Containers, this field MUST NOT be set.
    ///
    /// On all other platforms, this field is REQUIRED.
    root: Option<Root>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    /// Specifies additional mounts beyond `root`. The runtime MUST mount
    /// entries in the listed order.
    ///
    /// For Linux, the parameters are as documented in
    /// [`mount(2)`](http://man7.org/linux/man-pages/man2/mount.2.html) system call man page. For
    /// Solaris, the mount entry corresponds to the 'fs' resource in the
    /// [`zonecfg(1M)`](http://docs.oracle.com/cd/E86824_01/html/E54764/zonecfg-1m.html) man page.
    mounts: Option<Vec<Mount>>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    /// Specifies the container process. This property is REQUIRED when
    /// [`start`](https://github.com/opencontainers/runtime-spec/blob/master/runtime.md#start) is
    /// called.
    process: Option<Process>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    /// Specifies the container's hostname as seen by processes running
    /// inside the container. On Linux, for example, this will
    /// change the hostname in the container [UTS namespace](http://man7.org/linux/man-pages/man7/namespaces.7.html). Depending on your
    /// [namespace
    /// configuration](https://github.com/opencontainers/runtime-spec/blob/master/config-linux.md#namespaces),
    /// the container UTS namespace may be the runtime UTS namespace.
    hostname: Option<String>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    /// Specifies the container's domainame as seen by processes running
    /// inside the container. On Linux, for example, this will
    /// change the domainame in the container [UTS namespace](http://man7.org/linux/man-pages/man7/namespaces.7.html). Depending on your
    /// [namespace
    /// configuration](https://github.com/opencontainers/runtime-spec/blob/master/config-linux.md#namespaces),
    /// the container UTS namespace may be the runtime UTS namespace.
    domainname: Option<String>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    /// Hooks allow users to specify programs to run before or after various
    /// lifecycle events. Hooks MUST be called in the listed order.
    /// The state of the container MUST be passed to hooks over
    /// stdin so that they may do work appropriate to the current state of
    /// the container.
    hooks: Option<Hooks>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    /// Annotations contains arbitrary metadata for the container. This
    /// information MAY be structured or unstructured. Annotations
    /// MUST be a key-value map. If there are no annotations then
    /// this property MAY either be absent or an empty map.
    ///
    /// Keys MUST be strings. Keys MUST NOT be an empty string. Keys SHOULD
    /// be named using a reverse domain notation - e.g.
    /// com.example.myKey. Keys using the org.opencontainers
    /// namespace are reserved and MUST NOT be used by subsequent
    /// specifications. Runtimes MUST handle unknown annotation keys
    /// like any other unknown property.
    ///
    /// Values MUST be strings. Values MAY be an empty string.
    annotations: Option<HashMap<String, String>>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    /// Linux is platform-specific configuration for Linux based containers.
    linux: Option<Linux>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    /// Solaris is platform-specific configuration for Solaris based
    /// containers.
    solaris: Option<Solaris>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    /// Windows is platform-specific configuration for Windows based
    /// containers.
    windows: Option<Windows>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    /// VM specifies configuration for Virtual Machine based containers.
    vm: Option<VM>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    /// z/OS is platform-specific configuration for z/OS based containers.
    zos: Option<ZOS>,

    #[deprecated(
        since = "0.10.0",
        note = "uid_mappings on the top-level Spec struct has never existed in the OCI runtime spec. Use Linux::uid_mappings or Mount::uid_mappings instead."
    )]
    #[serde(default, skip_serializing_if = "Option::is_none")]
    /// UID mappings used for changing file owners w/o calling chown, fs should support it.
    /// Every mount point could have its own mapping.
    ///
    /// # Deprecated
    /// This field has never existed on the top-level `Spec` struct in the OCI runtime spec.
    /// Use [`Linux::uid_mappings`] or [`Mount::uid_mappings`] instead.
    uid_mappings: Option<Vec<LinuxIdMapping>>,

    #[deprecated(
        since = "0.10.0",
        note = "gid_mappings on the top-level Spec struct has never existed in the OCI runtime spec. Use Linux::gid_mappings or Mount::gid_mappings instead."
    )]
    #[serde(default, skip_serializing_if = "Option::is_none")]
    /// GID mappings used for changing file owners w/o calling chown, fs should support it.
    /// Every mount point could have its own mapping.
    ///
    /// # Deprecated
    /// This field has never existed on the top-level `Spec` struct in the OCI runtime spec.
    /// Use [`Linux::gid_mappings`] or [`Mount::gid_mappings`] instead.
    gid_mappings: Option<Vec<LinuxIdMapping>>,
}

// This gives a basic boilerplate for Spec that can be used calling
// Default::default(). The values given are similar to the defaults seen in
// docker and runc, it creates a containerized shell! (see respective types
// default impl for more info)
#[allow(deprecated)]
impl Default for Spec {
    fn default() -> Self {
        Spec {
            // Defaults to most current oci version
            version: String::from("1.0.2-dev"),
            process: Some(Default::default()),
            root: Some(Default::default()),
            hostname: "youki".to_string().into(),
            domainname: None,
            mounts: get_default_mounts().into(),
            // Defaults to empty metadata
            annotations: Some(Default::default()),
            linux: Some(Default::default()),
            hooks: None,
            solaris: None,
            windows: None,
            vm: None,
            zos: None,
            uid_mappings: None,
            gid_mappings: None,
        }
    }
}

impl Spec {
    /// Load a new `Spec` from the provided JSON file `path`.
    /// # Errors
    /// This function will return an [OciSpecError::Io] if the spec does not exist or an
    /// [OciSpecError::SerDe] if it is invalid.
    /// # Example
    /// ``` no_run
    /// use oci_spec::runtime::Spec;
    ///
    /// let spec = Spec::load("config.json").unwrap();
    /// ```
    pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
        let path = path.as_ref();
        let file = fs::File::open(path)?;
        let reader = BufReader::new(file);
        let s = serde_json::from_reader(reader)?;
        Ok(s)
    }

    /// Save a `Spec` to the provided JSON file `path`.
    /// # Errors
    /// This function will return an [OciSpecError::Io] if a file cannot be created at the provided
    /// path or an [OciSpecError::SerDe] if the spec cannot be serialized.
    /// # Example
    /// ``` no_run
    /// use oci_spec::runtime::Spec;
    ///
    /// let mut spec = Spec::load("config.json").unwrap();
    /// spec.save("my_config.json").unwrap();
    /// ```
    pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
        let path = path.as_ref();
        let file = fs::File::create(path)?;
        let mut writer = BufWriter::new(file);
        serde_json::to_writer(&mut writer, self)?;
        writer.flush()?;
        Ok(())
    }

    /// Canonicalize the `root.path` of the `Spec` for the provided `bundle`.
    pub fn canonicalize_rootfs<P: AsRef<Path>>(&mut self, bundle: P) -> Result<()> {
        let root = self
            .root
            .as_ref()
            .ok_or_else(|| oci_error("no root path provided for canonicalization"))?;
        let path = Self::canonicalize_path(bundle, root.path())?;
        self.root = Some(
            RootBuilder::default()
                .path(path)
                .readonly(root.readonly().unwrap_or(false))
                .build()
                .map_err(|_| oci_error("failed to set canonicalized root"))?,
        );
        Ok(())
    }

    /// Return default rootless spec.
    /// # Example
    /// ``` no_run
    /// use oci_spec::runtime::Spec;
    ///
    /// let spec = Spec::rootless(1000, 1000);
    /// ```
    pub fn rootless(uid: u32, gid: u32) -> Self {
        Self {
            mounts: get_rootless_mounts().into(),
            linux: Some(Linux::rootless(uid, gid)),
            ..Default::default()
        }
    }

    fn canonicalize_path<B, P>(bundle: B, path: P) -> Result<PathBuf>
    where
        B: AsRef<Path>,
        P: AsRef<Path>,
    {
        Ok(if path.as_ref().is_absolute() {
            fs::canonicalize(path.as_ref())?
        } else {
            let canonical_bundle_path = fs::canonicalize(&bundle)?;
            fs::canonicalize(canonical_bundle_path.join(path.as_ref()))?
        })
    }
}

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

    #[test]
    fn test_canonicalize_rootfs() {
        let rootfs_name = "rootfs";
        let bundle = tempfile::tempdir().expect("failed to create tmp test bundle dir");

        // On macOS, `$TMPDIR` may not point to canonicalized path.
        // ```
        // $ echo $TMPDIR; realpath $TMPDIR
        // /var/folders/_h/j_17023n23s3_50cq_gwhrrc0000gq/T/
        // /private/var/folders/_h/j_17023n23s3_50cq_gwhrrc0000gq/T
        // ```
        let bundle = fs::canonicalize(bundle.path()).expect("failed to canonicalize bundle");

        let rootfs_absolute_path = bundle.join(rootfs_name);
        assert!(
            rootfs_absolute_path.is_absolute(),
            "rootfs path is not absolute path"
        );
        fs::create_dir_all(&rootfs_absolute_path).expect("failed to create the testing rootfs");
        {
            // Test the case with absolute path
            let mut spec = SpecBuilder::default()
                .root(
                    RootBuilder::default()
                        .path(rootfs_absolute_path.clone())
                        .build()
                        .unwrap(),
                )
                .build()
                .unwrap();

            spec.canonicalize_rootfs(&bundle)
                .expect("failed to canonicalize rootfs");

            assert_eq!(
                &rootfs_absolute_path,
                spec.root.expect("no root in spec").path()
            );
        }
        {
            // Test the case with relative path
            let mut spec = SpecBuilder::default()
                .root(RootBuilder::default().path(rootfs_name).build().unwrap())
                .build()
                .unwrap();

            spec.canonicalize_rootfs(&bundle)
                .expect("failed to canonicalize rootfs");

            assert_eq!(
                &rootfs_absolute_path,
                spec.root.expect("no root in spec").path()
            );
        }
    }

    #[test]
    fn test_load_save() {
        let spec = Spec {
            ..Default::default()
        };
        let test_dir = tempfile::tempdir().expect("failed to create tmp test dir");
        let spec_path = test_dir.keep().join("config.json");

        // Test first save the default config, and then load the saved config.
        // The before and after should be the same.
        spec.save(&spec_path).expect("failed to save spec");
        let loaded_spec = Spec::load(&spec_path).expect("failed to load the saved spec.");
        assert_eq!(
            spec, loaded_spec,
            "The saved spec is not the same as the loaded spec"
        );
    }

    #[test]
    fn test_rootless() {
        const UID: u32 = 1000;
        const GID: u32 = 1000;

        let spec = Spec::default();
        let spec_rootless = Spec::rootless(UID, GID);
        assert!(
            spec != spec_rootless,
            "default spec and rootless spec should be different"
        );

        // Check rootless linux object.
        let linux = spec_rootless
            .linux
            .expect("linux object should not be empty");
        let uid_mappings = linux
            .uid_mappings()
            .clone()
            .expect("uid mappings should not be empty");
        let gid_mappings = linux
            .gid_mappings()
            .clone()
            .expect("gid mappings should not be empty");
        let namespaces = linux
            .namespaces()
            .clone()
            .expect("namespaces should not be empty");
        assert_eq!(uid_mappings.len(), 1, "uid mappings length should be 1");
        assert_eq!(
            uid_mappings[0].host_id(),
            UID,
            "uid mapping host id should be as defined"
        );
        assert_eq!(gid_mappings.len(), 1, "gid mappings length should be 1");
        assert_eq!(
            gid_mappings[0].host_id(),
            GID,
            "gid mapping host id should be as defined"
        );
        assert!(
            !namespaces
                .iter()
                .any(|ns| ns.typ() == LinuxNamespaceType::Network),
            "rootless spec should not contain network namespace type"
        );
        assert!(
            namespaces
                .iter()
                .any(|ns| ns.typ() == LinuxNamespaceType::User),
            "rootless spec should contain user namespace type"
        );
        assert!(
            linux.resources().is_none(),
            "resources in rootless spec should be empty"
        );

        // Check rootless mounts.
        let mounts = spec_rootless.mounts.expect("mounts should not be empty");
        assert!(
            !mounts.iter().any(|m| {
                if m.destination().to_string_lossy() == "/dev/pts" {
                    m.options()
                        .clone()
                        .expect("options should not be empty")
                        .iter()
                        .any(|o| o == "gid=5")
                } else {
                    false
                }
            }),
            "gid=5 in rootless should not be present"
        );
        let sys_mount = mounts
            .iter()
            .find(|m| m.destination().to_string_lossy() == "/sys")
            .expect("sys mount should be present");
        assert_eq!(
            sys_mount.typ(),
            &Some("none".to_string()),
            "type should be changed in sys mount"
        );
        assert_eq!(
            sys_mount
                .source()
                .clone()
                .expect("source should not be empty in sys mount")
                .to_string_lossy(),
            "/sys",
            "source should be changed in sys mount"
        );
        assert!(
            sys_mount
                .options()
                .clone()
                .expect("options should not be empty in sys mount")
                .iter()
                .any(|o| o == "rbind"),
            "rbind option should be present in sys mount"
        );

        // Check that some other objects have same values.
        assert!(spec.process == spec_rootless.process);
        assert!(spec.root == spec_rootless.root);
        assert!(spec.hooks == spec_rootless.hooks);
    }
}
