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
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:
commit
bc41dfcf7d
517 files changed
+92844
No files matched your search
@@ -0,0 +1,251 @@
|
||||
//! 账户存储:`data/auth/` 下的**元数据 / 凭据分文件**落盘(内存视图与增删)。
|
||||
//!
|
||||
//! 契约(`PlanDoc/07-accounts.md` §2、`PlanDoc/04-data-and-config.md`):
|
||||
//! 1. **分文件**:`accounts.json`(可公开)+ `credentials.dat`(凭据)。列账户不碰凭据文件;
|
||||
//! 2. **先写凭据、再写元数据**(HMCL 安全顺序)——元数据永不指向不存在的凭据;
|
||||
//! 3. **原子写**:走 `platform::atomic::write_atomic`(tmp → fsync → rename);
|
||||
//! 4. **容错**:单条坏记录只跳过该条并留 warning,绝不整体加载失败;
|
||||
//! 5. **孤儿清理**:`save()` 只写「元数据里还存在」的凭据。
|
||||
//!
|
||||
//! 落盘细节(payload 形状、权限收紧)在同目录的 [`payload`] 子模块,本文件只管模型与不变量。
|
||||
//!
|
||||
//! 保护级别:本期 `plain-v1`(分文件 + 权限 + 全链脱敏)。OS keychain / AES-256-GCM
|
||||
//! 分级是 07 §2 的第 1–2 条,需要 keyring/aes-gcm/argon2 的依赖决策,属后续 mission。
|
||||
|
||||
mod payload;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::auth::account::{AccountId, AccountMeta, Credentials};
|
||||
use crate::auth::error::{AuthError, AuthResult};
|
||||
|
||||
/// 元数据文件名(`data/auth/accounts.json`)。
|
||||
pub const ACCOUNTS_FILE: &str = "accounts.json";
|
||||
/// 凭据文件名(`data/auth/credentials.dat`)。
|
||||
pub const CREDENTIALS_FILE: &str = "credentials.dat";
|
||||
/// 元数据 schema 版本。
|
||||
pub const AUTH_SCHEMA_VERSION: u32 = 1;
|
||||
/// 本期凭据保护级别(明文 + 分文件 + 权限 + 脱敏)。
|
||||
pub const PROTECTION_PLAIN_V1: &str = "plain-v1";
|
||||
|
||||
/// `data/auth/` 的两个文件位置。
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AuthPaths {
|
||||
root: PathBuf,
|
||||
}
|
||||
|
||||
impl AuthPaths {
|
||||
/// 以数据分区为根(`LauncherLayout::auth_data()`)。
|
||||
#[must_use]
|
||||
pub fn new(root: impl Into<PathBuf>) -> Self {
|
||||
Self { root: root.into() }
|
||||
}
|
||||
|
||||
/// 根目录。
|
||||
#[must_use]
|
||||
pub fn root(&self) -> &Path {
|
||||
&self.root
|
||||
}
|
||||
|
||||
/// 元数据文件。
|
||||
#[must_use]
|
||||
pub fn accounts_file(&self) -> PathBuf {
|
||||
self.root.join(ACCOUNTS_FILE)
|
||||
}
|
||||
|
||||
/// 凭据文件。
|
||||
#[must_use]
|
||||
pub fn credentials_file(&self) -> PathBuf {
|
||||
self.root.join(CREDENTIALS_FILE)
|
||||
}
|
||||
}
|
||||
|
||||
/// 账户库(内存视图 + 落盘)。
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AccountStore {
|
||||
paths: AuthPaths,
|
||||
accounts: Vec<AccountMeta>,
|
||||
credentials: BTreeMap<String, Credentials>,
|
||||
warnings: Vec<String>,
|
||||
}
|
||||
|
||||
impl AccountStore {
|
||||
/// 打开(不存在就是空库;坏记录进 warnings)。
|
||||
#[must_use]
|
||||
pub fn open(root: impl Into<PathBuf>) -> Self {
|
||||
Self::open_paths(AuthPaths::new(root))
|
||||
}
|
||||
|
||||
/// 打开指定文件位置。
|
||||
#[must_use]
|
||||
pub fn open_paths(paths: AuthPaths) -> Self {
|
||||
let mut warnings = Vec::new();
|
||||
let mut accounts = payload::read_accounts(&paths.accounts_file(), &mut warnings);
|
||||
accounts.sort_by_key(|meta| meta.created_at_ms);
|
||||
let known: Vec<String> = accounts
|
||||
.iter()
|
||||
.map(|meta| meta.id.as_str().to_string())
|
||||
.collect();
|
||||
let credentials =
|
||||
payload::read_credentials(&paths.credentials_file(), &known, &mut warnings);
|
||||
Self {
|
||||
paths,
|
||||
accounts,
|
||||
credentials,
|
||||
warnings,
|
||||
}
|
||||
}
|
||||
|
||||
/// 文件位置。
|
||||
#[must_use]
|
||||
pub fn paths(&self) -> &AuthPaths {
|
||||
&self.paths
|
||||
}
|
||||
|
||||
/// 账户列表(按创建时间排序,展示顺序稳定)。
|
||||
#[must_use]
|
||||
pub fn accounts(&self) -> &[AccountMeta] {
|
||||
&self.accounts
|
||||
}
|
||||
|
||||
/// 加载过程中的告警(坏记录、孤儿凭据等)。
|
||||
#[must_use]
|
||||
pub fn warnings(&self) -> &[String] {
|
||||
&self.warnings
|
||||
}
|
||||
|
||||
/// 是否没有任何账户。
|
||||
#[must_use]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.accounts.is_empty()
|
||||
}
|
||||
|
||||
/// 按名字 / 登录名 / 条目 id 查找(大小写不敏感)。
|
||||
#[must_use]
|
||||
pub fn find(&self, key: &str) -> Option<&AccountMeta> {
|
||||
self.accounts.iter().find(|meta| meta.matches(key))
|
||||
}
|
||||
|
||||
/// 取某条目的凭据。
|
||||
#[must_use]
|
||||
pub fn credentials(&self, id: &AccountId) -> Option<&Credentials> {
|
||||
self.credentials.get(id.as_str())
|
||||
}
|
||||
|
||||
/// 取某条目的「元数据 + 凭据」(找不到凭据时报错)。
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// 条目不存在 → [`AuthError::NotFound`];条目在但凭据缺失 → [`AuthError::Malformed`]
|
||||
/// (说明文件被手工删过,别静默当成「没登录」)。
|
||||
pub fn entry(&self, id: &AccountId) -> AuthResult<(&AccountMeta, &Credentials)> {
|
||||
let meta = self
|
||||
.accounts
|
||||
.iter()
|
||||
.find(|meta| meta.id == *id)
|
||||
.ok_or_else(|| AuthError::NotFound {
|
||||
account: id.to_string(),
|
||||
})?;
|
||||
let credentials = self
|
||||
.credentials
|
||||
.get(id.as_str())
|
||||
.ok_or_else(|| AuthError::Malformed(format!("账户 {id} 的凭据缺失")))?;
|
||||
Ok((meta, credentials))
|
||||
}
|
||||
|
||||
/// 新增账户(内存)。同名同轨冲突时报错——`--account <名字>` 才有唯一解。
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// 重复、条目 id 重复、轨道与凭据不符都返回错误。
|
||||
pub fn insert(&mut self, meta: AccountMeta, credentials: Credentials) -> AuthResult<()> {
|
||||
meta.check_credentials(&credentials)?;
|
||||
if self.accounts.iter().any(|existing| existing.id == meta.id) {
|
||||
return Err(AuthError::Conflict {
|
||||
account: meta.id.to_string(),
|
||||
});
|
||||
}
|
||||
let duplicate = self
|
||||
.accounts
|
||||
.iter()
|
||||
.any(|existing| existing.kind == meta.kind && existing.matches(meta.display_name()));
|
||||
if duplicate {
|
||||
return Err(AuthError::Conflict {
|
||||
account: meta.display_name().to_string(),
|
||||
});
|
||||
}
|
||||
self.credentials
|
||||
.insert(meta.id.as_str().to_string(), credentials);
|
||||
self.accounts.push(meta);
|
||||
self.accounts.sort_by_key(|meta| meta.created_at_ms);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 删除账户(连同凭据)。
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// 找不到时返回 [`AuthError::NotFound`]。
|
||||
pub fn remove(&mut self, key: &str) -> AuthResult<AccountMeta> {
|
||||
let index = self
|
||||
.accounts
|
||||
.iter()
|
||||
.position(|meta| meta.matches(key))
|
||||
.ok_or_else(|| AuthError::NotFound {
|
||||
account: key.to_string(),
|
||||
})?;
|
||||
let meta = self.accounts.remove(index);
|
||||
self.credentials.remove(meta.id.as_str());
|
||||
Ok(meta)
|
||||
}
|
||||
|
||||
/// 更新某条目的凭据(刷新后轮换 token 用)。
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// 条目不存在或轨道不符时返回错误。
|
||||
pub fn update_credentials(
|
||||
&mut self,
|
||||
id: &AccountId,
|
||||
credentials: Credentials,
|
||||
) -> AuthResult<()> {
|
||||
let meta = self
|
||||
.accounts
|
||||
.iter()
|
||||
.find(|meta| meta.id == *id)
|
||||
.ok_or_else(|| AuthError::NotFound {
|
||||
account: id.to_string(),
|
||||
})?;
|
||||
meta.check_credentials(&credentials)?;
|
||||
self.credentials
|
||||
.insert(id.as_str().to_string(), credentials);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 同步某条目的元数据字段(刷新补齐了玩家名之后写回)。
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// 条目不存在时返回 [`AuthError::NotFound`]。
|
||||
pub fn update_meta(&mut self, meta: AccountMeta) -> AuthResult<()> {
|
||||
let slot = self
|
||||
.accounts
|
||||
.iter_mut()
|
||||
.find(|existing| existing.id == meta.id)
|
||||
.ok_or_else(|| AuthError::NotFound {
|
||||
account: meta.id.to_string(),
|
||||
})?;
|
||||
*slot = meta;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 落盘:**先凭据、后元数据**。
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// 两个文件任一写失败都返回 [`AuthError::Store`]。
|
||||
pub fn save(&self) -> AuthResult<()> {
|
||||
payload::write(&self.paths, &self.accounts, &self.credentials)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user