Files
Unlucky-Launcher/crates/presenter/src/host/accounts/failure.rs
T
AskaEth 8a8e5ca0c0
CI / runtime smoke (macos arm64, probe) (push) Skipped
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
CI / runtime smoke (ubuntu x64, xvfb) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Unlucky Launcher v0.2.0
首个 iced + Material 3 Expressive 版本的源码快照。
完整历史与日常提交在 dev 分支;main 只放阶段性快照。
2026-11-05 14:22:11 +08:00

127 lines
4.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 账户写操作的失败分类 + 离线名字契约(M114 r2)。
//!
//! 分类**只看 `AuthError` 的变体与 ascii 码**,不看中文文案——文案随时会改,
//! 而页面据 `AccountNoticeCode` 选 i18n,两边必须能独立演进。
use unlucky_core::auth::AuthError;
use unlucky_core::auth::BodyKind;
use crate::dto::{AccountNoticeCode, LoginFailure};
/// 账户写操作的失败:**结构化码 + 原文**(原文只进日志/详细信息,不上屏)。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccountFailure {
/// 结果码(页面据此选 i18n)
pub code: AccountNoticeCode,
/// 原始诊断(已脱敏)
pub detail: String,
}
impl AccountFailure {
/// 按 core 的错误类型分类(不看中文文案,只看变体)。
pub(super) fn classify(error: AuthError, fallback: AccountNoticeCode) -> Self {
let code = match &error {
unlucky_core::auth::AuthError::Conflict { .. } => AccountNoticeCode::AccountExists,
unlucky_core::auth::AuthError::NotFound { .. } => AccountNoticeCode::NotFound,
unlucky_core::auth::AuthError::Malformed(_) => AccountNoticeCode::OfflineNameInvalid,
unlucky_core::auth::AuthError::Store { .. } => AccountNoticeCode::StoreFailed,
_ => fallback,
};
Self {
code,
detail: error.to_string(),
}
}
}
/// `AuthError` → 原因码(**只看变体与 ascii 码,不看中文文案**)。
pub(super) fn classify(error: &AuthError) -> LoginFailure {
match error {
AuthError::Network { .. } | AuthError::Timeout { .. } => LoginFailure::Unreachable,
AuthError::Denied { code, description } => {
if mentions_whitelist(code) || mentions_whitelist(description) {
LoginFailure::NotWhitelisted
} else {
LoginFailure::Denied
}
}
// 只看 `ResponseDetail` 的**结构化字段**(status / kind / server_message),
// 不把整条错误 Debug 成字符串再去扫——那会把中文 `what`/`digest` 也算进判据里。
AuthError::Response(detail) => {
let server = detail.server_message.as_deref().unwrap_or_default();
let digest = detail.digest.as_str();
if mentions_whitelist(server) || mentions_whitelist(digest) {
LoginFailure::NotWhitelisted
} else if detail.status >= 500 || matches!(detail.kind, BodyKind::Html | BodyKind::Text)
{
// 5xx / 拦截页:服务器或链路出了问题,不是「它拒绝了你」。
LoginFailure::Unreachable
} else {
LoginFailure::Denied
}
}
AuthError::Xbox { .. } | AuthError::CredentialExpired => LoginFailure::Denied,
AuthError::Store { .. } => LoginFailure::StoreFailed,
_ => LoginFailure::Other,
}
}
/// 是否属于「应用注册未获批」这一类(AADSTS7000218 / consent required / whitelist)。
pub(super) fn mentions_whitelist(text: &str) -> bool {
let lowered = text.to_lowercase();
lowered.contains("aadsts7000218")
|| lowered.contains("aadsts65001")
|| lowered.contains("consent")
|| lowered.contains("whitelist")
|| lowered.contains("not approved")
}
#[cfg(test)]
mod tests {
use super::*;
use unlucky_core::auth::body::ResponseDetail;
fn response(status: u16, kind: BodyKind, server_message: Option<&str>) -> AuthError {
AuthError::Response(Box::new(ResponseDetail {
step: "xsts".to_string(),
what: "XSTS 授权".to_string(),
status,
content_type: None,
kind,
server_message: server_message.map(str::to_string),
digest: "digest".to_string(),
}))
}
/// 403 带 AADSTS 码 → 白名单未批;403 不带 → 一般性拒绝。
#[test]
fn aadsts_markers_win_over_the_generic_denial() {
assert_eq!(
classify(&response(
403,
BodyKind::Json,
Some("AADSTS7000218: app not approved")
)),
LoginFailure::NotWhitelisted
);
assert_eq!(
classify(&response(403, BodyKind::Json, Some("invalid_grant"))),
LoginFailure::Denied
);
}
/// 5xx 与「拦截页」(HTML/纯文本)归「连不上」,不是「服务器拒绝了你」。
#[test]
fn upstream_failures_and_intercept_pages_are_unreachable() {
assert_eq!(
classify(&response(503, BodyKind::Json, None)),
LoginFailure::Unreachable
);
assert_eq!(
classify(&response(200, BodyKind::Html, None)),
LoginFailure::Unreachable
);
}
}