Files
AskaEth bc41dfcf7d
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
Unlucky Launcher v0.0.1-alpha
Rust 原生单文件 Minecraft 启动器:CLI 完整可用(install/launch/accounts/java/diagnose/agent),
MCP server 接入外部 Agent 工具,GUI 开发中。
2026-10-31 21:54:10 +08:00

257 lines
8.1 KiB
Rust

//! 规则求值、参数展开、natives 选择(launch 直接消费这些结果)。
use std::collections::BTreeMap;
use unlucky_core::game::arguments::{
matches_simple_pattern, rules_allow, Argument, ArgumentValue, ConditionalArgument, Environment,
OsRestriction, Rule, RuleAction,
};
use unlucky_core::game::library::Library;
use crate::helpers::parse;
fn env(os: &str, arch: &str) -> Environment {
Environment {
os: os.to_owned(),
os_version: "10.0".to_owned(),
arch: arch.to_owned(),
features: BTreeMap::new(),
}
}
fn allow_os(name: &str) -> Rule {
Rule {
action: RuleAction::Allow,
os: Some(OsRestriction {
name: Some(name.to_owned()),
..OsRestriction::default()
}),
features: None,
}
}
fn disallow_os(name: &str) -> Rule {
Rule {
action: RuleAction::Disallow,
..allow_os(name)
}
}
#[test]
fn empty_rules_always_allow() {
assert!(rules_allow(&[], &env("linux", "x86_64")));
}
#[test]
fn allow_rule_must_match_environment() {
let rules = [allow_os("windows")];
assert!(rules_allow(&rules, &env("windows", "x86_64")));
assert!(!rules_allow(&rules, &env("linux", "x86_64")));
}
#[test]
fn later_rules_override_earlier_ones() {
// 白名单语义:列表非空时从 DISALLOW 起步。
let allow_after_disallow = [disallow_os("windows"), allow_os("windows")];
assert!(rules_allow(
&allow_after_disallow,
&env("windows", "x86_64")
));
let disallow_after_allow = [allow_os("windows"), disallow_os("windows")];
assert!(!rules_allow(
&disallow_after_allow,
&env("windows", "x86_64")
));
}
#[test]
fn feature_gate_blocks_argument() {
let mut features = BTreeMap::new();
features.insert("has_custom_resolution".to_owned(), true);
let rule = Rule {
action: RuleAction::Allow,
os: None,
features: Some(features),
};
let enabled = env("windows", "x86_64").with_feature("has_custom_resolution", true);
let disabled = env("windows", "x86_64");
assert_eq!(rule.action_in(&enabled), Some(RuleAction::Allow));
assert_eq!(rule.action_in(&disabled), None);
}
#[test]
fn host_environment_is_mojang_shaped() {
let host = Environment::host();
assert!(matches!(host.os.as_str(), "windows" | "osx" | "linux"));
assert!(!host.arch.is_empty());
assert!(!host.feature("has_custom_resolution"));
// 空串是「没探」,真值来自 platform::os_info::current();只要探得到就不能再是空的。
let probed = unlucky_core::platform::os_info::current();
assert_eq!(host.os_version, probed.os_version());
assert_eq!(host.os, probed.family.manifest_name());
assert_eq!(host.arch, probed.arch);
}
#[test]
fn environment_from_an_injected_os_snapshot_carries_os_version() {
use unlucky_core::platform::linux_caps::SessionKind;
use unlucky_core::platform::os_info::{support_level_for, OsFamily, OsInfo};
use unlucky_core::platform::vk_probe::VkProbe;
let fake = OsInfo {
family: OsFamily::Windows,
arch: "x86_64".to_string(),
major_minor: Some((10, 0)),
build: Some(19_045),
product: "Windows 10".to_string(),
version_string: "10.0.19045".to_string(),
session: SessionKind::Unknown,
gl: unlucky_core::platform::linux_caps::GlProbe::Unknown,
vk: VkProbe::Available,
kernel: None,
glibc: None,
support_level: support_level_for(OsFamily::Windows, Some((10, 0)), None, None),
};
let environment = Environment::from_os_info(&fake);
assert_eq!(environment.os, "windows");
assert_eq!(environment.os_version, "10.0.19045");
assert_eq!(environment.arch, "x86_64");
assert!(environment.features.is_empty(), "注入快照不该带特性开关");
// 系统版本真的参与规则求值(这正是不再用空串的原因)。
let rule = Rule {
os: Some(OsRestriction {
name: None,
version: Some("^10\\.".to_string()),
arch: None,
}),
..Rule::default()
};
assert_eq!(rule.action_in(&environment), Some(RuleAction::Allow));
let mac_rule = Rule {
os: Some(OsRestriction {
name: None,
version: Some("^11\\.".to_string()),
arch: None,
}),
..Rule::default()
};
assert_eq!(mac_rule.action_in(&environment), None);
}
#[test]
fn os_version_patterns_match_conservatively() {
assert!(matches_simple_pattern("^10\\.", "10.0"));
assert!(!matches_simple_pattern("^10\\.", "11.0"));
assert!(matches_simple_pattern("x86_64", "x86_64"));
assert!(!matches_simple_pattern("x86_64", "x86"));
assert!(matches_simple_pattern("10\\.0$", "10.0"));
// 不认识的复杂正则放宽为匹配(宁可多带参数也不漏参数)。
assert!(matches_simple_pattern("^(10|11)\\.", "11.0"));
}
#[test]
fn expand_arguments_by_environment() {
let version = parse(
r#"{
"id": "x",
"arguments": {
"game": [
"--username",
{ "rules": [{ "action": "allow", "features": { "is_demo_user": true } }],
"value": "--demo" },
{ "value": ["--pair", "1"] }
],
"jvm": [ "-Dfoo=1" ]
}
}"#,
);
let arguments = version.arguments.as_ref().expect("arguments 应解析");
let plain = arguments.expand(&env("windows", "x86_64"));
assert_eq!(plain.game, vec!["--username", "--pair", "1"]);
assert_eq!(plain.jvm, vec!["-Dfoo=1"]);
let demo = env("windows", "x86_64").with_feature("is_demo_user", true);
assert!(arguments.expand(&demo).game.contains(&"--demo".to_owned()));
}
#[test]
fn conditional_argument_expands_single_value() {
let argument = Argument::Conditional(ConditionalArgument {
rules: vec![allow_os("windows")],
value: ArgumentValue::One("--single".to_owned()),
});
let mut out = Vec::new();
argument.expand_into(&env("windows", "x86_64"), &mut out);
assert_eq!(out, vec!["--single"]);
let mut out = Vec::new();
argument.expand_into(&env("linux", "x86_64"), &mut out);
assert!(out.is_empty());
}
#[test]
fn native_classifier_expands_arch() {
let version = parse(
r#"{ "id": "x",
"libraries": [{
"name": "org.lwjgl:lwjgl:3.3.2",
"natives": { "windows": "natives-windows-${arch}", "linux": "natives-linux" },
"downloads": {
"artifact": { "path": "org/lwjgl/lwjgl/3.3.2/lwjgl-3.3.2.jar" },
"classifiers": {
"natives-windows-64": { "path": "org/lwjgl/lwjgl/3.3.2/lwjgl-3.3.2-natives-windows-64.jar" }
}
}
}]
}"#,
);
let library = &version.libraries.as_ref().expect("libraries 应解析")[0];
assert_eq!(
library.native_classifier(&env("windows", "x86_64")),
Some("natives-windows-64".to_owned())
);
assert_eq!(
library.native_classifier(&env("windows", "x86")),
Some("natives-windows-".to_owned())
);
assert_eq!(
library.native_classifier(&env("linux", "x86_64")),
Some("natives-linux".to_owned())
);
assert_eq!(library.native_classifier(&env("osx", "arm64")), None);
let classifier = "natives-windows-64";
assert!(library.native_artifact(classifier).is_some());
assert_eq!(
library
.native_relative_path(classifier)
.expect("路径应可推导"),
std::path::PathBuf::from("org/lwjgl/lwjgl/3.3.2/lwjgl-3.3.2-natives-windows-64.jar")
);
}
#[test]
fn library_rules_and_local_hint() {
let library = Library {
name: "com.example:inst:1".to_owned(),
rules: vec![allow_os("linux")],
hint: Some("local".to_owned()),
..Library::default()
};
assert!(library.applies(&env("linux", "x86_64")));
assert!(!library.applies(&env("windows", "x86_64")));
assert!(library.is_local());
assert_eq!(
library.relative_path().expect("路径应可推导"),
std::path::PathBuf::from("com/example/inst/1/inst-1.jar")
);
}