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

pub type ExtentProperties = disk_extractor_lib::ExtentProperties;
pub type Error = disk_extractor_lib::Error;
pub use disk_extractor_lib::{extractor::Extractor, options::ExtractorOptions};
use std::{
    fs::File,
    os::{raw::c_int, unix::io::FromRawFd},
};

/// A simple structure to convert rust result into C compatible error type.
#[repr(C)]
#[derive(Copy, Clone, Debug)]
#[must_use]
pub struct CResult {
    /// Set to `true` on success and false on failure.
    pub ok: bool,

    /// If an operation has failed i.e. `ok` is false, `kind` indicates the type
    /// of error.
    pub kind: Error,
}

impl CResult {
    /// Returns ok type CResult.
    pub fn ok() -> Self {
        CResult { ok: true, kind: Error::CannotOverride }
    }
}

/// Converts Result to new CResult.
impl From<Result<(), Error>> for CResult {
    fn from(result: Result<(), Error>) -> Self {
        if result.is_ok() {
            return CResult::ok();
        }
        CResult { ok: false, kind: result.err().unwrap() }
    }
}

/// Converts enum Error to new CResult.
impl From<Error> for CResult {
    fn from(kind: Error) -> Self {
        CResult { ok: false, kind }
    }
}

#[allow(clippy::not_unsafe_ptr_arg_deref)]
/// Creates a new [`Extractor`] and returns an opaque pointer to it.
///
/// # Arguments
/// `out_fd`: File descriptor pointing to rw file. The file will be truncated to
/// zero lenght.
/// `in_file`: File descriptor pointing to readable/seekable file.
/// `options`: [`ExtractorOptions`]
///
/// Asserts on failure to truncate.
#[no_mangle]
pub extern "C" fn extractor_new(
    in_fd: c_int,
    options: ExtractorOptions,
    out_fd: c_int,
    out_extractor: *mut *mut Extractor,
) -> CResult {
    if out_extractor.is_null() || options.alignment == 0 || in_fd == -1 || out_fd == -1 {
        return CResult::from(Error::InvalidArgument);
    }

    let out_file = unsafe { File::from_raw_fd(out_fd) };
    let in_file = unsafe { File::from_raw_fd(in_fd) };
    let metadata_or = out_file.metadata();
    if metadata_or.is_err() || metadata_or.unwrap().len() != 0 {
        return CResult::from(Error::ReadFailed);
    }
    unsafe {
        *out_extractor =
            Box::into_raw(Box::new(Extractor::new(Box::new(in_file), options, Box::new(out_file))));
    }
    CResult::ok()
}

#[allow(clippy::not_unsafe_ptr_arg_deref)]
/// Destroys an [`Extractor`] object.
#[no_mangle]
pub extern "C" fn extractor_delete(extractor: *mut Extractor) {
    if !extractor.is_null() {
        unsafe { Box::from_raw(extractor) };
    }
}

/// Adds a new extent to the `extractor`.
///
/// # Arguments
/// `offset`: Location where the extent's data can be found in `in_fd`.
/// `size`: Size of the extent in bytes.
/// `properties`: [`ExtentProperties`]
#[no_mangle]
#[must_use]
pub extern "C" fn extractor_add(
    extractor: &mut Extractor,
    offset: u64,
    size: u64,
    properties: ExtentProperties,
) -> CResult {
    if size == 0 {
        return CResult::from(Error::InvalidRange);
    }

    return CResult::from(extractor.add(offset..(offset + size), properties, None));
}

/// Writes staged extents to out_fd.
#[no_mangle]
#[must_use]
pub extern "C" fn extractor_write(extractor: &mut Extractor) -> CResult {
    return CResult::from(extractor.write().map(|_| ()));
}

/// Deflate extracted image.
///
/// # Arguments
/// `out_fd`: File descriptor pointing to rw file. The file will contain
/// deflated image.
/// `in_file`: File descriptor pointing to readable/seekable extracted image file.
/// `verbose_fd`: If valid(>=0), extractor will print information about extracted image to the
/// stream.
#[no_mangle]
pub extern "C" fn extractor_deflate(in_fd: c_int, out_fd: c_int, verbose_fd: c_int) -> CResult {
    let out_file = unsafe { File::from_raw_fd(out_fd) };
    let in_file = unsafe { File::from_raw_fd(in_fd) };
    let verbose_stream: Option<Box<dyn std::io::Write + 'static>> = match verbose_fd < 0 {
        true => None,
        false => unsafe { Some(Box::new(File::from_raw_fd(verbose_fd))) },
    };
    CResult::from(Extractor::deflate(Box::new(in_file), Box::new(out_file), verbose_stream))
}

#[cfg(test)]
mod test {
    use {
        crate::extractor::*,
        disk_extractor_lib::{DataKind, ExtentKind},
        std::{io::Write, os::unix::io::IntoRawFd},
        tempfile::tempfile,
    };

    fn new_file_based_extractor() -> (*mut Extractor, ExtractorOptions, File, File) {
        let options: ExtractorOptions = Default::default();
        let out_file = tempfile().unwrap();
        let mut in_file = tempfile().unwrap();
        for i in 0..64 {
            let buf = vec![i; options.alignment as usize];
            in_file.write_all(&buf).unwrap();
        }

        let mut extractor: *mut Extractor = std::ptr::null_mut();

        assert_eq!(
            extractor_new(
                in_file.try_clone().unwrap().into_raw_fd(),
                Default::default(),
                out_file.try_clone().unwrap().into_raw_fd(),
                &mut extractor,
            )
            .ok,
            true
        );
        (extractor, options, out_file, in_file)
    }

    #[test]
    fn test_new_with_null_output_argument() {
        let (extractor, options, out_file, in_file) = new_file_based_extractor();
        assert_eq!(options, Default::default());
        let result = extractor_new(
            in_file.try_clone().unwrap().into_raw_fd(),
            options,
            out_file.try_clone().unwrap().into_raw_fd(),
            std::ptr::null_mut(),
        );
        assert_eq!(result.ok, false);
        assert_eq!(result.kind, Error::InvalidArgument);
        extractor_delete(extractor);
    }

    #[test]
    fn test_delete_null_does_not_panic() {
        extractor_delete(std::ptr::null_mut());
    }

    #[test]
    fn test_add_zero_size_extent() {
        let (extractor, _, _, _) = new_file_based_extractor();
        let properties = disk_extractor_lib::ExtentProperties {
            extent_kind: ExtentKind::Data,
            data_kind: DataKind::Unmodified,
        };
        assert_eq!(
            extractor_add(unsafe { extractor.as_mut().unwrap() }, 10, 0, properties).kind,
            Error::InvalidRange
        );
        extractor_delete(extractor);
    }

    #[test]
    fn test_write() {
        let (extractor, options, out_file, _) = new_file_based_extractor();
        let properties = disk_extractor_lib::ExtentProperties {
            extent_kind: ExtentKind::Data,
            data_kind: DataKind::Unmodified,
        };

        // Assert that the out_file has zero length.
        assert_eq!(out_file.metadata().unwrap().len(), 0);
        assert_eq!(
            extractor_add(unsafe { extractor.as_mut().unwrap() }, 0, options.alignment, properties)
                .ok,
            true
        );
        let result = extractor_write(unsafe { extractor.as_mut().unwrap() });
        assert_eq!(result.ok, true, "{:?}", result);
        // Assert that something was written to the out_file.
        assert_ne!(out_file.metadata().unwrap().len(), 0);
        extractor_delete(extractor);
    }
}
