Files
Unlucky-Launcher/crates/core/src/setting/migrate.rs
T
AskaEth bc41dfcf7d
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
Unlucky Launcher v0.0.1-alpha
Rust 原生单文件 Minecraft 启动器:CLI 完整可用(install/launch/accounts/java/diagnose/agent),
MCP server 接入外部 Agent 工具,GUI 开发中。
2026-10-31 21:54:10 +08:00

223 lines
7.1 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.
//! 迁移链:`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)
}
}