gpcas_base 0.3.0

Common definitions and utilities for GPCAS
Documentation
// Filename: file.rs
// Version:	 0.2
// Date:	 24-09-2021 (DD-MM-YYYY)
// Library:  gpcas_base
//
// Copyright (c) 2021 Kai Rese
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this program. If not, see
// <https://www.gnu.org/licenses/>.

//! Structure and functions for handling GPCAS files.

use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::convert::TryFrom;

/// Deserializes parsed data into a specific type.
pub type DeserializeFunction<T> = fn(EncodedStructData) -> Result<T, Box<dyn std::error::Error>>;

/// A structure that can be deserialized from and serialized to a GPCAS file.
pub trait GpcasFileStruct: 'static + DeserializeOwned + Serialize {
    /// A string used as identifier in every file of this type.
    const FILE_IDENTIFIER: &'static str;
    /// The current file version.
    const CURRENT_FILE_VERSION: usize;
    /// A slice of compatible file versions, along with a pointer to a function that deserializes
    /// the inner data of a GPCAS file into this type.
    ///
    /// User should implement this as a slice of a static array that is defined like so:
    /// ```
    /// # use gpcas_base::file::{
    /// #   DeserializeFunction,
    /// #   GpcasFileStruct,
    /// #   EncodedStructData,
    /// #   deserialize_upgrade_from
    /// # };
    /// # use serde::{Deserialize, Serialize};
    /// #
    /// # #[derive(Deserialize, Serialize)]
    /// # struct MyGpcasFileStruct;
    /// # #[derive(Deserialize, Serialize)]
    /// # struct SomeOldGpcasFileStruct;
    /// # #[derive(Deserialize, Serialize)]
    /// # struct AnotherOldGpcasFileStruct;
    /// #
    /// # impl GpcasFileStruct for MyGpcasFileStruct {
    /// #   const FILE_IDENTIFIER: &'static str = "";
    /// #   const CURRENT_FILE_VERSION: usize = 0;
    /// #   const COMPATIBLE_VERSIONS: &'static [(usize, DeserializeFunction<Self>)] = &VERSIONS;
    /// # }
    /// #
    /// # impl From<SomeOldGpcasFileStruct> for MyGpcasFileStruct {
    /// #   fn from(_: SomeOldGpcasFileStruct) -> Self {
    /// #     MyGpcasFileStruct
    /// #   }
    /// # }
    /// #
    /// # impl From<AnotherOldGpcasFileStruct> for MyGpcasFileStruct {
    /// #   fn from(_: AnotherOldGpcasFileStruct) -> Self {
    /// #     MyGpcasFileStruct
    /// #   }
    /// # }
    /// #
    /// const VERSIONS: [(usize, DeserializeFunction<MyGpcasFileStruct>); 2] = [
    ///   (10, deserialize_upgrade_from::<SomeOldGpcasFileStruct, MyGpcasFileStruct>),
    ///   (11, deserialize_upgrade_from::<AnotherOldGpcasFileStruct, MyGpcasFileStruct>)
    /// ];
    /// ```
    const COMPATIBLE_VERSIONS: &'static [(usize, DeserializeFunction<Self>)];
}

/// A file type which has support for serialization and deserialization.
pub enum FileEncodingType {
    /// Uses the `bincode` crate for serialization and deserialization.
    Binary,
    /// The file has a JSON format.
    Json,
}

/// Data of a GPCAS file struct.
///
/// This is used so already parsed data, such as JSON objects, can be used for deserialization.
pub enum EncodedStructData<'a> {
    /// The data is encoded in binary.
    Binary(&'a [u8]),
    /// The data is encoded as a parsed JSON object.
    Json(serde_json::Value),
}

/// The representation and structure of a gpcas file. Wraps the content behind a header.
#[derive(Deserialize, Serialize)]
pub struct GpcasFile {
    /// Identification string of the file type.
    pub identifier: String,
    /// The version of the content structure.
    pub version: usize,
    /// The actual, serialized content in form of a [`GpcasFileStruct`].
    pub content: Vec<u8>,
}

/// Error returned when deserialization fails.
#[derive(Debug)]
pub enum DeserializationError {
    /// Deserialization of the inner data failed. Includes the inner error.
    CorruptFileData(Box<dyn std::error::Error>),
    /// The version of the file is either too old or too new for use with this software version.
    IncompatibleFileVersion(usize),
    /// The data is no valid gpcas file.
    NoGpcasFile,
    /// The file has a different gpcas file type than expected.
    /// Fields contain the expected and the actual identifier.
    WrongFileType(&'static str, String),
}

/// Deserializes raw data of a GPCAS file into the corresponding struct.
pub fn deserialize<T: GpcasFileStruct>(
    raw: &[u8],
    encoding_type: FileEncodingType,
) -> Result<T, DeserializationError> {
    match encoding_type {
        FileEncodingType::Binary => {
            if let Ok(gpcas_file) = bincode::deserialize::<GpcasFile>(raw) {
                if gpcas_file.identifier.as_str() != T::FILE_IDENTIFIER {
                    return Err(DeserializationError::WrongFileType(
                        T::FILE_IDENTIFIER,
                        gpcas_file.identifier,
                    ));
                }

                if gpcas_file.version == T::CURRENT_FILE_VERSION {
                    bincode::deserialize::<T>(gpcas_file.content.as_slice())
                        .map_err(|e| DeserializationError::CorruptFileData(Box::new(e)))
                } else if let Ok(index) =
                    T::COMPATIBLE_VERSIONS.binary_search_by_key(&gpcas_file.version, |(i, _)| *i)
                {
                    T::COMPATIBLE_VERSIONS[index].1(EncodedStructData::Binary(
                        gpcas_file.content.as_slice(),
                    ))
                    .map_err(|e| DeserializationError::CorruptFileData(e))
                } else {
                    Err(DeserializationError::IncompatibleFileVersion(
                        gpcas_file.version,
                    ))
                }
            } else {
                Err(DeserializationError::NoGpcasFile)
            }
        }
        FileEncodingType::Json => {
            if let Ok(wrapper) = serde_json::from_slice::<serde_json::Value>(raw) {
                if wrapper.is_object()
                    && wrapper.as_object().unwrap().len() == 3
                    && wrapper.as_object().unwrap().contains_key("identifier")
                    && wrapper.as_object().unwrap().contains_key("version")
                    && wrapper.as_object().unwrap().contains_key("data")
                    && wrapper.as_object().unwrap()["identifier"].is_string()
                    && wrapper.as_object().unwrap()["version"].is_u64()
                    && wrapper.as_object().unwrap()["data"].is_object()
                {
                    let wrapper = wrapper.as_object().unwrap();
                    if wrapper["identifier"].as_str().unwrap() != T::FILE_IDENTIFIER {
                        return Err(DeserializationError::WrongFileType(
                            T::FILE_IDENTIFIER,
                            wrapper["identifier"].as_str().unwrap().to_string(),
                        ));
                    }

                    let version = wrapper["version"].as_u64().unwrap() as usize;
                    if version == T::CURRENT_FILE_VERSION {
                        serde_json::from_value(wrapper["data"].clone())
                            .map_err(|e| DeserializationError::CorruptFileData(Box::new(e)))
                    } else if let Ok(index) =
                        T::COMPATIBLE_VERSIONS.binary_search_by_key(&version, |(i, _)| *i)
                    {
                        T::COMPATIBLE_VERSIONS[index].1(EncodedStructData::Json(
                            wrapper["data"].clone(),
                        ))
                        .map_err(|e| DeserializationError::CorruptFileData(e))
                    } else {
                        Err(DeserializationError::IncompatibleFileVersion(
                            wrapper["version"].as_u64().unwrap() as usize,
                        ))
                    }
                } else {
                    Err(DeserializationError::NoGpcasFile)
                }
            } else {
                Err(DeserializationError::NoGpcasFile)
            }
        }
    }
}

/// Deserializes an old version of the file struct and upgrades it into the current one.
///
/// This should really only be used in the context of an array for
/// [`GpcasFileStruct::COMPATIBLE_VERSIONS`].
pub fn deserialize_upgrade_from<Old, Current>(
    data: EncodedStructData,
) -> Result<Current, Box<dyn std::error::Error>>
where
    Old: DeserializeOwned,
    Current: GpcasFileStruct + From<Old>,
{
    match data {
        EncodedStructData::Binary(bytes) => Ok(bincode::deserialize::<Old>(bytes)
            .map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?
            .into()),
        EncodedStructData::Json(value) => Ok(serde_json::from_value::<Old>(value)
            .map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?
            .into()),
    }
}

/// Serializes a GPCAS file struct into the raw data of a full GPCAS file.
pub fn serialise<T: GpcasFileStruct>(from: &T, encoding_type: FileEncodingType) -> Vec<u8> {
    match encoding_type {
        FileEncodingType::Binary => bincode::serialize(&GpcasFile {
            identifier: T::FILE_IDENTIFIER.to_string(),
            version: T::CURRENT_FILE_VERSION,
            content: bincode::serialize(from).unwrap(),
        })
        .unwrap(),
        FileEncodingType::Json => {
            let mut map = serde_json::Map::with_capacity(3);
            map.insert(
                "identifier".to_string(),
                serde_json::Value::String(T::FILE_IDENTIFIER.to_string()),
            );
            map.insert(
                "version".to_string(),
                serde_json::Value::Number(serde_json::Number::from(T::CURRENT_FILE_VERSION)),
            );
            map.insert(
                "data".to_string(),
                serde_json::value::to_value(from).unwrap(),
            );
            serde_json::to_vec_pretty(&serde_json::Value::Object(map)).unwrap()
        }
    }
}

impl TryFrom<&str> for FileEncodingType {
    type Error = ();

    fn try_from(raw: &str) -> Result<Self, Self::Error> {
        match raw.to_ascii_lowercase().as_str() {
            "binary" | "bin" => Ok(FileEncodingType::Binary),
            "json" => Ok(FileEncodingType::Json),
            _ => Err(()),
        }
    }
}

impl std::fmt::Display for DeserializationError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            DeserializationError::CorruptFileData(e) => {
                write!(
                    f,
                    "Couldn't read file data as there was an error during deserialization: \"{}\"",
                    e
                )
            }
            DeserializationError::IncompatibleFileVersion(version) => {
                write!(
                    f,
                    "Couldn't read file because the file version ({}) is incompatible with this software version.",
                    version
                )
            }
            DeserializationError::NoGpcasFile => {
                write!(f, "Couldn't read file because it is no valid gpcas file.")
            }
            DeserializationError::WrongFileType(actual, expected) => {
                write!(
                    f,
                    "Couldn't read file because of a wrong file type: expected {}, found {}",
                    expected, actual
                )
            }
        }
    }
}

impl std::error::Error for DeserializationError {}