lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

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:
MCargo.lock | 1+
MCargo.toml | 3++-
Mcontracts/releases/sqlite_runtime.toml | 2+-
Mcrates/service_sqlite/Cargo.toml | 1+
Mcrates/service_sqlite/src/backup/capture.rs | 143++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Mcrates/service_sqlite/src/initialize.rs | 8+++++---
Mcrates/service_sqlite/src/integrity/catalog.rs | 4++--
Mcrates/service_sqlite/src/lib.rs | 9++++++++-
Acrates/service_sqlite/src/sqlite_native_backup.rs | 168+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/service_sqlite/src/status/disk.rs | 4+++-
Mcrates/service_sqlite/tests/package_boundary.rs | 24+++++++++++++++++++++++-
Mtools/xtask/src/architecture.rs | 6+++---
Mtools/xtask/src/contract.rs | 67++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
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],