cobble-core 1.2.0

Library for managing, installing and launching Minecraft instances and more.
Documentation
mod loader_mod;

use crate::error::{CobbleError, CobbleResult};
use crate::utils::Either;
use async_zip::read::seek::ZipFileReader;
use futures::TryStreamExt;
pub use loader_mod::*;
use std::path::{Path, PathBuf};
use tokio::fs::{read_dir, File};
use tokio_stream::wrappers::ReadDirStream;

/// Loads all loader mods from the minecraft folder.
#[cfg_attr(doc_cfg, doc(cfg(feature = "loader-mods")))]
#[instrument(
    name = "load_loader_mods",
    level = "debug",
    skip_all,
    fields(minecraft_path)
)]
pub async fn load_loader_mods(loader_mods_path: impl AsRef<Path>) -> CobbleResult<Vec<LoaderMod>> {
    if !loader_mods_path.as_ref().is_dir() {
        trace!("Loader mods directory is empty");
        return Ok(vec![]);
    }

    trace!("Loading loader mod files...");
    let file_stream = ReadDirStream::new(read_dir(loader_mods_path).await?);
    let loader_mods = file_stream
        .map_err(CobbleError::from)
        .try_filter_map(|e| parse_loader_mod(e.path()))
        .try_collect()
        .await?;

    Ok(loader_mods)
}

#[instrument(name = "parse_loader_mod", level = "trace", skip_all, fields(path,))]
pub(crate) async fn parse_loader_mod(path: impl AsRef<Path>) -> CobbleResult<Option<LoaderMod>> {
    // Check if file
    if !path.as_ref().is_file() {
        trace!("Entry is not a file.");
        return Ok(None);
    }

    // Check if jar archive
    let mime = match mime_guess::from_path(&path).first() {
        Some(mime) => mime,
        None => {
            trace!("Could not get MIME type for file.");
            return Ok(None);
        }
    };
    if mime != "application/java-archive" {
        trace!("Entry is not a jar archive");
        return Ok(None);
    }

    // Parse
    let enabled = !path.as_ref().ends_with(LoaderMod::DISABLED_JAR_SUFFIX);
    let mut file = File::open(&path).await?;
    let mut archive = ZipFileReader::new(&mut file).await?;

    let metadata = match archive.entry("fabric.mod.json") {
        Some((i, _entry)) => {
            trace!("Found 'fabric.mod.json'");
            let entry_reader = archive.entry_reader(i).await?;
            let bytes = entry_reader.read_to_end_crc().await?;
            serde_json::from_slice::<ModMetadata>(&bytes)?
        }
        None => {
            trace!("Archive is not a valid fabric mod (missing 'fabric.mod.json')");
            return Ok(None);
        }
    };

    let icon_path = metadata.icon.and_then(|i| match i {
        Either::Left(l) => Some(l),
        Either::Right(_) => {
            // TODO: Implement icon from HashMap
            warn!("Reading mod icon from map is not yet implemented!");
            None
        }
    });

    Ok(Some(LoaderMod {
        name: metadata.name.unwrap_or(metadata.id),
        version: metadata.version,
        description: metadata.description,
        icon_path,
        path: PathBuf::from(path.as_ref()),
        enabled,
    }))
}