CI / fmt (rustfmt --check) (push) Canceled after 0s
CI / clippy (ubuntu-latest) (push) Canceled after 0s
CI / clippy (windows-latest) (push) Canceled after 0s
CI / test (ubuntu-latest) (push) Canceled after 0s
CI / test (windows-latest) (push) Canceled after 0s
CI / build release (ubuntu-latest) (push) Canceled after 0s
CI / build release (windows-latest) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Rust 原生单文件 Minecraft 启动器:CLI 完整可用(install/launch/accounts/java/diagnose/agent), MCP server 接入外部 Agent 工具,GUI 开发中。
187 lines
6.6 KiB
Rust
187 lines
6.6 KiB
Rust
//! 版本 JSON 解析:宽松解析、未知字段保真、旧版字段并存。
|
|
|
|
use unlucky_core::game::arguments::{Argument, ArgumentValue, RuleAction};
|
|
use unlucky_core::game::manifest::VersionJson;
|
|
use unlucky_core::game::version::ReleaseType;
|
|
|
|
use crate::helpers::parse;
|
|
|
|
const VANILLA_1_20_1: &str = r#"{
|
|
"id": "1.20.1",
|
|
"type": "release",
|
|
"time": "2023-06-12T13:25:51+00:00",
|
|
"releaseTime": "2023-06-12T13:25:51+00:00",
|
|
"complianceLevel": 1,
|
|
"minimumLauncherVersion": 21,
|
|
"mainClass": "net.minecraft.client.main.Main",
|
|
"assets": "5",
|
|
"assetIndex": {
|
|
"id": "5",
|
|
"sha1": "a3b3b1b0e0d8f2a8f0fbe1a4b0a8f6a1c6c6a7a2",
|
|
"size": 404297,
|
|
"totalSize": 598385820,
|
|
"url": "https://piston-meta.mojang.com/v1/packages/sha/5.json"
|
|
},
|
|
"javaVersion": { "component": "java-runtime-gamma", "majorVersion": 17 },
|
|
"downloads": {
|
|
"client": { "sha1": "abc", "size": 100, "url": "https://example.invalid/client.jar" },
|
|
"client_mappings": { "sha1": "def", "size": 50, "url": "https://example.invalid/mappings.txt" }
|
|
},
|
|
"arguments": {
|
|
"game": [
|
|
"--username",
|
|
"${auth_player_name}",
|
|
{ "rules": [{ "action": "allow", "features": { "has_custom_resolution": true } }],
|
|
"value": ["--width", "${resolution_width}"] }
|
|
],
|
|
"jvm": [
|
|
{ "rules": [{ "action": "allow", "os": { "name": "windows", "version": "^10\\." } }],
|
|
"value": "-Dos.name=Windows 10" }
|
|
]
|
|
},
|
|
"libraries": [
|
|
{ "name": "com.mojang:logging:1.1.1",
|
|
"downloads": { "artifact": { "path": "com/mojang/logging/1.1.1/logging-1.1.1.jar",
|
|
"sha1": "aaa", "size": 10,
|
|
"url": "https://example.invalid/logging.jar" } } }
|
|
],
|
|
"logging": {
|
|
"client": { "argument": "-Dlog4j.configurationFile=${path}",
|
|
"file": { "id": "client-1.12.xml", "sha1": "bbb", "size": 20,
|
|
"url": "https://example.invalid/log4j.xml" },
|
|
"type": "log4j2-xml" }
|
|
},
|
|
"tlauncherVersion": 1,
|
|
"MMC-hint": "local"
|
|
}"#;
|
|
|
|
#[test]
|
|
fn parses_full_version_json() {
|
|
let version = parse(VANILLA_1_20_1);
|
|
|
|
assert_eq!(version.id.as_deref(), Some("1.20.1"));
|
|
assert_eq!(version.version_type, Some(ReleaseType::Release));
|
|
assert_eq!(
|
|
version.main_class.as_deref(),
|
|
Some("net.minecraft.client.main.Main")
|
|
);
|
|
assert_eq!(version.compliance_level, Some(1));
|
|
assert_eq!(version.minimum_launcher_version, Some(21));
|
|
assert_eq!(version.assets_id(), "5");
|
|
|
|
let asset_index = version.asset_index.as_ref().expect("assetIndex 应解析");
|
|
assert_eq!(asset_index.id, "5");
|
|
assert_eq!(asset_index.total_size, Some(598_385_820));
|
|
|
|
let java = version.java_version.as_ref().expect("javaVersion 应解析");
|
|
assert_eq!(java.component.as_deref(), Some("java-runtime-gamma"));
|
|
assert_eq!(java.major_version, Some(17));
|
|
|
|
let downloads = version.downloads.as_ref().expect("downloads 应解析");
|
|
assert_eq!(downloads.len(), 2, "未知键也要保留在映射里");
|
|
assert_eq!(
|
|
downloads.client().and_then(|c| c.url.as_deref()),
|
|
Some("https://example.invalid/client.jar")
|
|
);
|
|
assert!(downloads.get("client_mappings").is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn keeps_unknown_fields_verbatim() {
|
|
let version = parse(VANILLA_1_20_1);
|
|
// 第三方启动器私有字段(rawJson 保真,analyse/03 §4.2)。
|
|
assert_eq!(
|
|
version.extra.get("tlauncherVersion"),
|
|
Some(&serde_json::json!(1))
|
|
);
|
|
assert_eq!(
|
|
version.extra.get("MMC-hint"),
|
|
Some(&serde_json::json!("local"))
|
|
);
|
|
|
|
let roundtrip = version.to_json_string().expect("应可序列化");
|
|
let reparsed = VersionJson::from_json_str(&roundtrip).expect("应可再解析");
|
|
assert_eq!(reparsed.extra, version.extra);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_arguments_with_rules() {
|
|
let version = parse(VANILLA_1_20_1);
|
|
let arguments = version.arguments.as_ref().expect("arguments 应解析");
|
|
|
|
assert_eq!(arguments.game.len(), 3);
|
|
assert_eq!(arguments.jvm.len(), 1);
|
|
assert!(matches!(&arguments.game[0], Argument::Plain(value) if value == "--username"));
|
|
|
|
let conditional = match &arguments.game[2] {
|
|
Argument::Conditional(cond) => cond,
|
|
Argument::Plain(_) => panic!("第三项应是条件参数"),
|
|
};
|
|
assert_eq!(conditional.rules.len(), 1);
|
|
assert_eq!(conditional.rules[0].action, RuleAction::Allow);
|
|
assert_eq!(
|
|
conditional.rules[0]
|
|
.features
|
|
.as_ref()
|
|
.and_then(|f| f.get("has_custom_resolution")),
|
|
Some(&true)
|
|
);
|
|
assert_eq!(
|
|
conditional.value,
|
|
ArgumentValue::Many(vec!["--width".into(), "${resolution_width}".into()])
|
|
);
|
|
|
|
// jvm 里的 os 限制带正则版本模式。
|
|
let jvm = match &arguments.jvm[0] {
|
|
Argument::Conditional(cond) => cond,
|
|
Argument::Plain(_) => panic!("jvm 第一项应是条件参数"),
|
|
};
|
|
let os = jvm.rules[0].os.as_ref().expect("应有 os 限制");
|
|
assert_eq!(os.name.as_deref(), Some("windows"));
|
|
assert_eq!(os.version.as_deref(), Some("^10\\."));
|
|
}
|
|
|
|
#[test]
|
|
fn parses_libraries_and_logging() {
|
|
let version = parse(VANILLA_1_20_1);
|
|
let libraries = version.libraries.as_ref().expect("libraries 应解析");
|
|
assert_eq!(libraries.len(), 1);
|
|
let artifact = libraries[0].artifact().expect("应有 artifact");
|
|
assert_eq!(
|
|
artifact.path.as_deref(),
|
|
Some("com/mojang/logging/1.1.1/logging-1.1.1.jar")
|
|
);
|
|
|
|
let logging = version.logging.as_ref().expect("logging 应解析");
|
|
let client = logging.client().expect("应有 client 日志配置");
|
|
assert_eq!(client.kind.as_deref(), Some("log4j2-xml"));
|
|
assert_eq!(client.file.id.as_deref(), Some("client-1.12.xml"));
|
|
}
|
|
|
|
#[test]
|
|
fn tolerant_defaults_for_minimal_json() {
|
|
let version = parse(r#"{ "id": "b1.7.3" }"#);
|
|
assert_eq!(version.id.as_deref(), Some("b1.7.3"));
|
|
assert_eq!(version.version_type, None, "未知 type 不报错");
|
|
assert_eq!(version.client_jar(), None, "老 JSON 没有 downloads.client");
|
|
assert_eq!(version.assets_id(), "legacy", "assets 缺失按 legacy");
|
|
assert!(!version.is_resolved());
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_type_value_falls_back_to_unknown() {
|
|
let version = parse(r#"{ "id": "x", "type": "some_new_kind" }"#);
|
|
assert_eq!(version.version_type, Some(ReleaseType::Unknown));
|
|
}
|
|
|
|
#[test]
|
|
fn missing_id_is_reported() {
|
|
let version = parse(r#"{ "mainClass": "a.B" }"#);
|
|
let error = version.require_id().expect_err("缺 id 应报错");
|
|
assert_eq!(
|
|
error.to_string(),
|
|
"版本 JSON 缺少 id",
|
|
"错误文案要给用户看得懂"
|
|
);
|
|
}
|