downloader_mc 0.1.3

A library for downloading Minecraft assets and libraries.
Documentation
use std::{collections::HashMap, fs};

use serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::error::ManifestError;

#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all(deserialize = "camelCase"))]
pub struct ManifestAssetIndex {
    pub id: String,
    pub sha1: String,
    pub size: i32,
    pub total_size: i32,
    pub url: String,
}

#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all(deserialize = "camelCase"))]
pub struct ManifestComponent {
    pub component: String,
    pub major_version: i8,
}

#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all(deserialize = "camelCase"))]
pub struct ManifestFile {
    pub path: Option<String>,
    pub sha1: String,
    pub size: u64,
    pub url: String,
}

#[derive(Clone, Serialize, Deserialize)]
pub struct ManifestDownloads {
    pub client: ManifestFile,
    pub client_mappings: Option<ManifestFile>,
    pub server: ManifestFile,
    pub server_mappings: Option<ManifestFile>,
}

#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all(deserialize = "camelCase"))]
pub struct ManifestRule {
    pub action: String,
    pub os: Option<HashMap<String, String>>,
    pub features: Option<HashMap<String, Value>>,
}

#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all(deserialize = "camelCase"))]
pub struct ManifestLibraryDownloads {
    pub artifact: Option<ManifestFile>,
}

#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all(deserialize = "camelCase"))]
pub struct ManifestLibrary {
    pub downloads: ManifestLibraryDownloads,
    pub name: String,
    pub rules: Option<Vec<ManifestRule>>,
}

#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all(deserialize = "camelCase"))]
pub struct FabricManifestLibrary {
    pub name: String,
    pub url: String,
    pub sha1: Option<String>,
    pub size: Option<u64>,
}

#[derive(Clone, Deserialize, Serialize)]
pub struct Rules {
    pub action: String,
    pub features: Option<Features>,
    pub os: Option<Os>,
}

#[derive(Clone, Deserialize, Serialize)]
pub struct Features {
    pub is_demo_user: Option<bool>,
    pub has_custom_resolution: Option<bool>,
    pub is_quick_play_realms: Option<bool>,
}

#[derive(Clone, Deserialize, Serialize)]
pub struct Os {
    pub arch: Option<String>,
    pub name: Option<String>,
    pub version: Option<String>,
}

#[derive(Clone, Deserialize, Serialize)]
pub struct Arguments {
    pub game: Vec<JvmArgument>,
    pub jvm: Vec<JvmArgument>,
}

#[derive(Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum JvmArgument {
    String(String),
    Struct {
        rules: Vec<Rules>,
        value: serde_json::Value,
    },
}

#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum VersionType {
    Release,
    Snapshot,
    OldBeta,
    OldAlpha,
}

#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all(deserialize = "camelCase"))]
pub struct Manifest {
    pub arguments: Arguments,
    pub asset_index: ManifestAssetIndex,
    pub assets: String,
    pub compliance_level: i8,
    pub downloads: ManifestDownloads,
    pub id: String,
    pub java_version: ManifestComponent,
    pub libraries: Vec<ManifestLibrary>,
    pub main_class: String,
    pub minimum_launcher_version: i8,
    pub release_time: String,
    pub time: String,
    #[serde(rename = "type")]
    pub type_: VersionType,
}

#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all(deserialize = "camelCase"))]
pub struct FabricManifest {
    pub arguments: Arguments,
    pub inherits_from: String,
    pub id: String,
    pub libraries: Vec<FabricManifestLibrary>,
    pub main_class: String,
    pub release_time: String,
    pub time: String,
    #[serde(rename = "type")]
    pub type_: VersionType,
}

fn maven_to_path(coordinate: &str) -> String {
    let parts: Vec<&str> = coordinate.split(':').collect();
    if parts.len() != 3 {
        panic!("Invalid format");
    }
    let group = parts[0].replace('.', "/");
    let artifact = parts[1];
    let version = parts[2];
    format!(
        "{}/{}/{}/{}/{}-{}.jar",
        group, artifact, version, artifact, artifact, version
    )
}

pub fn manifest_from_fabric(
    fabric_manifest: FabricManifest,
    base_manifest: &mut Manifest,
) -> Result<Manifest, ManifestError> {
    let fabric_libraries: Vec<ManifestLibrary> = fabric_manifest
        .libraries
        .into_iter()
        .map(|lib| {
            let path = maven_to_path(&lib.name.clone());
            let sha1 = lib.sha1.unwrap_or_else(|| "".to_string());
            let size = lib.size.unwrap_or(1_i64 as u64);

            ManifestLibrary {
                name: lib.name.clone(),
                downloads: ManifestLibraryDownloads {
                    artifact: Some(ManifestFile {
                        path: Some(path),
                        sha1: sha1,
                        size: size,
                        url: format!("{}{}", lib.url, maven_to_path(&lib.name)),
                    }),
                },
                rules: None,
            }
        })
        .collect();

    let mut combined_libraries = fabric_libraries;
    combined_libraries.extend(base_manifest.libraries.clone());

    let mut combined_game_args = base_manifest.arguments.game.clone();
    combined_game_args.extend(fabric_manifest.arguments.game);

    let mut combined_jvm_args = base_manifest.arguments.jvm.clone();
    combined_jvm_args.extend(fabric_manifest.arguments.jvm);

    Ok(Manifest {
        arguments: Arguments {
            game: combined_game_args,
            jvm: combined_jvm_args,
        },
        libraries: combined_libraries,
        main_class: fabric_manifest.main_class,
        release_time: fabric_manifest.release_time,
        time: fabric_manifest.time,
        type_: fabric_manifest.type_,
        ..base_manifest.clone()
    })
}

pub fn read_manifest_from_str(string: &str) -> Result<Manifest, ManifestError> {
    let manifest: Manifest = serde_json::from_str(string)?;
    Ok(manifest)
}

pub fn read_manifest_from_file(file: &str) -> Result<Manifest, ManifestError> {
    let raw = fs::read_to_string(file)?;
    let manifest: Manifest = read_manifest_from_str(&raw)?;
    Ok(manifest)
}

impl ToString for VersionType {
    fn to_string(&self) -> String {
        match *self {
            VersionType::Release => String::from("Release"),
            VersionType::Snapshot => String::from("Snapshot"),
            VersionType::OldAlpha | VersionType::OldBeta => String::from("Old"),
        }
    }
}

#[cfg(test)]
mod tests {
    use serde::{Deserialize, Serialize};

    use super::VersionType;

    #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
    #[serde(rename_all(deserialize = "camelCase"))]
    struct TestStruct {
        #[serde(rename = "type")]
        type_: VersionType,
    }

    #[test]
    fn version_type_serialize() {
        let st = TestStruct {
            type_: VersionType::Release,
        };
        let expected_json = r#"{"type":"release"}"#;
        let json = serde_json::to_string(&st);

        assert!(json.is_ok());
        assert_eq!(json.unwrap(), expected_json);
    }

    #[test]
    fn version_type_serialize_snake_case() {
        let st = TestStruct {
            type_: VersionType::OldAlpha,
        };
        let expected_json = r#"{"type":"old_alpha"}"#;
        let json = serde_json::to_string(&st);

        assert!(json.is_ok());
        assert_eq!(json.unwrap(), expected_json);
    }

    #[test]
    fn version_type_deserialize() {
        let raw_json = r#"{"type":"old_beta"}"#;
        let expected_st = TestStruct {
            type_: VersionType::OldBeta,
        };
        let json = serde_json::from_str::<TestStruct>(raw_json);

        assert!(json.is_ok());
        assert_eq!(json.unwrap(), expected_st);
    }
}