uranium-rs 1.0.4

Lib for downloading/making minecraft and modpacks
Documentation
use std::path::Path;

use crate::downloaders::{Downloader, FileDownloader, get_lib_path};
use crate::error::Result;

use crate::downloaders::get_index_path;
use crate::{downloaders::DownloadableObject, version_checker::VersionCheckResult};

pub struct InstallationFixer {
    data: Vec<DownloadableObject>,
}

impl InstallationFixer {
    pub fn new(check_result: VersionCheckResult, installation_path: impl AsRef<Path>) -> Self {
        let installation_path = installation_path.as_ref();
        let mut fixer = Self { data: vec![] };
        fixer.add_objects(&check_result, installation_path);
        fixer.add_libs(&check_result, installation_path);
        fixer.add_index(&check_result, installation_path);
        fixer
    }

    pub async fn fix_installation(&mut self) -> Result<()> {
        let files = std::mem::take(&mut self.data);

        let mut downloader = Downloader::new(files);
        downloader.complete().await
    }

    fn add_objects(&mut self, check_result: &VersionCheckResult, installation_path: &Path) {
        self.data.extend(
            check_result
                .objects
                .iter()
                .map(|&f| DownloadableObject::from(f))
                .map(|mut f| {
                    f.path = installation_path.join(f.path);
                    f
                }),
        );
    }

    fn add_libs(&mut self, check_result: &VersionCheckResult, installation_path: &Path) {
        self.data.extend(
            check_result
                .libs
                .iter()
                .map(|&f| DownloadableObject::from(f))
                .map(|mut f| {
                    f.path = get_lib_path(installation_path, &f.path);
                    f
                }),
        );
    }

    fn add_index(&mut self, check_result: &VersionCheckResult, installation_path: &Path) {
        if let Some(mut idx) = check_result.index.map(DownloadableObject::from) {
            idx.path = get_index_path(installation_path, &idx.path);
            self.data.push(idx);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::mine_data_structs::minecraft::{Artifact, AssetIndex, Library, LibraryDownloads, ObjectData};

    fn make_library(path: &str, sha1: &str) -> Library {
        Library {
            name: "test:lib:1.0".into(),
            downloads: Some(LibraryDownloads {
                artifact: Artifact {
                    path: path.into(),
                    sha1: sha1.into(),
                    size: 1024,
                    url: "https://example.com/lib.jar".into(),
                },
            }),
            rules: None,
        }
    }

    fn make_object(hash: &str) -> ObjectData {
        ObjectData { hash: hash.into(), size: 512 }
    }

    fn make_index(id: &str, sha1: &str) -> AssetIndex {
        AssetIndex {
            id: id.into(),
            sha1: sha1.into(),
            size: 4096,
            total_size: 100_000,
            url: "https://example.com/index.json".into(),
        }
    }

    #[test]
    fn adds_objects() {
        let obj = make_object("a1b2c3d4e5f6");
        let result = VersionCheckResult {
            objects: Box::new([&obj]),
            libs: Box::new([]),
            index: None,
            client: None,
        };
        let fixer = InstallationFixer::new(result, "/tmp/test");

        assert_eq!(fixer.data.len(), 1);
        let entry = &fixer.data[0];
        assert!(
            entry.path.starts_with("/tmp/test"),
            "expected path to start with installation path, got: {:?}",
            entry.path,
        );
        assert!(
            entry.path.to_string_lossy().contains("a1b2c3d4e5f6"),
            "expected path to contain the object hash, got: {:?}",
            entry.path,
        );
        assert!(
            entry.url.contains("resources.download.minecraft.net"),
            "expected URL to be a Minecraft resource URL, got: {}",
            entry.url,
        );
        assert!(entry.hash.is_some());
    }

    #[test]
    fn adds_libs() {
        let lib = make_library("net/minecraft/client/1.21/client.jar", "ffgg1122");
        let result = VersionCheckResult {
            objects: Box::new([]),
            libs: Box::new([&lib]),
            index: None,
            client: None,
        };
        let fixer = InstallationFixer::new(result, "/tmp/test");

        assert_eq!(fixer.data.len(), 1);
        let entry = &fixer.data[0];
        assert!(
            entry.path.starts_with("/tmp/test/libraries"),
            "expected path under libraries/, got: {:?}",
            entry.path,
        );
        assert!(
            entry.path.to_string_lossy().contains("net/minecraft/client/1.21/client.jar"),
            "expected path to contain artifact path, got: {:?}",
            entry.path,
        );
        assert_eq!(entry.url, "https://example.com/lib.jar");
        assert!(entry.hash.is_some());
    }

    #[test]
    fn all_sources_accumulate() {
        let obj = make_object("aaa111");
        let lib = make_library("org/example/foo/1.0/foo.jar", "bbb222");
        let idx = make_index("1.21", "ccc333");
        let result = VersionCheckResult {
            objects: Box::new([&obj]),
            libs: Box::new([&lib]),
            index: Some(&idx),
            client: None,
        };
        let fixer = InstallationFixer::new(result, "/tmp/test");

        assert_eq!(fixer.data.len(), 3);
        let paths: Vec<_> = fixer.data.iter().map(|d| d.path.to_string_lossy().to_string()).collect();
        assert!(paths.iter().any(|p| p.contains("aaa111")),     "object missing");
        assert!(paths.iter().any(|p| p.contains("org/example")), "library missing");
        assert!(paths.iter().any(|p| p.contains("1.21")),       "index id missing");
    }

    #[tokio::test]
    async fn fix_installation_empty_returns_ok() {
        let result = VersionCheckResult {
            objects: Box::new([]),
            libs: Box::new([]),
            index: None,
            client: None,
        };
        let mut fixer = InstallationFixer::new(result, "/tmp/test");

        let outcome = fixer.fix_installation().await;

        assert!(outcome.is_ok(), "expected Ok(()) with no downloads, got: {:?}", outcome);
        assert!(fixer.data.is_empty());
    }
}