modcrawl 0.5.0

Detect, inspect, and analyze Minecraft mods and plugins from JAR files
Documentation
use zipcrawl::ZipManager;

use serde_json::Value;

use super::super::super::dep::types::{DepEntry, DepKind, VersionRange};
use crate::error::Result;

fn ver(value: Value) -> Option<String> {
    match value {
        Value::String(s) => Some(s),
        Value::Array(arr) => arr.into_iter().find_map(|v| v.as_str().map(String::from)),
        _ => None,
    }
}

pub fn extract(mng: &mut ZipManager) -> Result<Vec<DepEntry>> {
    let raw = mng.read_to_string("fabric.mod.json")?;
    let meta = serde_json::from_str::<super::metadata::FabricModMetadata>(&raw)?;
    let mut deps = Vec::new();

    for (name, range) in meta.depends {
        deps.push(DepEntry::new(
            name,
            DepKind::Required,
            VersionRange::parse(ver(range)),
        ));
    }
    for (name, range) in meta.recommends {
        deps.push(DepEntry::new(
            name,
            DepKind::Recommended,
            VersionRange::parse(ver(range)),
        ));
    }
    for (name, range) in meta.suggests {
        deps.push(DepEntry::new(
            name,
            DepKind::Suggested,
            VersionRange::parse(ver(range)),
        ));
    }

    Ok(deps)
}

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::default_trait_access)]
mod tests {
    use std::io::{Cursor, Write};
    use zip::ZipWriter;

    use super::*;
    use crate::core::dep::types::DepKind;

    fn make_zip_bytes(contents: &[(&str, &str)]) -> Vec<u8> {
        let mut buf = Cursor::new(Vec::new());
        let mut zip = ZipWriter::new(&mut buf);
        for (name, content) in contents {
            zip.start_file::<&str, ()>(name, Default::default())
                .unwrap();
            zip.write_all(content.as_bytes()).unwrap();
        }
        zip.finish().unwrap();
        buf.into_inner()
    }

    #[test]
    fn extract_depends() {
        let json = r#"{
            "schemaVersion": 1,
            "id": "testmod",
            "version": "1.0.0",
            "depends": {
                "fabric-api": ">=0.50.0",
                "minecraft": "~1.20.1"
            }
        }"#;
        let bytes = make_zip_bytes(&[("fabric.mod.json", json)]);
        let mut mng = ZipManager::from_reader(&mut Cursor::new(bytes)).unwrap();
        let deps = extract(&mut mng).unwrap();
        assert_eq!(deps.len(), 2);
        assert!(deps
            .iter()
            .any(|d| d.name == "fabric-api" && d.kind == DepKind::Required));
        assert!(deps
            .iter()
            .any(|d| d.name == "minecraft" && d.kind == DepKind::Required));
    }

    #[test]
    fn extract_recommends_and_suggests() {
        let json = r#"{
            "schemaVersion": 1,
            "id": "testmod",
            "version": "1.0.0",
            "recommends": {
                "sodium": "*"
            },
            "suggests": {
                "iris": "*"
            }
        }"#;
        let bytes = make_zip_bytes(&[("fabric.mod.json", json)]);
        let mut mng = ZipManager::from_reader(&mut Cursor::new(bytes)).unwrap();
        let deps = extract(&mut mng).unwrap();
        assert_eq!(deps.len(), 2);
        assert!(deps
            .iter()
            .any(|d| d.name == "sodium" && d.kind == DepKind::Recommended));
        assert!(deps
            .iter()
            .any(|d| d.name == "iris" && d.kind == DepKind::Suggested));
    }

    #[test]
    fn extract_empty_when_no_deps() {
        let json = r#"{"schemaVersion": 1, "id": "testmod", "version": "1.0.0"}"#;
        let bytes = make_zip_bytes(&[("fabric.mod.json", json)]);
        let mut mng = ZipManager::from_reader(&mut Cursor::new(bytes)).unwrap();
        let deps = extract(&mut mng).unwrap();
        assert!(deps.is_empty());
    }
}