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

+222
View File
@@ -0,0 +1,222 @@
//! 迁移链:`schemaVersion N → N+1` 顺序迁移框架 + 收据闸门(防重放)。
//!
//! 出处:`DevDocs/PlanDoc/04-data-and-config.md` §3(迁移链 + 迁移收据)、
//! `DevDocs/analyse/07-settings-packaging-update.md` §1.8(收据存源文件 `sourceSHA256`)。
//!
//! 与 HMCL 的差别:HMCL 完全靠 `$schema` 字段做兼容、迁移只处理遗留格式;ULL 在这之上补一条
//! 「major 逐级迁移链」——每一步只跨一个 major,缺环/重复/逆序在 `push` 时就报错,
//! 避免出现「跳版迁移脚本」这种组合爆炸。
use std::path::Path;
use serde_json::{Map, Value};
use crate::platform::error::PlatformResult;
use crate::platform::receipt::{self, MigrationReceipt, ReplayDecision};
use super::error::{SettingError, SettingResult};
use super::schema::{SchemaVersion, SCHEMA_VERSION_KEY};
/// 一步迁移。实现者只管把 `root` 从 `from` 改造成 `to`。
pub trait SettingMigration: Send + Sync {
/// 起始版本。
fn from(&self) -> SchemaVersion;
/// 目标版本(必须等于 `from.next_major()`)。
fn to(&self) -> SchemaVersion;
/// 一句话说明(进日志与迁移报告)。
fn describe(&self) -> &'static str;
/// 就地改 JSON 对象;`Err(String)` 会被包成 [`SettingError::Migration`]。
///
/// # Errors
/// 结构不符合预期(该有的键没有/类型不对)时返回原因。
fn apply(&self, root: &mut Map<String, Value>) -> Result<(), String>;
}
/// 迁移报告(进日志 / 遥测 / 收据)。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MigrationReport {
/// 起始版本。
pub from: SchemaVersion,
/// 结束版本。
pub to: SchemaVersion,
/// 依次执行的步骤说明。
pub applied: Vec<&'static str>,
}
impl MigrationReport {
/// 是否真的做了迁移。
#[must_use]
pub fn is_noop(&self) -> bool {
self.applied.is_empty()
}
}
/// 有序迁移链。
#[derive(Default)]
pub struct MigrationChain {
steps: Vec<Box<dyn SettingMigration>>,
}
impl std::fmt::Debug for MigrationChain {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_list()
.entries(self.steps.iter().map(|step| step.describe()))
.finish()
}
}
impl MigrationChain {
/// 空链。
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// 追加一步;必须与上一环首尾相接,且只跨一个 major。
///
/// # Errors
/// 跨多个 major、起始版本重复、顺序错乱时返回 [`SettingError::ChainBroken`]。
pub fn push(&mut self, step: Box<dyn SettingMigration>) -> SettingResult<()> {
let from = step.from();
let to = step.to();
if to != from.next_major() {
return Err(SettingError::ChainBroken {
reason: format!("一步迁移只能跨一个 major:{from} → {to}"),
});
}
if self.steps.iter().any(|existing| existing.from() == from) {
return Err(SettingError::ChainBroken {
reason: format!("{from} 已有迁移步骤"),
});
}
if let Some(last) = self.steps.last() {
if last.to() != from {
return Err(SettingError::ChainBroken {
reason: format!("顺序错误:上一环到 {},本环从 {from}", last.to()),
});
}
}
self.steps.push(step);
Ok(())
}
/// 步骤数。
#[must_use]
pub fn len(&self) -> usize {
self.steps.len()
}
/// 是否没有步骤。
#[must_use]
pub fn is_empty(&self) -> bool {
self.steps.is_empty()
}
/// 步骤说明清单(日志用)。
#[must_use]
pub fn describe(&self) -> Vec<&'static str> {
self.steps.iter().map(|step| step.describe()).collect()
}
/// 链能达到的最新版本。
#[must_use]
pub fn latest(&self) -> Option<SchemaVersion> {
self.steps.last().map(|step| step.to())
}
/// 从 `from` 起要执行的步骤(含 `from` 那一步)。
///
/// # Errors
/// 链里没有以 `from` 开头的步骤(版本不认识)时返回 [`SettingError::ChainBroken`]。
pub fn plan_from(&self, from: SchemaVersion) -> SettingResult<Vec<&dyn SettingMigration>> {
let start = self
.steps
.iter()
.position(|step| step.from() == from)
.ok_or_else(|| SettingError::ChainBroken {
reason: format!("链里没有以 {from} 起始的迁移步骤"),
})?;
Ok(self.steps[start..]
.iter()
.map(|step| step.as_ref())
.collect())
}
/// 顺序执行迁移,并把 `schemaVersion` 镜像更新成最终 major。
///
/// # Errors
/// 缺环 → [`SettingError::ChainBroken`];某步失败 → [`SettingError::Migration`]。
pub fn migrate(
&self,
from: SchemaVersion,
root: &mut Map<String, Value>,
) -> SettingResult<MigrationReport> {
let plan = self.plan_from(from)?;
let mut current = from;
let mut applied = Vec::with_capacity(plan.len());
for step in plan {
step.apply(root).map_err(|reason| SettingError::Migration {
from: current,
to: step.to(),
reason,
})?;
current = step.to();
applied.push(step.describe());
}
root.insert(SCHEMA_VERSION_KEY.to_owned(), Value::from(current.major));
Ok(MigrationReport {
from,
to: current,
applied,
})
}
}
/// 收据闸门:迁移前问一句「这次该不该跑」。
#[derive(Debug, Clone, Copy)]
pub struct MigrationGate<'a> {
receipt_path: &'a Path,
now_ms: u64,
}
impl<'a> MigrationGate<'a> {
/// 绑定收据路径与当前时间(毫秒 since UNIX_EPOCH,注入便于测试)。
#[must_use]
pub const fn new(receipt_path: &'a Path, now_ms: u64) -> Self {
Self {
receipt_path,
now_ms,
}
}
/// 当前时间。
#[must_use]
pub const fn now_ms(&self) -> u64 {
self.now_ms
}
/// 读收据并判断是否重放。
///
/// # Errors
/// 收据损坏或读失败时返回 [`crate::platform::error::PlatformError`]。
pub fn evaluate(&self, source_fingerprint: &str) -> PlatformResult<ReplayDecision> {
let existing = receipt::read(self.receipt_path)?;
Ok(receipt::decide(existing.as_ref(), source_fingerprint))
}
/// 是否该执行迁移(收据在且源指纹相同 → `false`)。
///
/// # Errors
/// 同 [`Self::evaluate`]。
pub fn should_run(&self, source_fingerprint: &str) -> PlatformResult<bool> {
Ok(self.evaluate(source_fingerprint)?.should_replay())
}
/// 记录一次迁移收据。
///
/// # Errors
/// 写盘失败时返回 [`crate::platform::error::PlatformError`]。
pub fn record(&self, receipt: &MigrationReceipt) -> PlatformResult<()> {
receipt::write(self.receipt_path, receipt)
}
}