refactor(database): scope untrusted restore to N/N-1

This commit is contained in:
SaladDay
2026-08-02 05:29:48 +00:00
parent ce6edf68a0
commit 3fa6b1f158
6 changed files with 536 additions and 1831 deletions
+25 -21
View File
@@ -1322,7 +1322,9 @@ impl UntrustedScratch {
scratch.connection.authorizer(
None::<fn(rusqlite::hooks::AuthContext<'_>) -> rusqlite::hooks::Authorization>,
);
result.map_err(|error| AppError::Database(format!("execute SQL import: {error}")))?;
result.map_err(|error| {
AppError::InvalidInput(format!("execute untrusted SQL import: {error}"))
})?;
scratch.finish_input()
}
@@ -1355,6 +1357,10 @@ impl UntrustedScratch {
"restore schema version {version} is newer than supported {SCHEMA_VERSION}"
)));
}
// Gate obsolete backups before schema inspection, sanitizing DDL, or
// migration dispatch. LocalUpgrade never enters this scratch path and
// retains the complete historical in-place migration chain.
super::migration_source::require_supported_untrusted_restore_version(version)?;
self.drop_untrusted_executable_objects()?;
self.connection
.set_db_config(DbConfig::SQLITE_DBCONFIG_ENABLE_TRIGGER, true)
@@ -1829,16 +1835,19 @@ fn canonical_user_tables(
let mut statement = conn
.prepare(
"SELECT name FROM sqlite_schema
WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
WHERE type = 'table'
ORDER BY name",
)
.map_err(|error| AppError::Database(error.to_string()))?;
let tables = statement
.query_map([], |row| row.get::<_, String>(0))
.map_err(|error| AppError::Database(error.to_string()))?
.collect::<Result<std::collections::BTreeSet<_>, _>>()
.collect::<Result<Vec<_>, _>>()
.map_err(|error| AppError::Database(error.to_string()))?;
Ok(tables)
Ok(tables
.into_iter()
.filter(|name| !super::is_sqlite_internal_table_name(name))
.collect())
}
fn assert_restore_policy_coverage(
@@ -2444,15 +2453,16 @@ impl Database {
let name: String = row.get(1).map_err(|e| AppError::Database(e.to_string()))?;
let sql: String = row.get(3).map_err(|e| AppError::Database(e.to_string()))?;
// 跳过 SQLite 内部对象(如 sqlite_sequence
if name.starts_with("sqlite_") {
// Skip only the exact internal objects owned by this SQLite build.
// Prefix matching would misclassify names such as `sqliteX`.
if super::is_sqlite_internal_table_name(&name) {
continue;
}
output.push_str(&sql);
output.push_str(";\n");
if obj_type == "table" && !name.starts_with("sqlite_") {
if obj_type == "table" && !super::is_sqlite_internal_table_name(&name) {
tables.push(name);
}
}
@@ -4008,9 +4018,8 @@ mod tests {
})?;
let _home_guard = TestHomeGuard::set(test_home.path());
// The source-spec authority supports exactly v1..v17. A legacy v0
// label must fail before migration DDL instead of being interpreted
// through the permissive local-upgrade path.
// A legacy v0 label must fail at the N/N-1 gate before migration DDL
// instead of being interpreted through the local-upgrade path.
for (entry_index, entry_point) in [RestoreEntryPoint::Sql, RestoreEntryPoint::Binary]
.into_iter()
.enumerate()
@@ -4042,9 +4051,8 @@ mod tests {
.expect_err("untrusted v0 is outside the declared source-spec range");
assert!(
matches!(error, AppError::InvalidInput(_))
&& error
.to_string()
.contains("unsupported restore user_version 0"),
&& error.to_string().contains("user_version=0")
&& error.to_string().contains("备份版本过旧"),
"v0 must fail at source recognition via entry {entry_index}: {error:?}"
);
}
@@ -4095,10 +4103,10 @@ mod tests {
assert_eq!(sentinel, (None, 0, 0, 0));
}
// Every supported version is materialized from its exact source spec.
// Both supported versions are materialized from their exact source specs.
// Each public entry must preserve a real Pi provider and its endpoint;
// this cannot pass by stamping a current database with an old version.
for version in 1..=SCHEMA_VERSION {
// this cannot pass by stamping a v17 database with a v16 label.
for version in (SCHEMA_VERSION - 1)..=SCHEMA_VERSION {
for (entry_index, entry_point) in [RestoreEntryPoint::Sql, RestoreEntryPoint::Binary]
.into_iter()
.enumerate()
@@ -4128,11 +4136,7 @@ mod tests {
.contains(&format!("\"migrationVersion\":{version}")),
"Pi settings payload was not preserved for v{version}"
);
assert_eq!(
restored.1,
if version == 1 { "1.0" } else { "1.25" },
"provider migration sentinel v{version}"
);
assert_eq!(restored.1, "1.25", "provider migration sentinel v{version}");
assert_eq!(
restored.2,
format!("https://pi-endpoint-v{version}.example/v1"),