cobble_core/minecraft/loader_mods/
mod.rs

1mod loader_mod;
2
3use crate::error::{CobbleError, CobbleResult};
4use crate::utils::Either;
5use async_zip::read::seek::ZipFileReader;
6use futures::TryStreamExt;
7pub use loader_mod::*;
8use std::path::{Path, PathBuf};
9use tokio::fs::{read_dir, File};
10use tokio_stream::wrappers::ReadDirStream;
11
12/// Loads all loader mods from the minecraft folder.
13#[cfg_attr(doc_cfg, doc(cfg(feature = "loader-mods")))]
14#[instrument(
15    name = "load_loader_mods",
16    level = "debug",
17    skip_all,
18    fields(minecraft_path)
19)]
20pub async fn load_loader_mods(loader_mods_path: impl AsRef<Path>) -> CobbleResult<Vec<LoaderMod>> {
21    if !loader_mods_path.as_ref().is_dir() {
22        trace!("Loader mods directory is empty");
23        return Ok(vec![]);
24    }
25
26    trace!("Loading loader mod files...");
27    let file_stream = ReadDirStream::new(read_dir(loader_mods_path).await?);
28    let loader_mods = file_stream
29        .map_err(CobbleError::from)
30        .try_filter_map(|e| parse_loader_mod(e.path()))
31        .try_collect()
32        .await?;
33
34    Ok(loader_mods)
35}
36
37#[instrument(name = "parse_loader_mod", level = "trace", skip_all, fields(path,))]
38pub(crate) async fn parse_loader_mod(path: impl AsRef<Path>) -> CobbleResult<Option<LoaderMod>> {
39    // Check if file
40    if !path.as_ref().is_file() {
41        trace!("Entry is not a file.");
42        return Ok(None);
43    }
44
45    // Check if jar archive
46    let mime = match mime_guess::from_path(&path).first() {
47        Some(mime) => mime,
48        None => {
49            trace!("Could not get MIME type for file.");
50            return Ok(None);
51        }
52    };
53    if mime != "application/java-archive" {
54        trace!("Entry is not a jar archive");
55        return Ok(None);
56    }
57
58    // Parse
59    let enabled = !path.as_ref().ends_with(LoaderMod::DISABLED_JAR_SUFFIX);
60    let mut file = File::open(&path).await?;
61    let mut archive = ZipFileReader::new(&mut file).await?;
62
63    let metadata = match archive.entry("fabric.mod.json") {
64        Some((i, _entry)) => {
65            trace!("Found 'fabric.mod.json'");
66            let entry_reader = archive.entry_reader(i).await?;
67            let bytes = entry_reader.read_to_end_crc().await?;
68            serde_json::from_slice::<ModMetadata>(&bytes)?
69        }
70        None => {
71            trace!("Archive is not a valid fabric mod (missing 'fabric.mod.json')");
72            return Ok(None);
73        }
74    };
75
76    let icon_path = metadata.icon.and_then(|i| match i {
77        Either::Left(l) => Some(l),
78        Either::Right(_) => {
79            // TODO: Implement icon from HashMap
80            warn!("Reading mod icon from map is not yet implemented!");
81            None
82        }
83    });
84
85    Ok(Some(LoaderMod {
86        name: metadata.name.unwrap_or(metadata.id),
87        version: metadata.version,
88        description: metadata.description,
89        icon_path,
90        path: PathBuf::from(path.as_ref()),
91        enabled,
92    }))
93}