Unlucky Launcher v0.0.1-alpha
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 开发中。
This commit is contained in:
AskaEth committed 2026-10-31 21:54:10 +08:00
commit bc41dfcf7d
517 files changed
+92844

No files matched your search

@@ -0,0 +1,86 @@
//! 认证服务器元数据里我们真正需要的字段(服务发现的产物)。
//!
//! authlib-injector 规范里根路径 `/` 返回 `{ meta, skinDomains, signaturePublickey }`
//! (`analyse/04` §6.3)。本期**只保存**名字/非邮箱登录/skinDomains:
//! `signaturePublickey` 与 `prefetched` 的 Base64 属于注入参数(后续 mission),
//! 现在存了也没人消费,不如不存。
use serde_json::Value;
/// 服务器元数据(缺失字段一律回落默认值)。
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthlibServerMeta {
/// 服务器显示名(缺省回落 URL)。
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
/// 是否允许非邮箱登录名(决定输入框校验)。
#[serde(default)]
pub non_email_login: bool,
/// `skinDomains`(纹理白名单,注入参数会用;本期只保存)。
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub skin_domains: Vec<String>,
}
impl AuthlibServerMeta {
/// 从根元数据 JSON 解析(容错:缺字段就用默认值)。
#[must_use]
pub fn from_value(value: &Value) -> Self {
let meta = value.get("meta");
let name = meta
.and_then(|meta| meta.get("serverName"))
.and_then(Value::as_str)
.map(str::to_string);
let non_email_login = meta
.and_then(|meta| meta.get("feature.non_email_login"))
.and_then(Value::as_bool)
.unwrap_or(false);
let skin_domains = value
.get("skinDomains")
.and_then(Value::as_array)
.map(|items| {
items
.iter()
.filter_map(Value::as_str)
.map(str::to_string)
.collect()
})
.unwrap_or_default();
Self {
name,
non_email_login,
skin_domains,
}
}
}
#[cfg(test)]
mod tests {
use super::AuthlibServerMeta;
use serde_json::json;
#[test]
fn parses_full_metadata() {
let value = json!({
"meta": {
"serverName": "Example Skin Site",
"feature.non_email_login": true
},
"skinDomains": ["example.com", "*.example.com"],
"signaturePublickey": "-----BEGIN PUBLIC KEY-----"
});
let meta = AuthlibServerMeta::from_value(&value);
assert_eq!(meta.name.as_deref(), Some("Example Skin Site"));
assert!(meta.non_email_login);
assert_eq!(meta.skin_domains.len(), 2);
}
#[test]
fn tolerates_missing_fields() {
let meta = AuthlibServerMeta::from_value(&json!({}));
assert_eq!(meta, AuthlibServerMeta::default());
assert!(meta.name.is_none());
assert!(!meta.non_email_login);
assert!(meta.skin_domains.is_empty());
}
}
+97
View File
@@ -0,0 +1,97 @@
//! authlib 轨的纯函数/模型测试(HTTP 部分在 `tests/auth/authlib_api.rs`)。
use super::{normalize_api_root, AuthlibEndpoints, AuthlibProfile, AuthlibSession, USER_TYPE_MSA};
use crate::auth::secret::Secret;
#[test]
fn api_root_normalization() {
assert_eq!(
normalize_api_root("littleskin.cn/api/yggdrasil").expect("补协议"),
"https://littleskin.cn/api/yggdrasil/"
);
assert_eq!(
normalize_api_root(" http://127.0.0.1:8080/api/yggdrasil/ ").expect("保留协议"),
"http://127.0.0.1:8080/api/yggdrasil/"
);
assert_eq!(
normalize_api_root("https://littleskin.cn").expect("根域名"),
"https://littleskin.cn/"
);
assert!(normalize_api_root(" ").is_err());
assert!(normalize_api_root("https://").is_err());
}
#[test]
fn endpoints_follow_directory_layout() {
let endpoints =
AuthlibEndpoints::from_api_root("https://example.invalid/api/yggdrasil").expect("端点");
assert_eq!(
endpoints.authenticate,
"https://example.invalid/api/yggdrasil/authserver/authenticate"
);
assert_eq!(
endpoints.refresh,
"https://example.invalid/api/yggdrasil/authserver/refresh"
);
assert_eq!(
endpoints.validate,
"https://example.invalid/api/yggdrasil/authserver/validate"
);
assert_eq!(
endpoints.invalidate,
"https://example.invalid/api/yggdrasil/authserver/invalidate"
);
}
#[test]
fn placeholders_require_a_profile() {
let without = AuthlibSession {
access_token: Secret::new("token"),
client_token: Secret::new("client"),
selected_profile: None,
};
assert!(!without.has_profile());
assert!(without.to_placeholders().is_err());
let with = AuthlibSession {
selected_profile: Some(AuthlibProfile {
id: "0123456789abcdef0123456789abcdef".to_string(),
name: "Steve".to_string(),
}),
..without
};
assert!(with.has_profile());
let placeholders = with.to_placeholders().expect("占位符");
assert_eq!(placeholders.player_name, "Steve");
assert_eq!(placeholders.uuid, "0123456789abcdef0123456789abcdef");
assert_eq!(placeholders.user_type, USER_TYPE_MSA);
assert_eq!(placeholders.access_token, "token");
}
#[test]
fn session_debug_never_leaks_tokens() {
let session = AuthlibSession {
access_token: Secret::new("access-token-value"),
client_token: Secret::new("client-token-value"),
selected_profile: None,
};
let debug = format!("{session:?}");
assert!(!debug.contains("access-token-value"));
assert!(!debug.contains("client-token-value"));
assert_eq!(session.secrets().len(), 2);
}
#[test]
fn session_survives_json_roundtrip() {
let session = AuthlibSession {
access_token: Secret::new("access-token-value"),
client_token: Secret::new("client-token-value"),
selected_profile: Some(AuthlibProfile {
id: "0123456789abcdef0123456789abcdef".to_string(),
name: "Steve".to_string(),
}),
};
let text = serde_json::to_string(&session).expect("序列化");
let back: AuthlibSession = serde_json::from_str(&text).expect("反序列化");
assert_eq!(back, session);
}