Files
CC-Switch/src-tauri/src/database/backup_restore_certification.rs
T
SaladDay a8246da3d6 test(pi): retire process-only guards from the contract suites
The suites were written as executable contracts for the implementation
process. Three of their checks only policed that process and would fail on
any legitimate future change:

- schema.rs/migration.rs/backup.rs pinned by SHA-256
- the impl Database write-surface symbol inventory pinned by name
- specific restore test function names asserted to exist

The behavioural assertions, the DML column-authority scanner, the type
barriers and the pinned oracle fixture hashes are kept: those catch real
regressions.

Headers are rewritten as contract documentation; the stale red/green
ledgers no longer described reality.
2026-08-03 08:01:24 +00:00

1081 lines
42 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.
#![cfg(test)]
// 裁决方授权:认证文件允许豁免纯风格类 clippy lint(不含任何安全/正确性
// lint)。type_complexity 属阈值型风格检查,对测试专用契约文件无意义。
#![allow(clippy::type_complexity)]
//! 前置工程 B:Canonical Restore 认证测试套件 v2(测试先行)
//!
//! 规则与前置 A 完全一致:实现方不得
//!
//! ## 与前置 A 的衔接(解冻声明)
//! 前置 A 套件以 SHA-256 冻结了 schema.rs/migration.rs/backup.rs。本前置
//! 工程的实现必然修改它们,因此:**pre-B 实现期间(已于 2026-08-02
//! 终止关账,该窗口已闭合),pre-A 的
//! `certify_infra_files_stay_frozen_after_restore_closeout` 预期为红**,这是裁决内
//! 的解冻,不是违规;pre-B 认证通过后由裁决方以终态哈希重冻基线。除此之外
//! pre-A 的其余 34 项必须全程保持绿——restore 改动破坏写面契约同样是失败。
//!
//! ## 范围界定
//! 既有 backup.rs 测试模块已实现修正案 2 E2 的大部分义务(恶意 schema 双
//! 入口、user_version 哨兵矩阵、VM/页预算、符号链接/FIFO/大小边界、NULL 与
//! 显式 ID 保真、canonical 预发布契约)。本套件不重复它们,而是:
//! 1. 绑定冻结这些既有测试(删除/改名/空壳化即红);
//! 2. 固化 R4 盲审确认、至今未修的缺口为可执行红灯;
//! 3. 以结构断言钉死类型屏障与搬运禁令。
//!
//! ## 义务清单(实现方交付,非本文件可执行部分)
//! O1【safety 窗口】`restore_from_backup` 必须自 safety backup 创建前起持有
//! live 写边界直至 publish 完成——期间任何并发写都不得既错过 safety
//! backup 又被 publish 覆盖。实现后补 seam 级确定性测试并上报并入。
//! **盲审重点核查项**:逐行核查 restore_from_backup 的锁范围。
//! O2【binary TOCTOU】文件解析链(backups 目录文件名解析 → 预检
//! symlink_metadata → O_NOFOLLOW 打开 → 打开后 fstat 身份复核)必须闭合,
//! 路径任何组件在检查后不得再经 symlink 重解析。**盲审重点核查项**。
//! O3 pre-B 认证通过后,裁决方重冻 infra 基线并将本套件红灯清单归零存档。
//!
//! ## 首轮认证失败后的契约裁决(v2,2026-08-01)
//! 首轮组件盲审(止于 89961dff)确认两 High 两 Medium,均属契约真空,
//! 现裁决如下:
//! 裁决5【写入所有权边界,High】进程内对 live 数据库文件的**可写连接必须
//! 唯一**(AppState Database 主连接);对 live 路径的任何其他打开必须
//! 携带 SQLITE_OPEN_READ_ONLY(database/mod.rs 的版本预检连接是现存违
//! 例)。restore 在 Database mutex 内自 safety backup 前持锁至 publish
//! 完成——唯一可写连接使该 mutex 成为真实的全局写边界,堵死"第二连接
//! 在 safety backup 后提交、被 publish 覆盖"的窗口。结构红灯 R5;
//! **盲审重点**:全树审计 Connection::open* 调用点与其目标路径。
//! 裁决6【迁移语义分离,High】迁移链必须携带
//! `MigrationRunContext { LocalUpgrade, UntrustedRestore }`;一切修复/
//! 合并/去重步骤(含 v16→v17 的 endpoint 去重)在 UntrustedRestore 下
//! 必须整体 abort 并给结构化错误,LocalUpgrade 保留既有容错。结构红灯
//! R6(调用点级绑定:enum 形状/迁移入口签名/不可信入口传
//! UntrustedRestore/本地入口传 LocalUpgrade);行为义务 O4:用既有
//! user_version 哨兵机器构造 v16 重复 endpoint 输入,断言 import 整体
//! Err、零合并——**O4 交付并经裁决并入是首轮重认证的前置条件**,
//! 不得以结构绿替代语义覆盖。语义注记:该枚举表达"修复策略",
//! fresh/canonical bootstrap 映射 LocalUpgrade。
//! 裁决7【REAL 值域,Medium】浮点域列必须声明有限区间;
//! `circuit_error_rate_threshold` ∈ [0,1] 且有限。行为红灯 R7。
//! O2 扩展【Windows TOCTOU,Medium】Windows 二进制路径必须基于已验证目录
//! 句柄相对打开(参考 NtCreateFile RootDirectory 语义)或在打开后按句柄
//! 身份复核,且补文件级预检;**盲审重点**,Linux 宿主无法执行,逐行核查。
//! 裁决5 边界补记:重复 Database::init()、其他连接 ATTACH live 库均属
//! 进程内旁路,纳入全树审计(盲审);**进程外写者**(第二进程、外部
//! SQLite 客户端、直接替换文件)不受进程内边界约束,single-instance
//! 插件不是数据库锁——显式列为残余风险,由发布说明与盲审知悉。
//! 重认证预算:修复后 2 轮 fresh 双审;写入所有权或迁移语义同 invariant
//! 复发 → 停止并上报用户。
//!
//! ## 残余风险与收口
//! 沿用前置 A 的收口边界与残余清单(动态 SQL、trigger/view、Backup API、
//! `#[path]` 等);本套件新增接受项:导出路径(`dump_sql`)的 `SELECT *`
//! 属导出语义,不在搬运禁令内;转移函数命名若脱离
//! restore/stage/scratch/publish/transfer/canonical 词根,搬运禁令扫描不到,
//! 由盲审兜底。清单外新绕过按盲审 finding 处理,不再扩充扫描器。
use crate::database::Database;
use std::collections::BTreeSet;
use std::fs;
use std::path::{Path, PathBuf};
use syn::visit::{self, Visit};
use syn::{Attribute, ImplItem, Item, Lit, Meta};
// ---------------------------------------------------------------------------
// 基建(与前置 A 同构;各套件自持,保持契约文件独立)
// ---------------------------------------------------------------------------
fn source_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("src")
}
fn backup_source() -> String {
fs::read_to_string(source_root().join("database/backup.rs")).expect("read backup.rs")
}
fn schema_source() -> String {
fs::read_to_string(source_root().join("database/schema.rs")).expect("read schema.rs")
}
fn cfg_expr_requires_test(expr: &str) -> bool {
let expr = expr.trim();
if expr == "test" {
return true;
}
let strip = |name: &str| -> Option<&str> {
expr.strip_prefix(name)
.and_then(|rest| rest.trim_start().strip_prefix('('))
.and_then(|rest| rest.strip_suffix(')'))
};
if let Some(args) = strip("all") {
return args.split(',').any(cfg_expr_requires_test);
}
if let Some(args) = strip("any") {
let parts: Vec<&str> = args.split(',').collect();
return !parts.is_empty() && parts.iter().all(|part| cfg_expr_requires_test(part));
}
false
}
fn attrs_mark_test_only(attrs: &[Attribute]) -> bool {
attrs.iter().any(|attribute| {
attribute.path().is_ident("cfg")
&& matches!(
&attribute.meta,
Meta::List(list) if cfg_expr_requires_test(&list.tokens.to_string())
)
})
}
/// 按函数收集生产字符串字面量(cfg-aware),供搬运禁令做函数级作用域判定。
#[derive(Default)]
struct FnLiteralCollector {
current: Vec<String>,
per_fn: Vec<(String, Vec<String>)>,
}
impl FnLiteralCollector {
fn enter_fn(&mut self, name: String, block: &syn::Block) {
let saved = std::mem::take(&mut self.current);
self.visit_block(block);
let literals = std::mem::replace(&mut self.current, saved);
self.per_fn.push((name, literals));
}
}
impl<'ast> Visit<'ast> for FnLiteralCollector {
fn visit_item(&mut self, item: &'ast Item) {
match item {
Item::Fn(function) if !attrs_mark_test_only(&function.attrs) => {
self.enter_fn(function.sig.ident.to_string(), &function.block);
}
Item::Impl(item_impl) if !attrs_mark_test_only(&item_impl.attrs) => {
for impl_item in &item_impl.items {
let ImplItem::Fn(function) = impl_item else {
continue;
};
if !attrs_mark_test_only(&function.attrs) {
self.enter_fn(function.sig.ident.to_string(), &function.block);
}
}
}
Item::Mod(item_mod) if !attrs_mark_test_only(&item_mod.attrs) => {
if let Some((_, nested)) = &item_mod.content {
for nested_item in nested {
self.visit_item(nested_item);
}
}
}
_ => {}
}
}
fn visit_expr_lit(&mut self, expression: &'ast syn::ExprLit) {
if let Lit::Str(literal) = &expression.lit {
self.current.push(literal.value());
}
visit::visit_expr_lit(self, expression);
}
}
fn find_fn<'a>(items: &'a [Item], name: &str) -> Option<&'a syn::ItemFn> {
for item in items {
match item {
Item::Fn(function) if function.sig.ident == name => return Some(function),
Item::Mod(item_mod) => {
let nested = item_mod.content.as_ref().map(|(_, items)| items.as_slice());
if let Some(found) = nested.and_then(|items| find_fn(items, name)) {
return Some(found);
}
}
_ => {}
}
}
None
}
fn find_impl_fn_signature(source: &syn::File, name: &str) -> Option<String> {
for item in &source.items {
let Item::Impl(item_impl) = item else {
continue;
};
for impl_item in &item_impl.items {
let ImplItem::Fn(function) = impl_item else {
continue;
};
if function.sig.ident == name {
let mut signature = String::new();
for input in &function.sig.inputs {
if let syn::FnArg::Typed(typed) = input {
let mut probe = IdentProbe::default();
probe.visit_type(&typed.ty);
signature.push_str(&probe.found.into_iter().collect::<Vec<_>>().join(","));
signature.push(';');
}
}
return Some(signature);
}
}
}
None
}
#[derive(Default)]
struct IdentProbe {
found: BTreeSet<String>,
}
impl<'ast> Visit<'ast> for IdentProbe {
fn visit_ident(&mut self, identifier: &'ast syn::Ident) {
self.found.insert(identifier.to_string());
}
// syn 的默认遍历不将方法名作为 ident 访问;绑定探针必须能看见
// `database.restore_from_backup(...)` 这类方法调用(裁决修正)。
fn visit_expr_method_call(&mut self, node: &'ast syn::ExprMethodCall) {
self.found.insert(node.method.to_string());
visit::visit_expr_method_call(self, node);
}
// syn 同样不遍历宏 token,而测试体大量使用 assert!(...) 包裹调用
// (backup.rs:2828 的 restore_from_backup 即在其中)。与前置 A 扫描器
// 的宏提取同源:把宏 token 按标识符切词纳入探针(裁决修正,第三层)。
fn visit_macro(&mut self, mac: &'ast syn::Macro) {
for word in mac
.tokens
.to_string()
.split(|c: char| !c.is_ascii_alphanumeric() && c != '_')
{
if !word.is_empty() {
self.found.insert(word.to_string());
}
}
visit::visit_macro(self, mac);
}
}
struct TestHomeGuard(Option<std::ffi::OsString>);
impl TestHomeGuard {
fn set(path: &Path) -> Self {
let previous = std::env::var_os("CC_SWITCH_TEST_HOME");
std::env::set_var("CC_SWITCH_TEST_HOME", path);
Self(previous)
}
}
impl Drop for TestHomeGuard {
fn drop(&mut self) {
match self.0.take() {
Some(previous) => std::env::set_var("CC_SWITCH_TEST_HOME", previous),
None => std::env::remove_var("CC_SWITCH_TEST_HOME"),
}
}
}
// ---------------------------------------------------------------------------
// S1:类型屏障——publish 只接受 CanonicalStage,工厂外不可构造
// ---------------------------------------------------------------------------
#[test]
fn certify_publish_consumes_only_canonical_stage() {
let syntax = syn::parse_file(&backup_source()).expect("parse backup.rs");
let signature = find_impl_fn_signature(&syntax, "publish_canonical_stage")
.expect("publish_canonical_stage exists");
assert!(
signature.contains("CanonicalStage"),
"publish must consume CanonicalStage, got param types: {signature}"
);
assert!(
!signature.contains("UntrustedScratch") && !signature.contains("Connection"),
"publish must not accept raw connections or untrusted scratch: {signature}"
);
// CanonicalStage 字段必须全私有:只有 schema.rs 的 current-schema 工厂
// 能构造,输入派生的 schema 在类型上无路可发布。
let schema = syn::parse_file(&schema_source()).expect("parse schema.rs");
fn find_struct<'a>(items: &'a [Item], name: &str) -> Option<&'a syn::ItemStruct> {
for item in items {
match item {
Item::Struct(item_struct) if item_struct.ident == name => return Some(item_struct),
Item::Mod(item_mod) => {
let nested = item_mod.content.as_ref().map(|(_, items)| items.as_slice());
if let Some(found) = nested.and_then(|items| find_struct(items, name)) {
return Some(found);
}
}
_ => {}
}
}
None
}
let stage = find_struct(&schema.items, "CanonicalStage").expect("CanonicalStage in schema.rs");
for field in &stage.fields {
assert!(
matches!(field.vis, syn::Visibility::Inherited),
"CanonicalStage fields must stay private to the schema factory"
);
}
// 禁止旁路转换:backup.rs/schema.rs 不得存在 UntrustedScratch →
// CanonicalStage 的 From/Into 实现。
for source in [backup_source(), schema_source()] {
let parsed = syn::parse_file(&source).expect("parse restore sources");
for item in &parsed.items {
let Item::Impl(item_impl) = item else {
continue;
};
let Some((_, trait_path, _)) = &item_impl.trait_ else {
continue;
};
let is_conversion = trait_path
.segments
.last()
.is_some_and(|segment| segment.ident == "From" || segment.ident == "Into");
if !is_conversion {
continue;
}
let mut probe = IdentProbe::default();
probe.visit_item_impl(item_impl);
assert!(
!(probe.found.contains("UntrustedScratch")
&& probe.found.contains("CanonicalStage")),
"no conversion between UntrustedScratch and CanonicalStage may exist"
);
}
}
}
// ---------------------------------------------------------------------------
// S2:搬运禁令——restore 侧函数不得使用通配/冲突改写动词
// ---------------------------------------------------------------------------
#[test]
fn certify_restore_transfer_forbids_wildcard_and_conflict_verbs() {
let syntax = syn::parse_file(&backup_source()).expect("parse backup.rs");
let mut collector = FnLiteralCollector::default();
for item in &syntax.items {
collector.visit_item(item);
}
let restore_scoped = |name: &str| {
[
"restore",
"stage",
"scratch",
"publish",
"transfer",
"canonical",
"copy_",
]
.iter()
.any(|root| name.contains(root))
};
// 核心搬运函数必须在扫描视野内(改名脱离词根即红,防止禁令被绕空)。
assert!(
collector
.per_fn
.iter()
.any(|(name, _)| name == "copy_fixed_table"),
"the fixed-column transfer function 'copy_fixed_table' must exist and be scanned"
);
let mut violations = Vec::new();
for (name, literals) in &collector.per_fn {
if !restore_scoped(name) {
continue;
}
for literal in literals {
let upper = literal.to_ascii_uppercase();
for banned in [
"SELECT *",
"INSERT OR IGNORE",
"INSERT OR REPLACE",
"ON CONFLICT DO UPDATE",
"REPLACE INTO",
] {
if upper.contains(banned) {
violations.push(format!("{name}: {banned}"));
}
}
}
}
assert!(
violations.is_empty(),
"restore-side transfer must use fixed column lists and plain INSERT:\n{}",
violations.join("\n")
);
}
#[test]
fn certify_no_wal_sidecar_manipulation() {
let syntax = syn::parse_file(&backup_source()).expect("parse backup.rs");
let mut collector = FnLiteralCollector::default();
for item in &syntax.items {
collector.visit_item(item);
}
for (name, literals) in &collector.per_fn {
for literal in literals {
assert!(
!literal.contains("-wal") && !literal.contains("-shm"),
"production fn '{name}' must not touch WAL/SHM sidecar files; \
all publication goes through the SQLite Backup API"
);
}
}
}
#[test]
fn certify_untrusted_scratch_hardening_idents_present() {
// 存在性绑定:NOFOLLOW 打开与打开后身份复核的关键符号不得被移除。
let syntax = syn::parse_file(&backup_source()).expect("parse backup.rs");
let mut probe = IdentProbe::default();
for item in &syntax.items {
if let Item::Fn(function) = item {
probe.visit_item_fn(function);
}
if let Item::Impl(item_impl) = item {
probe.visit_item_impl(item_impl);
}
if let Item::Struct(item_struct) = item {
probe.visit_item_struct(item_struct);
}
}
for required in ["O_NOFOLLOW", "SQLITE_OPEN_NOFOLLOW", "symlink_metadata"] {
assert!(
probe.found.contains(required),
"scratch hardening symbol '{required}' disappeared from backup.rs"
);
}
}
// ---------------------------------------------------------------------------
// S3:绑定冻结既有认证级测试(删除/改名/空壳化即红)
// ---------------------------------------------------------------------------
#[test]
fn certify_bound_restore_tests_present() {
let syntax = syn::parse_file(&backup_source()).expect("parse backup.rs");
for (required, must_reference) in [
(
"restore_policy_snapshot_is_exhaustive_and_detects_missing_table_fixture",
"RESTORE_TABLE_SPECS",
),
(
"sql_and_binary_restore_share_canonical_prepublication_contracts",
"assert_weak_ledgers_are_rebuilt",
),
(
"public_restore_entries_discard_hostile_schema_and_publish_only_canonical_objects",
"run_restore_entry",
),
(
"public_restore_entries_abort_invalid_rows_without_live_or_ledger_mutation",
"run_restore_entry",
),
(
"public_restore_entries_preserve_nulls_unknown_json_and_explicit_ids",
"run_restore_entry",
),
(
"every_supported_user_version_has_a_public_migration_sentinel",
"SCHEMA_VERSION",
),
(
"import_rejects_cross_file_statements_and_leaves_no_file_behind",
"import_sql_string",
),
(
"public_sql_restore_discards_input_trigger_before_local_copy",
"import_sql_string",
),
(
"public_file_restore_entries_reject_symlink_directory_and_fifo",
"restore_from_backup",
),
(
"restore_file_size_limits_accept_n_and_publicly_reject_n_plus_one",
"MAX_BINARY_RESTORE_BYTES",
),
(
"public_restore_entries_enforce_vm_and_page_budgets",
"RestoreLimitGuard",
),
("import_still_accepts_a_genuine_export", "export_sql_string"),
(
"publish_copies_device_local_ledgers_at_the_commit_boundary",
"UntrustedScratch",
),
] {
let function = find_fn(&syntax.items, required)
.unwrap_or_else(|| panic!("bound restore test '{required}' is missing"));
assert!(
function
.attrs
.iter()
.any(|attribute| attribute.path().is_ident("test")),
"'{required}' must be a #[test] function"
);
let mut probe = IdentProbe::default();
probe.visit_block(&function.block);
assert!(
probe.found.contains(must_reference),
"'{required}' must exercise '{must_reference}' (empty stubs cannot pass)"
);
}
}
// ---------------------------------------------------------------------------
// R1:导入值域——负 sort_index 不得发布
// ---------------------------------------------------------------------------
#[test]
fn certify_imported_sort_index_domain_is_enforced() {
use crate::database::NewProviderAggregate;
use crate::provider::ProviderMutationInput;
use serde_json::json;
let database = Database::memory().expect("memory db");
let input = ProviderMutationInput {
id: "domain-probe".to_string(),
name: "值域探针".to_string(),
settings_config: json!({"env": {}}),
website_url: None,
category: None,
created_at: Some(1_700_000_000),
sort_index: Some(7777),
notes: None,
meta: None,
icon: None,
icon_color: None,
in_failover_queue: false,
};
database
.create_provider(NewProviderAggregate::from_input("claude", input).expect("build"))
.expect("create");
let exported = database.export_sql_string().expect("export");
assert!(
exported.contains("7777"),
"export must contain the sort_index sentinel"
);
let hostile = exported.replace("7777", "-1");
let target = Database::memory().expect("target db");
let err = target
.import_sql_string(&hostile)
.expect_err("negative sort_index must abort the import: production reads Option<usize>");
let message = err.to_string();
assert!(
!message.is_empty(),
"domain violation must surface a real error"
);
// 值域违规必须整体中止:目标库不得出现该 provider 的任何残留。
let conn = target.conn.lock().expect("lock target");
let count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM providers WHERE id = 'domain-probe'",
[],
|row| row.get(0),
)
.expect("count probe");
assert_eq!(
count, 0,
"aborted import must leave no partial provider row"
);
}
// ---------------------------------------------------------------------------
// R2:publish 后 live 文件必须保持 INCREMENTAL auto-vacuum
// ---------------------------------------------------------------------------
#[test]
#[serial_test::serial]
fn certify_incremental_auto_vacuum_survives_restore() {
let home = tempfile::tempdir().expect("temp home");
let _guard = TestHomeGuard::set(home.path());
let database = Database::init().expect("file-backed db");
// 空库导出会被 import 的基本状态校验拒绝("未包含有效的供应商或 MCP
// 数据"),导致本测试在错误的位置变红;必须先播种最小数据,使红灯
// 精确落在 auto_vacuum 断言上、并在实现修复后能够转绿(裁决修正)。
{
use crate::database::NewProviderAggregate;
use crate::provider::ProviderMutationInput;
use serde_json::json;
let input = ProviderMutationInput {
id: "vacuum-probe".to_string(),
name: "auto-vacuum 探针".to_string(),
settings_config: json!({"env": {}}),
website_url: None,
category: None,
created_at: Some(1_700_000_000),
sort_index: None,
notes: None,
meta: None,
icon: None,
icon_color: None,
in_failover_queue: false,
};
database
.create_provider(NewProviderAggregate::from_input("claude", input).expect("build"))
.expect("seed provider");
}
let auto_vacuum_of = |db: &Database| -> i64 {
let conn = db.conn.lock().expect("lock");
conn.query_row("PRAGMA auto_vacuum", [], |row| row.get(0))
.expect("read auto_vacuum")
};
assert_eq!(
auto_vacuum_of(&database),
2,
"baseline: production file DB is created with INCREMENTAL auto-vacuum"
);
let exported = database.export_sql_string().expect("export");
database
.import_sql_string(&exported)
.expect("roundtrip import of a genuine export");
assert_eq!(
auto_vacuum_of(&database),
2,
"publish must not silently downgrade the live file to auto_vacuum=NONE \
(canonical stage must inherit INCREMENTAL before its first page is written)"
);
}
// ---------------------------------------------------------------------------
// R5:live 库的次级连接必须只读——调用级检查
// ---------------------------------------------------------------------------
/// 每个函数收集连接打开调用:(调用名, 该调用实参中的 ident 集)。
#[derive(Default)]
struct OpenCallCollector {
calls: Vec<(String, BTreeSet<String>)>,
}
impl<'ast> Visit<'ast> for OpenCallCollector {
fn visit_expr_call(&mut self, node: &'ast syn::ExprCall) {
if let syn::Expr::Path(path) = node.func.as_ref() {
if let Some(segment) = path.path.segments.last() {
let name = segment.ident.to_string();
if matches!(name.as_str(), "open" | "open_with_flags" | "open_in_memory") {
let mut args = IdentProbe::default();
for arg in &node.args {
args.visit_expr(arg);
}
self.calls.push((name, args.found));
}
}
}
visit::visit_expr_call(self, node);
}
fn visit_expr_method_call(&mut self, node: &'ast syn::ExprMethodCall) {
let name = node.method.to_string();
if matches!(name.as_str(), "open" | "open_with_flags" | "open_in_memory") {
let mut args = IdentProbe::default();
for arg in &node.args {
args.visit_expr(arg);
}
self.calls.push((name, args.found));
}
visit::visit_expr_method_call(self, node);
}
}
/// 裁决5 的调用级规则。返回违规描述;空即合规。
fn live_open_violations(fn_name: &str, calls: &[(String, BTreeSet<String>)]) -> Vec<String> {
let mut violations = Vec::new();
let mut primary_opens = 0usize;
for (call, args) in calls {
match call.as_str() {
"open_in_memory" => {}
"open" => {
if fn_name == "init" {
primary_opens += 1;
} else {
violations.push(format!(
"{fn_name}: bare Connection::open is read-write; secondary \
connections must use open_with_flags + SQLITE_OPEN_READ_ONLY"
));
}
}
"open_with_flags" => {
if fn_name == "init" {
primary_opens += 1;
} else if !args.contains("SQLITE_OPEN_READ_ONLY") {
violations.push(format!(
"{fn_name}: open_with_flags without SQLITE_OPEN_READ_ONLY"
));
}
}
_ => {}
}
}
if fn_name == "init" && primary_opens > 1 {
violations.push(
"init: more than one primary open; the writable live connection must be unique"
.to_string(),
);
}
violations
}
fn collect_open_calls(items: &[Item], out: &mut Vec<(String, Vec<(String, BTreeSet<String>)>)>) {
for item in items {
match item {
Item::Fn(function) if !attrs_mark_test_only(&function.attrs) => {
let mut collector = OpenCallCollector::default();
collector.visit_block(&function.block);
out.push((function.sig.ident.to_string(), collector.calls));
}
Item::Impl(item_impl) if !attrs_mark_test_only(&item_impl.attrs) => {
for impl_item in &item_impl.items {
let ImplItem::Fn(function) = impl_item else {
continue;
};
if !attrs_mark_test_only(&function.attrs) {
let mut collector = OpenCallCollector::default();
collector.visit_block(&function.block);
out.push((function.sig.ident.to_string(), collector.calls));
}
}
}
Item::Mod(item_mod) if !attrs_mark_test_only(&item_mod.attrs) => {
if let Some((_, nested)) = &item_mod.content {
collect_open_calls(nested, out);
}
}
_ => {}
}
}
}
#[test]
fn certify_live_database_secondary_opens_are_read_only() {
// 负向 fixture 先证明检查器会响(修复后假绿是 R9 终审确认的 P0)。
let fixture = r#"
impl Database {
fn bad_bare(path: &Path) { let _ = Connection::open(path); }
fn bad_flags(path: &Path) {
let _ = Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_WRITE);
}
fn good_flags(path: &Path) {
let _ = Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_ONLY);
}
fn good_memory() { let _ = Connection::open_in_memory(); }
fn init(path: &Path) {
let _ = Connection::open(path);
let _ = Connection::open(path);
}
}
"#;
let parsed = syn::parse_file(fixture).expect("parse fixture");
let mut fixture_fns = Vec::new();
collect_open_calls(&parsed.items, &mut fixture_fns);
let violations_of = |name: &str, fns: &[(String, Vec<(String, BTreeSet<String>)>)]| {
fns.iter()
.filter(|(fn_name, _)| fn_name == name)
.flat_map(|(fn_name, calls)| live_open_violations(fn_name, calls))
.collect::<Vec<_>>()
};
assert!(!violations_of("bad_bare", &fixture_fns).is_empty());
assert!(!violations_of("bad_flags", &fixture_fns).is_empty());
assert!(violations_of("good_flags", &fixture_fns).is_empty());
assert!(violations_of("good_memory", &fixture_fns).is_empty());
assert!(
!violations_of("init", &fixture_fns).is_empty(),
"a second primary open inside init must be flagged"
);
// 真实检查:database/mod.rs 全部生产函数。
let source =
fs::read_to_string(source_root().join("database/mod.rs")).expect("read database/mod.rs");
let syntax = syn::parse_file(&source).expect("parse database/mod.rs");
let mut fns = Vec::new();
collect_open_calls(&syntax.items, &mut fns);
let mut violations = Vec::new();
for (name, calls) in &fns {
if name == "memory" {
continue; // 测试内存库工厂
}
violations.extend(live_open_violations(name, calls));
}
assert!(
violations.is_empty(),
"writable live-database connections must be unique to init: {violations:?}"
);
}
// ---------------------------------------------------------------------------
// R6:迁移语义分离——迁移调用实参级绑定
// ---------------------------------------------------------------------------
/// 收集对 `apply_schema_migrations_on_conn` 的每次调用及其实参 ident 集。
#[derive(Default)]
struct MigrationCallCollector {
calls: Vec<BTreeSet<String>>,
}
impl<'ast> Visit<'ast> for MigrationCallCollector {
fn visit_expr_call(&mut self, node: &'ast syn::ExprCall) {
if let syn::Expr::Path(path) = node.func.as_ref() {
if path
.path
.segments
.last()
.is_some_and(|segment| segment.ident == "apply_schema_migrations_on_conn")
{
let mut args = IdentProbe::default();
for arg in &node.args {
args.visit_expr(arg);
}
self.calls.push(args.found);
}
}
visit::visit_expr_call(self, node);
}
fn visit_expr_method_call(&mut self, node: &'ast syn::ExprMethodCall) {
if node.method == "apply_schema_migrations_on_conn" {
let mut args = IdentProbe::default();
for arg in &node.args {
args.visit_expr(arg);
}
self.calls.push(args.found);
}
visit::visit_expr_method_call(self, node);
}
}
#[test]
fn certify_untrusted_migration_context_is_threaded() {
// 函数体 ident 聚合会被迁移函数内部的 `match context` 分支假绿
// (R10 终审 P0),因此绑定到调用实参:
// 1) enum 精确形状;2) 确切入口 apply_schema_migrations_on_conn 的签名
// 携带 context;3) 不可信区域的迁移调用实参含 UntrustedRestore 且绝无
// LocalUpgrade;4) 本地区域的迁移调用实参含 LocalUpgrade 且绝无
// UntrustedRestore。v16 去重 fail-closed 的行为覆盖由义务 O4 交付
// (首轮重认证前置条件)。语义注记:枚举表达"修复策略",
// fresh/canonical bootstrap 映射 LocalUpgrade。
let schema = syn::parse_file(&schema_source()).expect("parse schema.rs");
let migration_source =
fs::read_to_string(source_root().join("database/migration.rs")).expect("read migration.rs");
let migration = syn::parse_file(&migration_source).expect("parse migration.rs");
// 1) enum 精确形状
fn find_enum<'a>(items: &'a [Item], name: &str) -> Option<&'a syn::ItemEnum> {
for item in items {
match item {
Item::Enum(item_enum) if item_enum.ident == name => return Some(item_enum),
Item::Mod(item_mod) => {
let nested = item_mod.content.as_ref().map(|(_, items)| items.as_slice());
if let Some(found) = nested.and_then(|items| find_enum(items, name)) {
return Some(found);
}
}
_ => {}
}
}
None
}
let context_enum = find_enum(&schema.items, "MigrationRunContext")
.or_else(|| find_enum(&migration.items, "MigrationRunContext"))
.expect("MigrationRunContext enum must exist in the migration chain");
let variants: Vec<String> = context_enum
.variants
.iter()
.map(|variant| variant.ident.to_string())
.collect();
assert_eq!(
variants,
vec!["LocalUpgrade".to_string(), "UntrustedRestore".to_string()],
"MigrationRunContext variants drifted from the adjudicated contract"
);
// 2) 确切入口签名携带 context
fn entry_takes_context(items: &[Item]) -> bool {
for item in items {
match item {
Item::Fn(function) if function.sig.ident == "apply_schema_migrations_on_conn" => {
let mut probe = IdentProbe::default();
for input in &function.sig.inputs {
if let syn::FnArg::Typed(typed) = input {
probe.visit_type(&typed.ty);
}
}
return probe.found.contains("MigrationRunContext");
}
Item::Impl(item_impl) => {
for impl_item in &item_impl.items {
let ImplItem::Fn(function) = impl_item else {
continue;
};
if function.sig.ident != "apply_schema_migrations_on_conn" {
continue;
}
let mut probe = IdentProbe::default();
for input in &function.sig.inputs {
if let syn::FnArg::Typed(typed) = input {
probe.visit_type(&typed.ty);
}
}
return probe.found.contains("MigrationRunContext");
}
}
Item::Mod(item_mod) => {
if let Some((_, nested)) = &item_mod.content {
if entry_takes_context(nested) {
return true;
}
}
}
_ => {}
}
}
false
}
assert!(
entry_takes_context(&schema.items) || entry_takes_context(&migration.items),
"apply_schema_migrations_on_conn must take MigrationRunContext in its signature"
);
// 3) 不可信区域:UntrustedScratch impl 内的迁移调用
let backup = syn::parse_file(&backup_source()).expect("parse backup.rs");
let mut untrusted_calls = MigrationCallCollector::default();
for item in &backup.items {
let Item::Impl(item_impl) = item else {
continue;
};
let matches_type = matches!(
item_impl.self_ty.as_ref(),
syn::Type::Path(type_path)
if type_path
.path
.segments
.last()
.is_some_and(|segment| segment.ident == "UntrustedScratch")
);
if !matches_type {
continue;
}
for impl_item in &item_impl.items {
if let ImplItem::Fn(function) = impl_item {
untrusted_calls.visit_block(&function.block);
}
}
}
assert!(
untrusted_calls
.calls
.iter()
.any(|args| args.contains("UntrustedRestore")),
"the untrusted scratch pipeline must call migrations with UntrustedRestore"
);
assert!(
untrusted_calls
.calls
.iter()
.all(|args| !args.contains("LocalUpgrade")),
"the untrusted pipeline must never request tolerant LocalUpgrade repairs"
);
// 4) 本地区域:database/mod.rs schema.rs migration.rs 生产函数内的
// 迁移调用(排除 backup.rs;调用实参而非函数体,match 分支无法假绿)
let mod_source =
fs::read_to_string(source_root().join("database/mod.rs")).expect("read database/mod.rs");
let mod_parsed = syn::parse_file(&mod_source).expect("parse database/mod.rs");
let mut local_calls = MigrationCallCollector::default();
fn visit_production_fns(items: &[Item], collector: &mut MigrationCallCollector) {
for item in items {
match item {
Item::Fn(function) if !attrs_mark_test_only(&function.attrs) => {
collector.visit_block(&function.block);
}
Item::Impl(item_impl) if !attrs_mark_test_only(&item_impl.attrs) => {
for impl_item in &item_impl.items {
if let ImplItem::Fn(function) = impl_item {
if !attrs_mark_test_only(&function.attrs) {
collector.visit_block(&function.block);
}
}
}
}
Item::Mod(item_mod) if !attrs_mark_test_only(&item_mod.attrs) => {
if let Some((_, nested)) = &item_mod.content {
visit_production_fns(nested, collector);
}
}
_ => {}
}
}
}
visit_production_fns(&mod_parsed.items, &mut local_calls);
visit_production_fns(&schema.items, &mut local_calls);
visit_production_fns(&migration.items, &mut local_calls);
assert!(
local_calls
.calls
.iter()
.any(|args| args.contains("LocalUpgrade")),
"a production local-upgrade call site must pass LocalUpgrade"
);
assert!(
local_calls
.calls
.iter()
.all(|args| !args.contains("UntrustedRestore")),
"local upgrade paths must never run under UntrustedRestore semantics"
);
}
// ---------------------------------------------------------------------------
// R7:REAL 值域——熔断阈值必须限于有限 [0,1](定向篡改)
// ---------------------------------------------------------------------------
#[test]
fn certify_imported_circuit_threshold_domain_is_enforced() {
let database = Database::memory().expect("memory db");
let exported = database.export_sql_string().expect("export");
// 定向替换:只动 proxy_config 表插入行里的阈值,不碰 DDL 默认值与
// model_pricing 的 0.60/0.65 子串(R9 终审确认全局替换会误伤)。
for hostile_value in ["6.5", "-0.5", "1e999"] {
let mut replaced = 0usize;
let hostile: String = exported
.lines()
.map(|line| {
if replaced == 0
&& line.contains("proxy_config")
&& !line.to_ascii_uppercase().contains("CREATE TABLE")
&& line.contains(", 0.6,")
{
replaced += 1;
let mut tampered = line.replacen(", 0.6,", &format!(", {hostile_value},"), 1);
tampered.push('\n');
tampered
} else {
let mut kept = line.to_string();
kept.push('\n');
kept
}
})
.collect();
assert_eq!(
replaced, 1,
"exactly one proxy_config insert row must carry the 0.6 sentinel \
(export format drifted; escalate for adjudication)"
);
let target = Database::memory().expect("target db");
target
.import_sql_string(&hostile)
.expect_err("circuit_error_rate_threshold outside finite [0,1] must abort the import");
let conn = target.conn.lock().expect("lock target");
let out_of_domain: i64 = conn
.query_row(
"SELECT COUNT(*) FROM proxy_config
WHERE circuit_error_rate_threshold < 0.0
OR circuit_error_rate_threshold > 1.0",
[],
|row| row.get(0),
)
.expect("count out-of-domain thresholds");
assert_eq!(
out_of_domain, 0,
"aborted import must leave no out-of-domain breaker thresholds behind"
);
}
}