commit cc9aa1ef5d6e570a5d6d834c0d602e301375b1dd
parent 4f3b51fa899a4e91661eef0f5600f98ea7a5b99e
Author: triesap <tyson@radroots.org>
Date: Fri, 14 Aug 2026 22:38:01 +0000
service-sqlite: seal incremental backup native access
- bind online backup to SQLx-owned locked source and destination handles
- isolate the exact native backup calls behind one lifetime-bound adapter
- preserve page batching, cancellation cleanup, and authority precedence
- guard the sole native linkage, unsafe allowance, and cross-target boundary
Diffstat:
13 files changed, 383 insertions(+), 57 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -3526,6 +3526,7 @@ version = "0.1.0-alpha"
dependencies = [
"fs2",
"futures",
+ "libsqlite3-sys",
"radroots_runtime_paths",
"radroots_storage",
"rusqlite",
diff --git a/Cargo.toml b/Cargo.toml
@@ -96,7 +96,7 @@ authors = ["Tyson Lupul <tyson@radroots.org>"]
readme = "README.md"
[workspace.lints.rust]
-unsafe_code = "forbid"
+unsafe_code = "deny"
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] }
[workspace.lints.rustdoc]
@@ -208,6 +208,7 @@ keyring = { version = "3.6.3", default-features = false, features = [
"vendored",
] }
libc = { version = "0.2" }
+libsqlite3-sys = { version = "=0.37.0", default-features = false }
nostr = { version = "0.44.7", default-features = false }
nostr-relay-pool = { version = "0.44.0" }
nostr-sdk = { version = "0.44.1" }
diff --git a/contracts/releases/sqlite_runtime.toml b/contracts/releases/sqlite_runtime.toml
@@ -31,7 +31,7 @@ forbidden_high_level_dependencies = [
owner_package = "radroots_service_sqlite"
relative_module = "crates/service_sqlite/src/sqlite_native_backup.rs"
capability = "incremental_online_backup"
-status = "planned"
+status = "active"
[migration]
owner = "rcld-rshr-045"
diff --git a/crates/service_sqlite/Cargo.toml b/crates/service_sqlite/Cargo.toml
@@ -14,6 +14,7 @@ readme = "README.md"
[dependencies]
fs2 = { workspace = true }
futures = { workspace = true }
+libsqlite3-sys = { workspace = true }
radroots_runtime_paths = { workspace = true }
radroots_storage = { workspace = true }
rusqlite = { workspace = true, features = ["backup", "bundled"] }
diff --git a/crates/service_sqlite/src/backup/capture.rs b/crates/service_sqlite/src/backup/capture.rs
@@ -18,18 +18,22 @@ use std::{
#[cfg(test)]
use core::sync::atomic::AtomicU8;
-use rusqlite::{Connection, OpenFlags, OptionalExtension, backup::StepResult, types::ValueRef};
+use rusqlite::{Connection, OpenFlags, OptionalExtension, types::ValueRef};
use rustix::{
fs::{AtFlags, FileType, Mode, OFlags, fchmod, fstat, mkdirat, open, openat, statat, unlinkat},
process::geteuid,
};
use sha2::{Digest, Sha256};
-use sqlx::{Sqlite, pool::PoolConnection};
+use sqlx::{
+ ConnectOptions, Connection as _, Sqlite, SqliteConnection, pool::PoolConnection,
+ sqlite::SqliteConnectOptions,
+};
use crate::{
BackupCreatedAtUnixMs, BackupMemberSha256, OpenMode, ServiceBackupManifest,
ServiceDatabaseMetadata, ServiceSqliteError, ServiceSqliteErrorKind,
open::{BackupSourceValidator, PrivateConnectionPool},
+ sqlite_native_backup::{NativeBackup, NativeBackupStep},
};
const MAX_STAGING_PATH_BYTES: usize = 4_096;
@@ -252,7 +256,7 @@ async fn capture_online_backup_with_operations(
let cancellation = Arc::new(AtomicBool::new(false));
let cancellation_guard = CaptureCancellation::new(Arc::clone(&cancellation));
let worker = CaptureWorker {
- _admission: admission,
+ admission: Some(admission),
_permit: permit,
validator,
metadata,
@@ -261,6 +265,7 @@ async fn capture_online_backup_with_operations(
cancellation,
operations,
failpoints: failpoints.clone(),
+ runtime: tokio::runtime::Handle::current(),
};
let joined = tokio::task::spawn_blocking(move || worker.run()).await;
test_async_phase(TEST_CAPTURE_PHASE_JOIN_AWAITED).await;
@@ -316,7 +321,7 @@ impl Drop for CaptureCancellation {
}
struct CaptureWorker {
- _admission: PoolConnection<Sqlite>,
+ admission: Option<PoolConnection<Sqlite>>,
_permit: CapturePermit,
validator: BackupSourceValidator,
metadata: ServiceDatabaseMetadata,
@@ -325,10 +330,11 @@ struct CaptureWorker {
cancellation: Arc<AtomicBool>,
operations: Arc<dyn CaptureOperations>,
failpoints: crate::failpoint::DurabilityFailpoints,
+ runtime: tokio::runtime::Handle,
}
impl CaptureWorker {
- fn run(self) -> Result<PendingCapture, ServiceSqliteError> {
+ fn run(mut self) -> Result<PendingCapture, ServiceSqliteError> {
self.check_cancelled()?;
self.validator.validate()?;
self.test_phase(TEST_CAPTURE_PHASE_BEFORE_CREATE);
@@ -353,46 +359,33 @@ impl CaptureWorker {
self.validator.validate()?;
staging.validate()?;
- let source = self.open_source()?;
- let mut destination = self.open_destination(&staging)?;
- staging.record_sidecars();
+ let source = self.open_inspection_source()?;
verify_database_inventory(&source)?;
verify_database_metadata(&source, &self.metadata)?;
+ source
+ .close()
+ .map_err(|(_, source)| backup_source(BackupFailureKind::Capture, source))?;
self.validator.validate()?;
staging.validate()?;
+ let mut destination = self.open_sqlx_destination(&staging)?;
+ staging.record_sidecars();
+
self.hit_checked(
Some(&staging),
crate::failpoint::DurabilityFailpoint::BackupBeforeCopy,
BackupFailureKind::Capture,
)?;
- {
- let backup = match rusqlite::backup::Backup::new(&source, &mut destination) {
- Ok(backup) => backup,
- Err(source) => {
- staging.record_sidecars();
- return Err(backup_source(BackupFailureKind::Capture, source));
- }
- };
- loop {
- self.check_cancelled()?;
- self.validator.validate()?;
- staging.validate()?;
- let step = backup.step(BACKUP_PAGES_PER_STEP);
- staging.record_sidecars();
- self.test_phase(TEST_CAPTURE_PHASE_BACKUP_STEPPED);
- let step =
- step.map_err(|source| backup_source(BackupFailureKind::Capture, source))?;
- self.validator.validate()?;
- staging.validate()?;
- match step {
- StepResult::Done => break,
- StepResult::More => {}
- StepResult::Busy | StepResult::Locked => thread::yield_now(),
- _ => return Err(backup_error(BackupFailureKind::Capture)),
- }
- }
- }
+ let capture_result = self.copy_with_locked_sqlx_handles(&mut destination, &mut staging);
+ let close_result = self
+ .runtime
+ .block_on(destination.close())
+ .map_err(|source| backup_source(BackupFailureKind::Capture, source));
+ staging.record_sidecars();
+ self.validator.validate()?;
+ staging.validate()?;
+ capture_result?;
+ close_result?;
self.hit_checked(
Some(&staging),
crate::failpoint::DurabilityFailpoint::BackupAfterCopy,
@@ -404,6 +397,7 @@ impl CaptureWorker {
self.check_cancelled()?;
self.validator.validate()?;
staging.validate()?;
+ let destination = self.open_inspection_destination(&staging)?;
verify_database_inventory(&destination)?;
verify_database_metadata(&destination, &self.metadata)?;
verify_integrity(&destination)?;
@@ -416,9 +410,6 @@ impl CaptureWorker {
.close()
.map_err(|(_, source)| backup_source(BackupFailureKind::Capture, source))?;
staging.record_sidecars();
- source
- .close()
- .map_err(|(_, source)| backup_source(BackupFailureKind::Capture, source))?;
self.validator.validate()?;
staging.validate()?;
staging.validate_inventory()?;
@@ -463,13 +454,63 @@ impl CaptureWorker {
.map_err(|source| backup_source(BackupFailureKind::Manifest, source))?;
Ok(PendingCapture {
staging,
- _admission: self._admission,
+ _admission: self
+ .admission
+ .take()
+ .ok_or_else(|| backup_error(BackupFailureKind::Capture))?,
_permit: self._permit,
manifest,
})
}
- fn open_source(&self) -> Result<Connection, ServiceSqliteError> {
+ fn copy_with_locked_sqlx_handles(
+ &mut self,
+ destination: &mut SqliteConnection,
+ staging: &mut StagingGuard,
+ ) -> Result<(), ServiceSqliteError> {
+ let mut admission = self
+ .admission
+ .take()
+ .ok_or_else(|| backup_error(BackupFailureKind::Capture))?;
+ let result = (|| {
+ let mut source_handle = self
+ .runtime
+ .block_on(admission.lock_handle())
+ .map_err(|source| backup_source(BackupFailureKind::Capture, source))?;
+ let mut destination_handle = self
+ .runtime
+ .block_on(destination.lock_handle())
+ .map_err(|source| backup_source(BackupFailureKind::Capture, source))?;
+ let mut backup = NativeBackup::start(&mut destination_handle, &mut source_handle)
+ .map_err(|source| backup_source(BackupFailureKind::Capture, source))?;
+ loop {
+ self.check_cancelled()?;
+ self.validator.validate()?;
+ staging.validate()?;
+ let step = backup
+ .step(BACKUP_PAGES_PER_STEP)
+ .map_err(|source| backup_source(BackupFailureKind::Capture, source));
+ staging.record_sidecars();
+ self.test_phase(TEST_CAPTURE_PHASE_BACKUP_STEPPED);
+ let step = step?;
+ self.validator.validate()?;
+ staging.validate()?;
+ match step {
+ NativeBackupStep::Done => {
+ return backup
+ .finish()
+ .map_err(|source| backup_source(BackupFailureKind::Capture, source));
+ }
+ NativeBackupStep::More => {}
+ NativeBackupStep::Busy | NativeBackupStep::Locked => thread::yield_now(),
+ }
+ }
+ })();
+ self.admission = Some(admission);
+ result
+ }
+
+ fn open_inspection_source(&self) -> Result<Connection, ServiceSqliteError> {
self.validator.validate()?;
let result = Connection::open_with_flags(
self.validator.database_path(),
@@ -486,7 +527,27 @@ impl CaptureWorker {
Ok(connection)
}
- fn open_destination(&self, staging: &StagingGuard) -> Result<Connection, ServiceSqliteError> {
+ fn open_sqlx_destination(
+ &self,
+ staging: &StagingGuard,
+ ) -> Result<SqliteConnection, ServiceSqliteError> {
+ staging.validate()?;
+ let options = SqliteConnectOptions::new()
+ .filename(staging.state_path())
+ .create_if_missing(false)
+ .foreign_keys(false)
+ .disable_statement_logging();
+ let result = self
+ .runtime
+ .block_on(SqliteConnection::connect_with(&options));
+ staging.validate()?;
+ result.map_err(|source| backup_source(BackupFailureKind::Capture, source))
+ }
+
+ fn open_inspection_destination(
+ &self,
+ staging: &StagingGuard,
+ ) -> Result<Connection, ServiceSqliteError> {
staging.validate()?;
let result = Connection::open_with_flags(
staging.state_path(),
diff --git a/crates/service_sqlite/src/initialize.rs b/crates/service_sqlite/src/initialize.rs
@@ -178,6 +178,7 @@ fn initialization_error(cause: InitializationCause) -> ServiceSqliteError {
ServiceSqliteError::with_source(ServiceSqliteErrorKind::Create, cause)
}
+#[cfg(any(test, target_os = "linux", target_os = "macos"))]
fn require_initialization_condition(
condition: bool,
kind: InitializationFailureKind,
@@ -194,7 +195,7 @@ mod failure_tests {
#[test]
fn initialization_failure_inventory_is_complete_and_source_aware() {
- let mut cases = vec![
+ let cases = [
(
InitializationFailureKind::UnsupportedMode,
"SQLite initialization requires initialize mode",
@@ -203,9 +204,10 @@ mod failure_tests {
InitializationFailureKind::CreateUnavailable,
"SQLite state could not be reserved",
),
- ];
+ ]
+ .into_iter();
#[cfg(any(target_os = "linux", target_os = "macos"))]
- cases.extend([
+ let cases = cases.chain([
(
InitializationFailureKind::StateAlreadyExists,
"SQLite state already exists",
diff --git a/crates/service_sqlite/src/integrity/catalog.rs b/crates/service_sqlite/src/integrity/catalog.rs
@@ -355,7 +355,7 @@ impl SchemaCatalog {
self.digest
}
- #[cfg(any(target_os = "linux", target_os = "macos"))]
+ #[cfg(any(test, target_os = "linux", target_os = "macos"))]
pub(crate) fn matches_migrations(&self, migrations: &MigrationCatalog) -> bool {
crate::all_constraints([
self.migration_catalog_digest == migrations.digest(),
@@ -364,7 +364,7 @@ impl SchemaCatalog {
])
}
- #[cfg(any(target_os = "linux", target_os = "macos"))]
+ #[cfg(any(test, target_os = "linux", target_os = "macos"))]
pub(crate) fn version(&self, version: u32) -> Option<SchemaVersionCatalog> {
let index = usize::try_from(version.checked_sub(1)?).ok()?;
self.versions.get(index).copied()
diff --git a/crates/service_sqlite/src/lib.rs b/crates/service_sqlite/src/lib.rs
@@ -1,4 +1,4 @@
-#![forbid(unsafe_code)]
+#![deny(unsafe_code)]
#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
//! Reusable, service-neutral SQLite mechanics for Radroots services.
@@ -18,6 +18,12 @@ mod migration;
mod native_metadata;
mod open;
mod restore;
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+#[allow(
+ unsafe_code,
+ reason = "the sealed SQLx-handle adapter owns the missing SQLite online-backup calls"
+)]
+mod sqlite_native_backup;
mod status;
mod transaction_control;
@@ -25,6 +31,7 @@ pub(crate) fn all_constraints<const N: usize>(constraints: [bool; N]) -> bool {
constraints.into_iter().all(core::convert::identity)
}
+#[cfg(any(test, target_os = "linux", target_os = "macos"))]
pub(crate) fn require_condition(
condition: bool,
kind: ServiceSqliteErrorKind,
diff --git a/crates/service_sqlite/src/sqlite_native_backup.rs b/crates/service_sqlite/src/sqlite_native_backup.rs
@@ -0,0 +1,168 @@
+//! Sealed online-backup calls over SQLx-owned locked SQLite handles.
+
+use core::{fmt, marker::PhantomData, ptr::NonNull};
+use std::error::Error;
+
+use libsqlite3_sys as ffi;
+use sqlx::sqlite::LockedSqliteHandle;
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub(crate) enum NativeBackupStep {
+ Done,
+ More,
+ Busy,
+ Locked,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+enum NativeBackupFailureKind {
+ Initialize,
+ Step,
+ Finish,
+}
+
+#[derive(PartialEq, Eq)]
+pub(crate) struct NativeBackupError {
+ kind: NativeBackupFailureKind,
+ code: i32,
+}
+
+impl fmt::Debug for NativeBackupError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("NativeBackupError")
+ .field("kind", &self.kind)
+ .field("code", &self.code)
+ .finish()
+ }
+}
+
+impl fmt::Display for NativeBackupError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str(match self.kind {
+ NativeBackupFailureKind::Initialize => "native SQLite backup initialization failed",
+ NativeBackupFailureKind::Step => "native SQLite backup step failed",
+ NativeBackupFailureKind::Finish => "native SQLite backup finalization failed",
+ })
+ }
+}
+
+impl Error for NativeBackupError {}
+
+pub(crate) struct NativeBackup<'destination, 'source> {
+ handle: Option<NonNull<ffi::sqlite3_backup>>,
+ _locked_handles: PhantomData<(&'destination mut (), &'source mut ())>,
+}
+
+impl<'destination, 'source> NativeBackup<'destination, 'source> {
+ pub(crate) fn start(
+ destination: &'destination mut LockedSqliteHandle<'_>,
+ source: &'source mut LockedSqliteHandle<'_>,
+ ) -> Result<Self, NativeBackupError> {
+ let destination = destination.as_raw_handle();
+ let source = source.as_raw_handle();
+ // SAFETY: both handles are borrowed from live SQLx lock guards, the
+ // fixed schema names are valid NUL-terminated strings, and the backup
+ // handle cannot outlive this function's returned owner.
+ let handle = unsafe {
+ ffi::sqlite3_backup_init(
+ destination.as_ptr(),
+ c"main".as_ptr(),
+ source.as_ptr(),
+ c"main".as_ptr(),
+ )
+ };
+ let handle = NonNull::new(handle).ok_or_else(|| {
+ // SAFETY: the destination SQLx guard keeps this handle live and
+ // exclusively locked for the duration of the call.
+ let code = unsafe { ffi::sqlite3_errcode(destination.as_ptr()) };
+ NativeBackupError {
+ kind: NativeBackupFailureKind::Initialize,
+ code,
+ }
+ })?;
+ Ok(Self {
+ handle: Some(handle),
+ _locked_handles: PhantomData,
+ })
+ }
+
+ pub(crate) fn step(&mut self, pages: i32) -> Result<NativeBackupStep, NativeBackupError> {
+ if pages <= 0 {
+ return Err(NativeBackupError {
+ kind: NativeBackupFailureKind::Step,
+ code: ffi::SQLITE_MISUSE,
+ });
+ }
+ let Some(handle) = self.handle else {
+ return Err(NativeBackupError {
+ kind: NativeBackupFailureKind::Step,
+ code: ffi::SQLITE_MISUSE,
+ });
+ };
+ // SAFETY: `handle` remains owned by this adapter and `pages` is a
+ // positive bounded batch supplied by the capture driver.
+ classify_step(unsafe { ffi::sqlite3_backup_step(handle.as_ptr(), pages) })
+ }
+
+ pub(crate) fn finish(mut self) -> Result<(), NativeBackupError> {
+ let code = self.finish_once();
+ if code == ffi::SQLITE_OK {
+ Ok(())
+ } else {
+ Err(NativeBackupError {
+ kind: NativeBackupFailureKind::Finish,
+ code,
+ })
+ }
+ }
+
+ fn finish_once(&mut self) -> i32 {
+ self.handle.map_or(ffi::SQLITE_OK, |handle| {
+ self.handle = None;
+ // SAFETY: taking the handle ensures exactly one finalization call.
+ unsafe { ffi::sqlite3_backup_finish(handle.as_ptr()) }
+ })
+ }
+}
+
+impl Drop for NativeBackup<'_, '_> {
+ fn drop(&mut self) {
+ let _ = self.finish_once();
+ }
+}
+
+fn classify_step(code: i32) -> Result<NativeBackupStep, NativeBackupError> {
+ match code {
+ ffi::SQLITE_DONE => Ok(NativeBackupStep::Done),
+ ffi::SQLITE_OK => Ok(NativeBackupStep::More),
+ ffi::SQLITE_BUSY => Ok(NativeBackupStep::Busy),
+ ffi::SQLITE_LOCKED => Ok(NativeBackupStep::Locked),
+ code => Err(NativeBackupError {
+ kind: NativeBackupFailureKind::Step,
+ code,
+ }),
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn step_codes_are_closed_and_stable() {
+ assert_eq!(classify_step(ffi::SQLITE_DONE), Ok(NativeBackupStep::Done));
+ assert_eq!(classify_step(ffi::SQLITE_OK), Ok(NativeBackupStep::More));
+ assert_eq!(classify_step(ffi::SQLITE_BUSY), Ok(NativeBackupStep::Busy));
+ assert_eq!(
+ classify_step(ffi::SQLITE_LOCKED),
+ Ok(NativeBackupStep::Locked)
+ );
+ assert_eq!(
+ classify_step(ffi::SQLITE_CORRUPT)
+ .expect_err("unexpected native code must fail")
+ .kind,
+ NativeBackupFailureKind::Step
+ );
+ }
+}
diff --git a/crates/service_sqlite/src/status/disk.rs b/crates/service_sqlite/src/status/disk.rs
@@ -5,7 +5,9 @@ use std::error::Error;
use serde::{Deserialize, Deserializer, Serialize, de::Error as _};
-use crate::{ServiceSqliteErrorKind, ServiceSqlitePaths};
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+use crate::ServiceSqliteErrorKind;
+use crate::ServiceSqlitePaths;
const MAXIMUM_MINIMUM_FREE_BYTES: u64 = i64::MAX as u64;
diff --git a/crates/service_sqlite/tests/package_boundary.rs b/crates/service_sqlite/tests/package_boundary.rs
@@ -30,6 +30,7 @@ const RESTORE_RECOVER_SOURCE: &str = include_str!("../src/restore/recover.rs");
const RESTORE_ROOT_SOURCE: &str = include_str!("../src/restore/mod.rs");
const RESTORE_PROCESS_TEST_SOURCE: &str = include_str!("../src/restore/process_tests.rs");
const RESTORE_STAGE_SOURCE: &str = include_str!("../src/restore/stage.rs");
+const SQLITE_NATIVE_BACKUP_SOURCE: &str = include_str!("../src/sqlite_native_backup.rs");
const STATUS_SOURCE: &str = include_str!("../src/status/mod.rs");
const DISK_SOURCE: &str = include_str!("../src/status/disk.rs");
const TRANSACTION_CONTROL_SOURCE: &str = include_str!("../src/transaction_control.rs");
@@ -55,6 +56,7 @@ fn service_sqlite_is_unpublished_lint_governed_and_dependency_bounded() {
BTreeSet::from([
"fs2",
"futures",
+ "libsqlite3-sys",
"radroots_runtime_paths",
"radroots_storage",
"rusqlite",
@@ -126,6 +128,7 @@ fn service_sqlite_is_unpublished_lint_governed_and_dependency_bounded() {
"native_metadata",
"open",
"restore",
+ "sqlite_native_backup",
"status",
"transaction_control"
])
@@ -925,7 +928,8 @@ fn service_sqlite_is_unpublished_lint_governed_and_dependency_bounded() {
}
for required in [
- "rusqlite::backup::Backup::new",
+ "NativeBackup::start",
+ "lock_handle()",
"tokio::task::spawn_blocking",
"BACKUP_PAGES_PER_STEP",
"HASH_BUFFER_BYTES",
@@ -951,6 +955,24 @@ fn service_sqlite_is_unpublished_lint_governed_and_dependency_bounded() {
"Step 064 backup capture source is missing `{required}`"
);
}
+ for required in [
+ "use libsqlite3_sys as ffi;",
+ "LockedSqliteHandle",
+ "ffi::sqlite3_backup_init",
+ "ffi::sqlite3_backup_step",
+ "ffi::sqlite3_backup_finish",
+ ] {
+ assert!(
+ SQLITE_NATIVE_BACKUP_SOURCE.contains(required),
+ "sealed native backup adapter is missing `{required}`"
+ );
+ }
+ for forbidden in ["pub ", "SqliteConnection", "PoolConnection", "Path", "File"] {
+ assert!(
+ !SQLITE_NATIVE_BACKUP_SOURCE.contains(forbidden),
+ "sealed native backup adapter exposes or owns forbidden authority `{forbidden}`"
+ );
+ }
for forbidden in [
"pub use rusqlite",
"pub fn restore",
diff --git a/tools/xtask/src/architecture.rs b/tools/xtask/src/architecture.rs
@@ -242,14 +242,14 @@ fn validate_workspace_toolchain(
));
}
let lints = &manifest.workspace.lints;
- if lints.rust.unsafe_code != "forbid"
+ if lints.rust.unsafe_code != "deny"
|| lints.rustdoc.broken_intra_doc_links != "deny"
|| lints.clippy.dbg_macro != "deny"
|| lints.clippy.todo != "deny"
|| lints.clippy.unimplemented != "deny"
{
return Err(
- "workspace lints must forbid unsafe code and deny the approved rustdoc/Clippy baseline"
+ "workspace lints must deny unsafe code and deny the approved rustdoc/Clippy baseline"
.to_owned(),
);
}
@@ -1289,7 +1289,7 @@ adr_required = false
fn complete_workspace_manifest(members: &str) -> String {
format!(
- "[workspace]\nmembers = [{members}]\nresolver = \"3\"\n\n[workspace.package]\nversion = \"0.1.0\"\nedition = \"2024\"\nrust-version = \"1.97.1\"\nlicense = \"MIT OR Apache-2.0\"\nrepository = \"https://github.com/radrootslabs/sdk\"\nhomepage = \"https://radroots.org\"\nreadme = \"README\"\nauthors = [\"Tyson Lupul <tyson@radroots.org>\"]\n\n[workspace.metadata.radroots.public-package]\nversion = \"0.1.0\"\nauthors = [\"Tyson Lupul <tyson@radroots.org>\"]\nreadme = \"README.md\"\n\n[workspace.lints.rust]\nunsafe_code = \"forbid\"\n\n[workspace.lints.rustdoc]\nbroken_intra_doc_links = \"deny\"\n\n[workspace.lints.clippy]\ndbg_macro = \"deny\"\ntodo = \"deny\"\nunimplemented = \"deny\"\n"
+ "[workspace]\nmembers = [{members}]\nresolver = \"3\"\n\n[workspace.package]\nversion = \"0.1.0\"\nedition = \"2024\"\nrust-version = \"1.97.1\"\nlicense = \"MIT OR Apache-2.0\"\nrepository = \"https://github.com/radrootslabs/sdk\"\nhomepage = \"https://radroots.org\"\nreadme = \"README\"\nauthors = [\"Tyson Lupul <tyson@radroots.org>\"]\n\n[workspace.metadata.radroots.public-package]\nversion = \"0.1.0\"\nauthors = [\"Tyson Lupul <tyson@radroots.org>\"]\nreadme = \"README.md\"\n\n[workspace.lints.rust]\nunsafe_code = \"deny\"\n\n[workspace.lints.rustdoc]\nbroken_intra_doc_links = \"deny\"\n\n[workspace.lints.clippy]\ndbg_macro = \"deny\"\ntodo = \"deny\"\nunimplemented = \"deny\"\n"
)
}
diff --git a/tools/xtask/src/contract.rs b/tools/xtask/src/contract.rs
@@ -33,6 +33,7 @@ use sha2::{Digest, Sha256};
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::{Path, PathBuf};
+use walkdir::WalkDir;
pub(crate) fn validate_artifact_contracts(workspace_root: &Path) -> Result<(), String> {
validate_event_contract_registry_v7_inventory(workspace_root)?;
@@ -4184,7 +4185,7 @@ fn validate_sqlite_runtime_contract(workspace_root: &Path) -> Result<(), String>
|| contract.sealed_native_adapter.relative_module
!= "crates/service_sqlite/src/sqlite_native_backup.rs"
|| contract.sealed_native_adapter.capability != "incremental_online_backup"
- || contract.sealed_native_adapter.status != "planned"
+ || contract.sealed_native_adapter.status != "active"
|| contract.migration.owner != "rcld-rshr-045"
|| contract.migration.status != "in_progress"
|| contract.migration.temporary_direct_dependencies != TEMPORARY_DIRECT_DEPENDENCIES
@@ -4267,11 +4268,16 @@ fn validate_sqlite_runtime_contract(workspace_root: &Path) -> Result<(), String>
&workspace.workspace.members,
PACKAGE_NAME,
)?;
- if !direct_native_dependencies.is_empty() {
+ let expected_direct_native_dependencies = BTreeSet::from([
+ "radroots_service_sqlite:libsqlite3-sys".to_owned(),
+ "workspace:libsqlite3-sys".to_owned(),
+ ]);
+ if direct_native_dependencies != expected_direct_native_dependencies {
return Err(format!(
- "direct {PACKAGE_NAME} access is forbidden while the sealed native adapter is planned: {direct_native_dependencies:?}"
+ "direct {PACKAGE_NAME} access must be limited to the sealed adapter owner: expected {expected_direct_native_dependencies:?}, found {direct_native_dependencies:?}"
));
}
+ validate_sqlite_native_adapter_source(workspace_root, &contract.sealed_native_adapter)?;
let storage_sqlite =
fs::read_to_string(workspace_root.join("crates/storage_sqlite/Cargo.toml"))
@@ -4298,6 +4304,61 @@ fn validate_sqlite_runtime_contract(workspace_root: &Path) -> Result<(), String>
Ok(())
}
+fn validate_sqlite_native_adapter_source(
+ workspace_root: &Path,
+ adapter: &SqliteRuntimeSealedNativeAdapter,
+) -> Result<(), String> {
+ let adapter_path = workspace_root.join(&adapter.relative_module);
+ let adapter_source = fs::read_to_string(&adapter_path)
+ .map_err(|error| format!("read {}: {error}", adapter_path.display()))?;
+ for required in [
+ "use libsqlite3_sys as ffi;",
+ "LockedSqliteHandle",
+ "ffi::sqlite3_backup_init",
+ "ffi::sqlite3_backup_step",
+ "ffi::sqlite3_backup_finish",
+ ] {
+ if !adapter_source.contains(required) {
+ return Err(format!(
+ "{} must contain the sealed native adapter boundary `{required}`",
+ adapter_path.display()
+ ));
+ }
+ }
+
+ let source_root = workspace_root.join("crates/service_sqlite/src");
+ for entry in WalkDir::new(&source_root).follow_links(false) {
+ let entry = entry.map_err(|error| format!("walk {}: {error}", source_root.display()))?;
+ if !entry.file_type().is_file()
+ || entry
+ .path()
+ .extension()
+ .and_then(|extension| extension.to_str())
+ != Some("rs")
+ || entry.path() == adapter_path
+ {
+ continue;
+ }
+ let source = fs::read_to_string(entry.path())
+ .map_err(|error| format!("read {}: {error}", entry.path().display()))?;
+ for forbidden in [
+ "libsqlite3_sys",
+ "sqlite3_backup_",
+ "unsafe {",
+ "unsafe fn ",
+ "unsafe impl ",
+ ] {
+ if source.contains(forbidden) {
+ return Err(format!(
+ "{} contains native SQLite or unsafe authority outside the sealed adapter: `{forbidden}`",
+ entry.path().display()
+ ));
+ }
+ }
+ }
+ Ok(())
+}
+
fn sqlite_forbidden_direct_dependencies(
workspace_root: &Path,
members: &[String],