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 开发中。
106 lines
4.1 KiB
Rust
106 lines
4.1 KiB
Rust
//! 离线 UUID 与 **JDK 对拍**(这就是「HMCL 兼容」的定义)。
|
||
//!
|
||
//! 期望值不是我们算出来再抄回来的,而是用 JDK 25 跑
|
||
//! `UUID.nameUUIDFromBytes(("OfflinePlayer:" + name).getBytes(UTF_8))`
|
||
//! 生成的(驱动源码见下方注释里的等价 Java)。HMCL 的
|
||
//! `OfflineAccountFactory.getUUIDFromUserName` 就是这一行,因此对拍 JDK 即对拍 HMCL。
|
||
//!
|
||
//! ```java
|
||
//! UUID id = UUID.nameUUIDFromBytes(("OfflinePlayer:" + name).getBytes(StandardCharsets.UTF_8));
|
||
//! System.out.println(id.toString().replace("-", ""));
|
||
//! ```
|
||
|
||
use unlucky_core::auth::offline::{offline_login, offline_meta, offline_uuid, offline_uuid_dashed};
|
||
use unlucky_core::auth::AccountKind;
|
||
|
||
/// `(名字, JDK 输出 32 位无连字符)`。
|
||
const JDK_VECTORS: [(&str, &str); 7] = [
|
||
("Notch", "b50ad385829d3141a2167e7d7539ba7f"),
|
||
("jeb_", "a762f5604fce3236812ab80efff0b62b"),
|
||
("Player", "a01e3843e5213998958af459800e4d11"),
|
||
("Steve", "5627dd98e6be3c21b8a8e92344183641"),
|
||
// 非 ASCII:UTF-8 字节参与 MD5(中文名离线服常见)。
|
||
("小明", "e1fa999fd9e93419bc012f51157a00e5"),
|
||
("name with space", "46c02d33ff53313b813aa95508518305"),
|
||
("AskaEth", "5c5918fb61303d6eb8d8eb2d1da6e439"),
|
||
];
|
||
|
||
#[test]
|
||
fn matches_jdk_name_uuid_from_bytes() {
|
||
for (name, expected) in JDK_VECTORS {
|
||
assert_eq!(
|
||
offline_uuid(name),
|
||
expected,
|
||
"离线 UUID 与 JDK/HMCL 不一致:{name}"
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn dashed_form_is_the_same_uuid() {
|
||
for (name, expected) in JDK_VECTORS {
|
||
let dashed = offline_uuid_dashed(name);
|
||
assert_eq!(dashed.replace('-', ""), expected, "{name} 的连字符形式");
|
||
assert_eq!(dashed.len(), 36);
|
||
assert_eq!(dashed.matches('-').count(), 4);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn version_is_3_and_variant_is_ietf() {
|
||
for (name, raw) in JDK_VECTORS {
|
||
let version_nibble = &raw[12..13];
|
||
assert_eq!(version_nibble, "3", "{name} 必须是 v3");
|
||
let variant = u8::from_str_radix(&raw[16..17], 16).expect("变体位");
|
||
assert!((0x8..=0xb).contains(&variant), "{name} 变体必须是 10xx");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn uuid_depends_on_exact_bytes_not_case() {
|
||
// 大小写不同 → 不同身份(服务端就是按字节算的,别做「友好」归一化)。
|
||
assert_ne!(offline_uuid("Steve"), offline_uuid("steve"));
|
||
// 前缀也参与哈希:直接哈希名字会得到另一个 UUID。
|
||
assert_ne!(offline_uuid("Notch"), offline_uuid("OfflinePlayer:Notch"));
|
||
}
|
||
|
||
#[test]
|
||
fn offline_login_matches_hmcl_behaviour() {
|
||
let meta = offline_meta("小明", 1).expect("建元数据");
|
||
assert_eq!(meta.kind, AccountKind::Offline);
|
||
assert_eq!(meta.profile_id, "e1fa999fd9e93419bc012f51157a00e5");
|
||
assert_eq!(meta.profile_name, "小明");
|
||
|
||
let placeholders = offline_login(&meta).expect("占位符");
|
||
// uuid 必须是**无连字符** 32 位(HMCL 的 UUIDs.toCompactString)。
|
||
assert_eq!(placeholders.uuid, meta.profile_id);
|
||
assert_eq!(placeholders.uuid.len(), 32);
|
||
assert!(!placeholders.uuid.contains('-'));
|
||
// accessToken = 随机 32 位十六进制(每次不同、非空:游戏不接受空 token)。
|
||
assert_eq!(placeholders.access_token.len(), 32);
|
||
assert!(placeholders
|
||
.access_token
|
||
.chars()
|
||
.all(|ch| ch.is_ascii_hexdigit()));
|
||
assert_ne!(
|
||
placeholders.access_token,
|
||
offline_login(&meta).expect("再来一次").access_token
|
||
);
|
||
// userType=msa、userProperties={}(承 OfflineAccount.logInWithoutSkin)。
|
||
assert_eq!(placeholders.user_type, "msa");
|
||
assert_eq!(placeholders.user_properties, "{}");
|
||
}
|
||
|
||
#[test]
|
||
fn names_are_validated_before_hashing() {
|
||
assert!(offline_meta(" ", 1).is_err());
|
||
assert!(offline_meta("0123456789abcdefg", 1).is_err());
|
||
// 16 字符刚好合法。
|
||
assert!(offline_meta("0123456789abcdef", 1).is_ok());
|
||
// 首尾空白被去掉后哈希(避免「名字带空格」算出两套 UUID)。
|
||
assert_eq!(
|
||
offline_meta(" Steve ", 1).expect("合法").profile_id,
|
||
offline_uuid("Steve")
|
||
);
|
||
}
|