commit a5985c1a5b2b063f063c42c7f72a92669cdf5d79
parent 8332b86a7d3a9cbc2210038a1e9a21fca6f73312
Author: triesap <tyson@radroots.org>
Date: Tue, 11 Aug 2026 12:42:03 +0000
service-sqlite: bind database identity
- bind canonical service, instance, generation, schema, creation, and application metadata
- verify immutable identity during initialization, pool opening, and every checkout
- permit only monotonic supported schema advancement for the next migration checkpoints
- cover native behavior and warning-free Windows and Android compile boundaries
Diffstat:
8 files changed, 1212 insertions(+), 96 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -3911,6 +3911,7 @@ version = "0.1.0-alpha"
dependencies = [
"fs2",
"radroots_runtime_paths",
+ "radroots_storage",
"rustix 1.1.4",
"serde",
"serde_json",
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 }
radroots_runtime_paths = { workspace = true }
+radroots_storage = { workspace = true }
rustix = { workspace = true }
serde = { workspace = true, features = ["derive", "std"] }
sqlx = { workspace = true, features = ["runtime-tokio", "sqlite-bundled"] }
diff --git a/crates/service_sqlite/README.md b/crates/service_sqlite/README.md
@@ -3,7 +3,8 @@
`radroots_service_sqlite` is the unpublished, native SQLite-mechanism crate for
Radroots services. It provides narrow, reusable building blocks for exclusive
writer authority, instance locking, versioned schema mechanics, bounded
-transactions, integrity checks, backup, restore, and passive storage status.
+transactions, immutable service-instance database identity, integrity checks,
+backup, restore, and passive storage status.
The crate owns mechanics only. Service-specific tables, SQL, repositories,
backup content policy, identity material, process lifecycle, and readiness
diff --git a/crates/service_sqlite/src/initialize.rs b/crates/service_sqlite/src/initialize.rs
@@ -4,18 +4,22 @@ use core::{fmt, future::Future};
use std::{error::Error, path::PathBuf};
use crate::{
- OpenMode, ServiceSqliteError, ServiceSqliteErrorKind, ServiceSqlitePaths, WriterAuthority,
+ OpenMode, ServiceDatabaseMetadata, ServiceSqliteError, ServiceSqliteErrorKind,
+ ServiceSqlitePaths, WriterAuthority,
};
/// Creates and initializes a missing service database while holding sole writer authority.
///
/// The callback receives the already-reserved canonical database path. It must
/// open that file without create or replacement flags, initialize its schema,
-/// close every database handle, and only then resolve its future. Cancellation
-/// or failure removes only the exact inode reserved by this call.
+/// close every database handle, and only then resolve its future. The supplied
+/// metadata must derive from the same paths; it is written and verified after
+/// the callback but before the database becomes durable. Cancellation or
+/// failure removes only the exact inode reserved by this call.
pub async fn initialize_database<F, Fut, E>(
paths: &ServiceSqlitePaths,
mode: OpenMode,
+ metadata: &ServiceDatabaseMetadata,
initialize_schema: F,
) -> Result<WriterAuthority, ServiceSqliteError>
where
@@ -28,6 +32,9 @@ where
InitializationFailureKind::UnsupportedMode,
)));
}
+ if !metadata.matches_paths(paths) {
+ return Err(ServiceSqliteError::new(ServiceSqliteErrorKind::Metadata));
+ }
let authority = WriterAuthority::acquire(paths, mode)?.ok_or_else(|| {
initialization_error(InitializationCause::new(
@@ -40,6 +47,7 @@ where
initialize_with_ops(
paths,
authority,
+ metadata,
initialize_schema,
&SystemInitializationOperations,
)
@@ -48,7 +56,7 @@ where
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
- drop((authority, initialize_schema));
+ drop((authority, metadata, initialize_schema));
Err(initialization_error(InitializationCause::new(
InitializationFailureKind::CreateUnavailable,
)))
@@ -414,9 +422,23 @@ mod supported {
}
}
+ async fn fail_metadata_with_rollback<O: InitializationOperations>(
+ mut pending: PendingDatabase<'_, O>,
+ primary: ServiceSqliteError,
+ ) -> Result<WriterAuthority, ServiceSqliteError> {
+ match pending.rollback() {
+ Ok(()) => Err(primary),
+ Err(cleanup) => Err(initialization_error(InitializationCause::with_source(
+ cleanup.kind,
+ primary,
+ ))),
+ }
+ }
+
pub(super) async fn initialize_with_ops<F, Fut, E, O>(
paths: &ServiceSqlitePaths,
authority: WriterAuthority,
+ metadata: &ServiceDatabaseMetadata,
initialize_schema: F,
operations: &O,
) -> Result<WriterAuthority, ServiceSqliteError>
@@ -451,6 +473,41 @@ mod supported {
if let Err(error) = pending.validate_canonical_path(paths.state_database()) {
return fail_with_rollback(pending, error).await;
}
+ let metadata_result = async {
+ use sqlx::{ConnectOptions, Connection, sqlite::SqliteConnectOptions};
+
+ let options = SqliteConnectOptions::new()
+ .filename(paths.state_database())
+ .create_if_missing(false)
+ .disable_statement_logging();
+ let mut connection = sqlx::SqliteConnection::connect_with(&options)
+ .await
+ .map_err(|source| {
+ ServiceSqliteError::with_source(ServiceSqliteErrorKind::Metadata, source)
+ })?;
+ let write_result =
+ crate::metadata::write_database_metadata(&mut connection, metadata).await;
+ let close_result = connection.close().await.map_err(|source| {
+ ServiceSqliteError::with_source(ServiceSqliteErrorKind::Metadata, source)
+ });
+ write_result.and(close_result)
+ }
+ .await;
+ if let Err(error) = pending.validate() {
+ return fail_with_rollback(pending, error).await;
+ }
+ if let Err(error) = pending.validate_canonical_path(paths.state_database()) {
+ return fail_with_rollback(pending, error).await;
+ }
+ if let Err(error) = metadata_result {
+ return fail_metadata_with_rollback(pending, error).await;
+ }
+ if let Err(error) = pending.validate() {
+ return fail_with_rollback(pending, error).await;
+ }
+ if let Err(error) = pending.validate_canonical_path(paths.state_database()) {
+ return fail_with_rollback(pending, error).await;
+ }
if let Err(error) = pending.commit(paths.state_database()) {
return fail_with_rollback(pending, error).await;
}
@@ -462,9 +519,10 @@ mod supported {
mod tests {
use std::{
cell::{Cell, RefCell},
- fs::{self, OpenOptions},
+ fs,
future::{Future, pending, ready},
- io::{self, Write},
+ io,
+ num::NonZeroU32,
os::unix::fs::{MetadataExt, PermissionsExt, symlink},
path::Path,
pin::Pin,
@@ -476,6 +534,7 @@ mod supported {
RadrootsPlatform, RuntimeContext, RuntimeContextBootstrap, RuntimeContextSource,
ServiceId,
};
+ use radroots_storage::event::SourceGeneration;
use super::*;
@@ -542,14 +601,6 @@ mod supported {
(result, future)
}
- fn run_ready<F: Future>(future: F) -> F::Output {
- let (result, _) = poll_once(future);
- match result {
- Poll::Ready(output) => output,
- Poll::Pending => panic!("test future must be immediately ready"),
- }
- }
-
fn paths(root: &Path, instance: &str) -> ServiceSqlitePaths {
let context = RuntimeContext::resolve(
&RadrootsPathResolver::new(
@@ -575,28 +626,46 @@ mod supported {
.expect("create state directory");
}
- #[test]
- fn successful_initialization_is_create_new_mode_0600_and_retains_authority() {
+ fn metadata(paths: &ServiceSqlitePaths) -> ServiceDatabaseMetadata {
+ ServiceDatabaseMetadata::new(
+ paths,
+ SourceGeneration::new([7; 32]).expect("source generation"),
+ NonZeroU32::new(1).expect("schema version"),
+ 1_700_000_000_000,
+ crate::ServiceSqliteApplicationId::new(0x5244_5351).expect("application ID"),
+ )
+ .expect("database metadata")
+ }
+
+ #[tokio::test(flavor = "current_thread")]
+ async fn successful_initialization_is_create_new_mode_0600_and_retains_authority() {
let root = tempfile::tempdir().expect("root");
let paths = paths(root.path(), "success");
prepare(&paths);
let expected_path = paths.state_database().to_path_buf();
- let mut authority = run_ready(initialize_database(
- &paths,
- OpenMode::Initialize,
- |path| async move {
+ let metadata = metadata(&paths);
+ let mut authority =
+ initialize_database(&paths, OpenMode::Initialize, &metadata, |path| async move {
assert_eq!(path, expected_path);
- let mut file = OpenOptions::new().write(true).open(path)?;
- file.write_all(b"schema-v1")?;
- Ok::<(), io::Error>(())
- },
- ))
- .expect("initialize");
-
- assert_eq!(fs::read(paths.state_database()).unwrap(), b"schema-v1");
- let metadata = fs::metadata(paths.state_database()).unwrap();
- assert_eq!(metadata.permissions().mode() & 0o777, 0o600);
- assert_eq!(metadata.nlink(), 1);
+ use sqlx::{ConnectOptions, Connection, sqlite::SqliteConnectOptions};
+
+ let options = SqliteConnectOptions::new()
+ .filename(path)
+ .create_if_missing(false)
+ .disable_statement_logging();
+ let mut connection = sqlx::SqliteConnection::connect_with(&options).await?;
+ sqlx::query("CREATE TABLE service_schema (id INTEGER PRIMARY KEY)")
+ .execute(&mut connection)
+ .await?;
+ connection.close().await?;
+ Ok::<(), sqlx::Error>(())
+ })
+ .await
+ .expect("initialize");
+
+ let filesystem_metadata = fs::metadata(paths.state_database()).unwrap();
+ assert_eq!(filesystem_metadata.permissions().mode() & 0o777, 0o600);
+ assert_eq!(filesystem_metadata.nlink(), 1);
assert!(authority.is_held());
assert!(WriterAuthority::acquire(&paths, OpenMode::ReadWriteExisting).is_err());
authority.release().expect("release");
@@ -607,8 +676,8 @@ mod supported {
);
}
- #[test]
- fn existing_regular_symlink_directory_and_hardlink_are_never_mutated() {
+ #[tokio::test(flavor = "current_thread")]
+ async fn existing_regular_symlink_directory_and_hardlink_are_never_mutated() {
for shape in ["regular", "symlink", "directory", "hardlink"] {
let root = tempfile::tempdir().expect("root");
let paths = paths(root.path(), shape);
@@ -623,10 +692,12 @@ mod supported {
_ => unreachable!(),
}
let called = Cell::new(false);
- let error = run_ready(initialize_database(&paths, OpenMode::Initialize, |_| {
+ let metadata = metadata(&paths);
+ let error = initialize_database(&paths, OpenMode::Initialize, &metadata, |_| {
called.set(true);
ready(Ok::<(), CallbackFailure>(()))
- }))
+ })
+ .await
.expect_err("existing state must fail");
assert_eq!(error.kind(), ServiceSqliteErrorKind::Create);
assert!(!called.get());
@@ -634,16 +705,18 @@ mod supported {
}
}
- #[test]
- fn non_initialize_modes_have_zero_callback_and_filesystem_effects() {
+ #[tokio::test(flavor = "current_thread")]
+ async fn non_initialize_modes_have_zero_callback_and_filesystem_effects() {
for mode in [OpenMode::ReadWriteExisting, OpenMode::ReadOnlyInspection] {
let root = tempfile::tempdir().expect("root");
let paths = paths(root.path(), "wrong-mode");
let called = Cell::new(false);
- let error = run_ready(initialize_database(&paths, mode, |_| {
+ let metadata = metadata(&paths);
+ let error = initialize_database(&paths, mode, &metadata, |_| {
called.set(true);
ready(Ok::<(), CallbackFailure>(()))
- }))
+ })
+ .await
.expect_err("mode must reject");
assert_eq!(error.kind(), ServiceSqliteErrorKind::Create);
assert!(!called.get());
@@ -653,14 +726,16 @@ mod supported {
}
}
- #[test]
- fn callback_failure_cleans_up_releases_authority_and_preserves_trusted_cause() {
+ #[tokio::test(flavor = "current_thread")]
+ async fn callback_failure_cleans_up_releases_authority_and_preserves_trusted_cause() {
let root = tempfile::tempdir().expect("root");
let paths = paths(root.path(), "callback-failure");
prepare(&paths);
- let error = run_ready(initialize_database(&paths, OpenMode::Initialize, |_| {
+ let metadata = metadata(&paths);
+ let error = initialize_database(&paths, OpenMode::Initialize, &metadata, |_| {
ready(Err::<(), _>(CallbackFailure))
- }))
+ })
+ .await
.expect_err("callback failure");
assert_eq!(error.kind(), ServiceSqliteErrorKind::Create);
assert!(!paths.state_database().exists());
@@ -681,23 +756,60 @@ mod supported {
Some("secret callback path=/private/state.sqlite")
);
- let retry = run_ready(initialize_database(&paths, OpenMode::Initialize, |_| {
+ let retry = initialize_database(&paths, OpenMode::Initialize, &metadata, |_| {
ready(Ok::<(), CallbackFailure>(()))
- }))
+ })
+ .await
.expect("retry after cleanup");
assert!(retry.is_held());
assert!(paths.state_database().exists());
}
- #[test]
- fn cancellation_rolls_back_and_releases_authority() {
+ #[tokio::test(flavor = "current_thread")]
+ async fn metadata_write_failure_cleans_the_exact_reserved_database() {
+ let root = tempfile::tempdir().expect("root");
+ let paths = paths(root.path(), "metadata-failure");
+ prepare(&paths);
+ let metadata = metadata(&paths);
+ let error =
+ initialize_database(&paths, OpenMode::Initialize, &metadata, |path| async move {
+ use sqlx::{ConnectOptions, Connection, sqlite::SqliteConnectOptions};
+
+ let options = SqliteConnectOptions::new()
+ .filename(path)
+ .create_if_missing(false)
+ .disable_statement_logging();
+ let mut connection = sqlx::SqliteConnection::connect_with(&options).await?;
+ sqlx::query("CREATE TABLE radroots_service_metadata (value TEXT)")
+ .execute(&mut connection)
+ .await?;
+ connection.close().await?;
+ Ok::<(), sqlx::Error>(())
+ })
+ .await
+ .expect_err("conflicting metadata must fail");
+
+ assert_eq!(error.kind(), ServiceSqliteErrorKind::Metadata);
+ assert!(!paths.state_database().exists());
+ assert!(
+ WriterAuthority::acquire(&paths, OpenMode::Initialize)
+ .expect("reacquire")
+ .is_some()
+ );
+ }
+
+ #[tokio::test(flavor = "current_thread")]
+ async fn cancellation_rolls_back_and_releases_authority() {
let root = tempfile::tempdir().expect("root");
let paths = paths(root.path(), "cancelled");
prepare(&paths);
- let (poll, future) =
- poll_once(initialize_database(&paths, OpenMode::Initialize, |_| {
- pending::<Result<(), CallbackFailure>>()
- }));
+ let metadata = metadata(&paths);
+ let (poll, future) = poll_once(initialize_database(
+ &paths,
+ OpenMode::Initialize,
+ &metadata,
+ |_| pending::<Result<(), CallbackFailure>>(),
+ ));
assert!(poll.is_pending());
assert!(paths.state_database().exists());
assert!(WriterAuthority::acquire(&paths, OpenMode::Initialize).is_err());
@@ -710,39 +822,44 @@ mod supported {
);
}
- #[test]
- fn replacement_is_detected_and_never_deleted() {
+ #[tokio::test(flavor = "current_thread")]
+ async fn replacement_is_detected_and_never_deleted() {
let root = tempfile::tempdir().expect("root");
let paths = paths(root.path(), "replacement");
prepare(&paths);
+ let metadata = metadata(&paths);
let replacement_path = paths.state_database().to_path_buf();
- let error = run_ready(initialize_database(
+ let error = initialize_database(
&paths,
OpenMode::Initialize,
+ &metadata,
move |path| async move {
fs::remove_file(&path)?;
fs::write(&replacement_path, b"replacement")?;
Ok::<(), io::Error>(())
},
- ))
+ )
+ .await
.expect_err("replacement must fail");
assert_eq!(error.kind(), ServiceSqliteErrorKind::Create);
assert_eq!(fs::read(paths.state_database()).unwrap(), b"replacement");
}
- #[test]
- fn parent_directory_replacement_cannot_rebind_the_callback_path() {
+ #[tokio::test(flavor = "current_thread")]
+ async fn parent_directory_replacement_cannot_rebind_the_callback_path() {
let root = tempfile::tempdir().expect("root");
let paths = paths(root.path(), "parent-replacement");
prepare(&paths);
+ let metadata = metadata(&paths);
let state_directory = paths.state_database().parent().unwrap().to_path_buf();
let displaced_directory = state_directory.with_file_name("parent-replacement-old");
let displaced_for_callback = displaced_directory.clone();
let replacement_path = paths.state_database().to_path_buf();
- let error = run_ready(initialize_database(
+ let error = initialize_database(
&paths,
OpenMode::Initialize,
+ &metadata,
move |_| async move {
fs::rename(&state_directory, &displaced_for_callback)?;
fs::create_dir(&state_directory)?;
@@ -750,7 +867,8 @@ mod supported {
fs::set_permissions(&replacement_path, fs::Permissions::from_mode(0o600))?;
Ok::<(), io::Error>(())
},
- ))
+ )
+ .await
.expect_err("canonical path replacement must fail");
assert_eq!(error.kind(), ServiceSqliteErrorKind::Create);
@@ -758,8 +876,8 @@ mod supported {
assert!(!displaced_directory.join("state.sqlite").exists());
}
- #[test]
- fn injected_sync_and_cleanup_failures_preserve_exact_ordering() {
+ #[tokio::test(flavor = "current_thread")]
+ async fn injected_sync_and_cleanup_failures_preserve_exact_ordering() {
let scenarios = [
"reservation-sync",
"database-sync",
@@ -770,6 +888,7 @@ mod supported {
let root = tempfile::tempdir().expect("root");
let paths = paths(root.path(), scenario);
prepare(&paths);
+ let metadata = metadata(&paths);
let authority = WriterAuthority::acquire(&paths, OpenMode::Initialize)
.unwrap()
.unwrap();
@@ -786,19 +905,23 @@ mod supported {
_ => unreachable!(),
}
let result = if scenario == "cleanup" {
- run_ready(initialize_with_ops(
+ initialize_with_ops(
&paths,
authority,
+ &metadata,
|_| ready(Err::<(), _>(CallbackFailure)),
&operations,
- ))
+ )
+ .await
} else {
- run_ready(initialize_with_ops(
+ initialize_with_ops(
&paths,
authority,
+ &metadata,
|_| ready(Ok::<(), CallbackFailure>(())),
&operations,
- ))
+ )
+ .await
};
assert_eq!(
result.expect_err("injected failure").kind(),
diff --git a/crates/service_sqlite/src/lib.rs b/crates/service_sqlite/src/lib.rs
@@ -6,6 +6,7 @@ mod authority;
mod config;
mod error;
mod initialize;
+mod metadata;
mod open;
mod status;
@@ -15,5 +16,9 @@ pub use error::{
SafeServiceSqliteError, ServiceSqliteError, ServiceSqliteErrorCode, ServiceSqliteErrorKind,
};
pub use initialize::initialize_database;
+pub use metadata::{
+ ServiceDatabaseIdentity, ServiceDatabaseMetadata, ServiceSqliteApplicationId,
+ ServiceSqliteMetadataValueError,
+};
pub use open::{OpenMode, ServiceSqlitePathError, ServiceSqlitePaths};
pub use status::{StorageHealth, StorageIntegrity, StorageStatus};
diff --git a/crates/service_sqlite/src/metadata.rs b/crates/service_sqlite/src/metadata.rs
@@ -0,0 +1,796 @@
+//! Immutable database identity metadata for one service instance.
+
+use core::{fmt, num::NonZeroU32};
+use std::error::Error;
+
+use radroots_runtime_paths::{InstanceId, ServiceId};
+use radroots_storage::event::SourceGeneration;
+
+use crate::ServiceSqlitePaths;
+
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+use crate::{ServiceSqliteError, ServiceSqliteErrorKind};
+
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+use sqlx::{Connection, Row, SqliteConnection};
+
+const MAX_APPLICATION_ID: u32 = i32::MAX as u32;
+const MAX_CREATED_AT_UNIX_MS: u64 = i64::MAX as u64;
+
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+const CREATE_METADATA_SQL: &str = r#"
+CREATE TABLE radroots_service_metadata (
+ singleton INTEGER NOT NULL PRIMARY KEY CHECK (singleton = 1),
+ service_id TEXT NOT NULL,
+ instance_id TEXT NOT NULL,
+ source_generation BLOB NOT NULL CHECK (length(source_generation) = 32),
+ state_schema_version INTEGER NOT NULL
+ CHECK (state_schema_version BETWEEN 1 AND 4294967295),
+ created_at_unix_ms INTEGER NOT NULL CHECK (created_at_unix_ms > 0)
+) STRICT;
+CREATE TRIGGER radroots_service_metadata_guard_update
+BEFORE UPDATE ON radroots_service_metadata
+WHEN NEW.singleton != OLD.singleton
+ OR NEW.service_id != OLD.service_id
+ OR NEW.instance_id != OLD.instance_id
+ OR NEW.source_generation != OLD.source_generation
+ OR NEW.created_at_unix_ms != OLD.created_at_unix_ms
+ OR NEW.state_schema_version <= OLD.state_schema_version
+BEGIN
+ SELECT RAISE(ABORT, 'service metadata identity is immutable');
+END;
+CREATE TRIGGER radroots_service_metadata_no_delete
+BEFORE DELETE ON radroots_service_metadata
+BEGIN
+ SELECT RAISE(ABORT, 'service metadata is immutable');
+END;
+"#;
+
+/// A validated nonzero SQLite application identifier.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct ServiceSqliteApplicationId(u32);
+
+impl ServiceSqliteApplicationId {
+ /// Validates a caller-owned application identifier for SQLite's signed range.
+ pub const fn new(value: u32) -> Result<Self, ServiceSqliteMetadataValueError> {
+ if value == 0 || value > MAX_APPLICATION_ID {
+ return Err(ServiceSqliteMetadataValueError::InvalidApplicationId);
+ }
+ Ok(Self(value))
+ }
+
+ /// Returns the validated application identifier.
+ #[must_use]
+ pub const fn get(self) -> u32 {
+ self.0
+ }
+}
+
+/// Invalid caller-supplied database metadata.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum ServiceSqliteMetadataValueError {
+ InvalidApplicationId,
+ InvalidCreationTime,
+}
+
+impl fmt::Display for ServiceSqliteMetadataValueError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str(match self {
+ Self::InvalidApplicationId => "SQLite application ID is out of range",
+ Self::InvalidCreationTime => "SQLite creation time is out of range",
+ })
+ }
+}
+
+impl Error for ServiceSqliteMetadataValueError {}
+
+/// Exact immutable identity expected from one service database.
+#[derive(Clone, PartialEq, Eq)]
+pub struct ServiceDatabaseMetadata {
+ service: ServiceId,
+ instance: InstanceId,
+ source_generation: SourceGeneration,
+ state_schema_version: NonZeroU32,
+ created_at_unix_ms: u64,
+ application_id: ServiceSqliteApplicationId,
+}
+
+/// Exact mount identity and maximum schema version accepted by one service binary.
+#[derive(Clone, PartialEq, Eq)]
+pub struct ServiceDatabaseIdentity {
+ service: ServiceId,
+ instance: InstanceId,
+ source_generation: SourceGeneration,
+ supported_state_schema_version: NonZeroU32,
+ application_id: ServiceSqliteApplicationId,
+}
+
+impl ServiceDatabaseMetadata {
+ /// Constructs metadata bound to the service and instance in canonical paths.
+ pub fn new(
+ paths: &ServiceSqlitePaths,
+ source_generation: SourceGeneration,
+ state_schema_version: NonZeroU32,
+ created_at_unix_ms: u64,
+ application_id: ServiceSqliteApplicationId,
+ ) -> Result<Self, ServiceSqliteMetadataValueError> {
+ if created_at_unix_ms == 0 || created_at_unix_ms > MAX_CREATED_AT_UNIX_MS {
+ return Err(ServiceSqliteMetadataValueError::InvalidCreationTime);
+ }
+ Ok(Self {
+ service: paths.service().clone(),
+ instance: paths.instance().clone(),
+ source_generation,
+ state_schema_version,
+ created_at_unix_ms,
+ application_id,
+ })
+ }
+
+ /// Returns the bound service identity.
+ #[must_use]
+ pub fn service(&self) -> &ServiceId {
+ &self.service
+ }
+
+ /// Returns the bound instance identity.
+ #[must_use]
+ pub fn instance(&self) -> &InstanceId {
+ &self.instance
+ }
+
+ /// Returns the opaque nonzero source generation.
+ #[must_use]
+ pub const fn source_generation(&self) -> SourceGeneration {
+ self.source_generation
+ }
+
+ /// Returns the expected nonzero state schema version.
+ #[must_use]
+ pub const fn state_schema_version(&self) -> NonZeroU32 {
+ self.state_schema_version
+ }
+
+ /// Returns the injected positive creation time in Unix milliseconds.
+ #[must_use]
+ pub const fn created_at_unix_ms(&self) -> u64 {
+ self.created_at_unix_ms
+ }
+
+ /// Returns the caller-owned SQLite application identifier.
+ #[must_use]
+ pub const fn application_id(&self) -> ServiceSqliteApplicationId {
+ self.application_id
+ }
+
+ /// Returns the reopen identity derived from this initialization record.
+ #[must_use]
+ pub fn identity(&self) -> ServiceDatabaseIdentity {
+ ServiceDatabaseIdentity {
+ service: self.service.clone(),
+ instance: self.instance.clone(),
+ source_generation: self.source_generation,
+ supported_state_schema_version: self.state_schema_version,
+ application_id: self.application_id,
+ }
+ }
+
+ pub(crate) fn matches_paths(&self, paths: &ServiceSqlitePaths) -> bool {
+ self.service == *paths.service() && self.instance == *paths.instance()
+ }
+}
+
+impl ServiceDatabaseIdentity {
+ /// Constructs a reopen expectation bound to canonical service-instance paths.
+ #[must_use]
+ pub fn new(
+ paths: &ServiceSqlitePaths,
+ source_generation: SourceGeneration,
+ supported_state_schema_version: NonZeroU32,
+ application_id: ServiceSqliteApplicationId,
+ ) -> Self {
+ Self {
+ service: paths.service().clone(),
+ instance: paths.instance().clone(),
+ source_generation,
+ supported_state_schema_version,
+ application_id,
+ }
+ }
+
+ /// Returns the bound service identity.
+ #[must_use]
+ pub fn service(&self) -> &ServiceId {
+ &self.service
+ }
+
+ /// Returns the bound instance identity.
+ #[must_use]
+ pub fn instance(&self) -> &InstanceId {
+ &self.instance
+ }
+
+ /// Returns the expected opaque source generation.
+ #[must_use]
+ pub const fn source_generation(&self) -> SourceGeneration {
+ self.source_generation
+ }
+
+ /// Returns the newest state schema version this binary accepts.
+ #[must_use]
+ pub const fn supported_state_schema_version(&self) -> NonZeroU32 {
+ self.supported_state_schema_version
+ }
+
+ /// Returns the expected SQLite application identifier.
+ #[must_use]
+ pub const fn application_id(&self) -> ServiceSqliteApplicationId {
+ self.application_id
+ }
+
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
+ pub(crate) fn matches_paths(&self, paths: &ServiceSqlitePaths) -> bool {
+ self.service == *paths.service() && self.instance == *paths.instance()
+ }
+}
+
+impl fmt::Debug for ServiceDatabaseIdentity {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("ServiceDatabaseIdentity")
+ .field("service", &"[redacted]")
+ .field("instance", &"[redacted]")
+ .field("source_generation", &"[redacted]")
+ .field(
+ "supported_state_schema_version",
+ &self.supported_state_schema_version,
+ )
+ .field("application_id", &self.application_id)
+ .finish()
+ }
+}
+
+impl fmt::Debug for ServiceDatabaseMetadata {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("ServiceDatabaseMetadata")
+ .field("service", &"[redacted]")
+ .field("instance", &"[redacted]")
+ .field("source_generation", &"[redacted]")
+ .field("state_schema_version", &self.state_schema_version)
+ .field("created_at_unix_ms", &self.created_at_unix_ms)
+ .field("application_id", &self.application_id)
+ .finish()
+ }
+}
+
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+enum MetadataFailureKind {
+ AlreadyPresent,
+ Missing,
+ Corrupt,
+ Mismatch,
+ Storage,
+}
+
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+#[derive(Debug)]
+struct MetadataFailure(MetadataFailureKind);
+
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+impl fmt::Display for MetadataFailure {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str(match self.0 {
+ MetadataFailureKind::AlreadyPresent => "SQLite metadata already exists",
+ MetadataFailureKind::Missing => "SQLite metadata is missing",
+ MetadataFailureKind::Corrupt => "SQLite metadata is corrupt",
+ MetadataFailureKind::Mismatch => "SQLite metadata identity does not match",
+ MetadataFailureKind::Storage => "SQLite metadata could not be accessed",
+ })
+ }
+}
+
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+impl Error for MetadataFailure {}
+
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+fn metadata_error(kind: MetadataFailureKind) -> ServiceSqliteError {
+ ServiceSqliteError::with_source(ServiceSqliteErrorKind::Metadata, MetadataFailure(kind))
+}
+
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+pub(crate) async fn write_database_metadata(
+ connection: &mut SqliteConnection,
+ expected: &ServiceDatabaseMetadata,
+) -> Result<(), ServiceSqliteError> {
+ if read_application_id(connection).await? != 0 {
+ return Err(metadata_error(MetadataFailureKind::AlreadyPresent));
+ }
+
+ let mut transaction = connection
+ .begin()
+ .await
+ .map_err(|_| metadata_error(MetadataFailureKind::Storage))?;
+ sqlx::raw_sql(CREATE_METADATA_SQL)
+ .execute(&mut *transaction)
+ .await
+ .map_err(|_| metadata_error(MetadataFailureKind::AlreadyPresent))?;
+ sqlx::query(
+ "INSERT INTO radroots_service_metadata (
+ singleton, service_id, instance_id, source_generation,
+ state_schema_version, created_at_unix_ms
+ ) VALUES (1, ?, ?, ?, ?, ?)",
+ )
+ .bind(expected.service().as_str())
+ .bind(expected.instance().as_str())
+ .bind(expected.source_generation().as_bytes().as_slice())
+ .bind(i64::from(expected.state_schema_version().get()))
+ .bind(
+ i64::try_from(expected.created_at_unix_ms())
+ .map_err(|_| metadata_error(MetadataFailureKind::Corrupt))?,
+ )
+ .execute(&mut *transaction)
+ .await
+ .map_err(|_| metadata_error(MetadataFailureKind::Storage))?;
+ let set_application_id = format!(
+ "PRAGMA application_id = {}",
+ expected.application_id().get()
+ );
+ // The only dynamic token is a validated decimal u31 value.
+ sqlx::query(sqlx::AssertSqlSafe(set_application_id.as_str()))
+ .execute(&mut *transaction)
+ .await
+ .map_err(|_| metadata_error(MetadataFailureKind::Storage))?;
+ transaction
+ .commit()
+ .await
+ .map_err(|_| metadata_error(MetadataFailureKind::Storage))?;
+
+ let actual = read_database_metadata(connection).await?;
+ if actual != *expected {
+ return Err(metadata_error(MetadataFailureKind::Mismatch));
+ }
+ Ok(())
+}
+
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+pub(crate) async fn verify_database_metadata(
+ connection: &mut SqliteConnection,
+ expected: &ServiceDatabaseIdentity,
+) -> Result<ServiceDatabaseMetadata, ServiceSqliteError> {
+ let actual = read_database_metadata(connection).await?;
+ if actual.service != expected.service
+ || actual.instance != expected.instance
+ || actual.source_generation != expected.source_generation
+ || actual.application_id != expected.application_id
+ || actual.state_schema_version > expected.supported_state_schema_version
+ {
+ return Err(metadata_error(MetadataFailureKind::Mismatch));
+ }
+ Ok(actual)
+}
+
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+async fn read_database_metadata(
+ connection: &mut SqliteConnection,
+) -> Result<ServiceDatabaseMetadata, ServiceSqliteError> {
+ let application_id = read_application_id(connection).await?;
+ let application_id = ServiceSqliteApplicationId::new(
+ u32::try_from(application_id).map_err(|_| metadata_error(MetadataFailureKind::Corrupt))?,
+ )
+ .map_err(|_| metadata_error(MetadataFailureKind::Corrupt))?;
+ let rows = sqlx::query(
+ "SELECT
+ singleton, service_id, instance_id, source_generation,
+ state_schema_version, created_at_unix_ms,
+ typeof(singleton) AS singleton_type,
+ typeof(service_id) AS service_id_type,
+ typeof(instance_id) AS instance_id_type,
+ typeof(source_generation) AS source_generation_type,
+ typeof(state_schema_version) AS state_schema_version_type,
+ typeof(created_at_unix_ms) AS created_at_unix_ms_type
+ FROM radroots_service_metadata
+ ORDER BY singleton
+ LIMIT 2",
+ )
+ .fetch_all(&mut *connection)
+ .await
+ .map_err(|_| metadata_error(MetadataFailureKind::Missing))?;
+ let [row] = rows.as_slice() else {
+ return Err(metadata_error(if rows.is_empty() {
+ MetadataFailureKind::Missing
+ } else {
+ MetadataFailureKind::Corrupt
+ }));
+ };
+ for (column, expected_type) in [
+ ("singleton_type", "integer"),
+ ("service_id_type", "text"),
+ ("instance_id_type", "text"),
+ ("source_generation_type", "blob"),
+ ("state_schema_version_type", "integer"),
+ ("created_at_unix_ms_type", "integer"),
+ ] {
+ if row
+ .try_get::<String, _>(column)
+ .map_err(|_| metadata_error(MetadataFailureKind::Corrupt))?
+ != expected_type
+ {
+ return Err(metadata_error(MetadataFailureKind::Corrupt));
+ }
+ }
+ if row
+ .try_get::<i64, _>("singleton")
+ .map_err(|_| metadata_error(MetadataFailureKind::Corrupt))?
+ != 1
+ {
+ return Err(metadata_error(MetadataFailureKind::Corrupt));
+ }
+ let service = ServiceId::new(
+ row.try_get::<String, _>("service_id")
+ .map_err(|_| metadata_error(MetadataFailureKind::Corrupt))?,
+ )
+ .map_err(|_| metadata_error(MetadataFailureKind::Corrupt))?;
+ let instance = InstanceId::new(
+ row.try_get::<String, _>("instance_id")
+ .map_err(|_| metadata_error(MetadataFailureKind::Corrupt))?,
+ )
+ .map_err(|_| metadata_error(MetadataFailureKind::Corrupt))?;
+ let source_generation = SourceGeneration::new(
+ row.try_get::<Vec<u8>, _>("source_generation")
+ .map_err(|_| metadata_error(MetadataFailureKind::Corrupt))?
+ .try_into()
+ .map_err(|_| metadata_error(MetadataFailureKind::Corrupt))?,
+ )
+ .map_err(|_| metadata_error(MetadataFailureKind::Corrupt))?;
+ let state_schema_version = NonZeroU32::new(
+ u32::try_from(
+ row.try_get::<i64, _>("state_schema_version")
+ .map_err(|_| metadata_error(MetadataFailureKind::Corrupt))?,
+ )
+ .map_err(|_| metadata_error(MetadataFailureKind::Corrupt))?,
+ )
+ .ok_or_else(|| metadata_error(MetadataFailureKind::Corrupt))?;
+ let created_at_unix_ms = u64::try_from(
+ row.try_get::<i64, _>("created_at_unix_ms")
+ .map_err(|_| metadata_error(MetadataFailureKind::Corrupt))?,
+ )
+ .map_err(|_| metadata_error(MetadataFailureKind::Corrupt))?;
+ if created_at_unix_ms == 0 {
+ return Err(metadata_error(MetadataFailureKind::Corrupt));
+ }
+
+ Ok(ServiceDatabaseMetadata {
+ service,
+ instance,
+ source_generation,
+ state_schema_version,
+ created_at_unix_ms,
+ application_id,
+ })
+}
+
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+async fn read_application_id(connection: &mut SqliteConnection) -> Result<i64, ServiceSqliteError> {
+ sqlx::query_scalar::<_, i64>("PRAGMA application_id")
+ .fetch_one(connection)
+ .await
+ .map_err(|_| metadata_error(MetadataFailureKind::Storage))
+}
+
+#[cfg(test)]
+mod tests {
+ use std::path::PathBuf;
+
+ use radroots_runtime_paths::{
+ RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver, RadrootsPlatform,
+ RuntimeContext, RuntimeContextBootstrap, RuntimeContextSource,
+ };
+
+ use super::*;
+
+ fn sqlite_paths(service: &str, instance: &str) -> ServiceSqlitePaths {
+ let context = RuntimeContext::resolve(
+ &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()),
+ RuntimeContextBootstrap::new(
+ RadrootsPathProfile::RepoLocal,
+ Some(PathBuf::from("/isolated/service-metadata")),
+ RuntimeContextSource::BootstrapCli,
+ RuntimeContextSource::BootstrapCli,
+ )
+ .expect("bootstrap"),
+ ServiceId::new(service).expect("service"),
+ InstanceId::new(instance).expect("instance"),
+ )
+ .expect("runtime context");
+ ServiceSqlitePaths::from_runtime_context(&context).expect("SQLite paths")
+ }
+
+ fn metadata(
+ paths: &ServiceSqlitePaths,
+ generation_byte: u8,
+ schema_version: u32,
+ creation_time: u64,
+ application_id: u32,
+ ) -> ServiceDatabaseMetadata {
+ ServiceDatabaseMetadata::new(
+ paths,
+ SourceGeneration::new([generation_byte; 32]).expect("source generation"),
+ NonZeroU32::new(schema_version).expect("schema version"),
+ creation_time,
+ ServiceSqliteApplicationId::new(application_id).expect("application ID"),
+ )
+ .expect("database metadata")
+ }
+
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
+ async fn memory_connection() -> SqliteConnection {
+ SqliteConnection::connect("sqlite::memory:")
+ .await
+ .expect("memory SQLite")
+ }
+
+ #[test]
+ fn application_id_and_creation_time_bounds_are_exact() {
+ assert_eq!(
+ ServiceSqliteApplicationId::new(0),
+ Err(ServiceSqliteMetadataValueError::InvalidApplicationId)
+ );
+ assert_eq!(
+ ServiceSqliteApplicationId::new(MAX_APPLICATION_ID + 1),
+ Err(ServiceSqliteMetadataValueError::InvalidApplicationId)
+ );
+ assert_eq!(
+ ServiceSqliteApplicationId::new(MAX_APPLICATION_ID)
+ .expect("maximum application ID")
+ .get(),
+ MAX_APPLICATION_ID
+ );
+
+ let paths = sqlite_paths("myc", "primary");
+ let generation = SourceGeneration::new([7; 32]).expect("source generation");
+ let schema = NonZeroU32::new(1).expect("schema version");
+ let application = ServiceSqliteApplicationId::new(1).expect("application ID");
+ assert_eq!(
+ ServiceDatabaseMetadata::new(&paths, generation, schema, 0, application),
+ Err(ServiceSqliteMetadataValueError::InvalidCreationTime)
+ );
+ assert_eq!(
+ ServiceDatabaseMetadata::new(
+ &paths,
+ generation,
+ schema,
+ MAX_CREATED_AT_UNIX_MS + 1,
+ application,
+ ),
+ Err(ServiceSqliteMetadataValueError::InvalidCreationTime)
+ );
+ assert!(
+ ServiceDatabaseMetadata::new(
+ &paths,
+ generation,
+ schema,
+ MAX_CREATED_AT_UNIX_MS,
+ application,
+ )
+ .is_ok()
+ );
+ assert!(SourceGeneration::new([0; 32]).is_err());
+ assert!(NonZeroU32::new(0).is_none());
+ }
+
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
+ #[tokio::test(flavor = "current_thread")]
+ async fn fresh_metadata_write_read_and_immutability_are_exact() {
+ let paths = sqlite_paths("myc", "primary");
+ let expected = metadata(&paths, 7, 1, 1_700_000_000_000, 0x5244_5351);
+ let mut connection = memory_connection().await;
+
+ write_database_metadata(&mut connection, &expected)
+ .await
+ .expect("write metadata");
+ verify_database_metadata(&mut connection, &expected.identity())
+ .await
+ .expect("verify metadata");
+ let actual = read_database_metadata(&mut connection)
+ .await
+ .expect("read metadata");
+ assert_eq!(actual, expected);
+ assert_eq!(actual.service().as_str(), "myc");
+ assert_eq!(actual.instance().as_str(), "primary");
+ assert_eq!(actual.source_generation().as_bytes(), &[7; 32]);
+ assert_eq!(actual.state_schema_version().get(), 1);
+ assert_eq!(actual.created_at_unix_ms(), 1_700_000_000_000);
+ assert_eq!(actual.application_id().get(), 0x5244_5351);
+ assert_eq!(
+ read_application_id(&mut connection).await.unwrap(),
+ 0x5244_5351
+ );
+
+ sqlx::query(
+ "UPDATE radroots_service_metadata SET state_schema_version = 2 WHERE singleton = 1",
+ )
+ .execute(&mut connection)
+ .await
+ .expect("monotonic schema advance");
+ assert_eq!(
+ read_database_metadata(&mut connection)
+ .await
+ .expect("advanced metadata")
+ .state_schema_version()
+ .get(),
+ 2
+ );
+ for statement in [
+ "UPDATE radroots_service_metadata SET service_id = 'rhi' WHERE singleton = 1",
+ "UPDATE radroots_service_metadata SET state_schema_version = 2 WHERE singleton = 1",
+ "UPDATE radroots_service_metadata SET state_schema_version = 1 WHERE singleton = 1",
+ "DELETE FROM radroots_service_metadata WHERE singleton = 1",
+ "INSERT INTO radroots_service_metadata VALUES (2, 'rhi', 'default', zeroblob(32), 1, 1)",
+ ] {
+ assert!(
+ sqlx::query(statement)
+ .execute(&mut connection)
+ .await
+ .is_err(),
+ "immutable metadata accepted `{statement}`"
+ );
+ }
+ assert_eq!(
+ write_database_metadata(&mut connection, &expected)
+ .await
+ .expect_err("second write must fail")
+ .kind(),
+ ServiceSqliteErrorKind::Metadata
+ );
+
+ let debug = format!("{actual:?}");
+ for sensitive in ["myc", "primary", "07070707"] {
+ assert!(!debug.contains(sensitive));
+ }
+ assert!(debug.contains("source_generation: \"[redacted]\""));
+ }
+
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
+ #[tokio::test(flavor = "current_thread")]
+ async fn every_identity_dimension_must_match() {
+ let paths = sqlite_paths("myc", "primary");
+ let expected = metadata(&paths, 7, 1, 1_700_000_000_000, 0x5244_5351);
+ let mut connection = memory_connection().await;
+ write_database_metadata(&mut connection, &expected)
+ .await
+ .expect("write metadata");
+
+ let alternatives = [
+ ServiceDatabaseIdentity::new(
+ &sqlite_paths("rhi", "primary"),
+ SourceGeneration::new([7; 32]).unwrap(),
+ NonZeroU32::new(1).unwrap(),
+ ServiceSqliteApplicationId::new(0x5244_5351).unwrap(),
+ ),
+ ServiceDatabaseIdentity::new(
+ &sqlite_paths("myc", "secondary"),
+ SourceGeneration::new([7; 32]).unwrap(),
+ NonZeroU32::new(1).unwrap(),
+ ServiceSqliteApplicationId::new(0x5244_5351).unwrap(),
+ ),
+ ServiceDatabaseIdentity::new(
+ &paths,
+ SourceGeneration::new([8; 32]).unwrap(),
+ NonZeroU32::new(1).unwrap(),
+ ServiceSqliteApplicationId::new(0x5244_5351).unwrap(),
+ ),
+ ServiceDatabaseIdentity::new(
+ &paths,
+ SourceGeneration::new([7; 32]).unwrap(),
+ NonZeroU32::new(1).unwrap(),
+ ServiceSqliteApplicationId::new(0x5244_5352).unwrap(),
+ ),
+ ];
+ for alternative in alternatives {
+ assert_eq!(
+ verify_database_metadata(&mut connection, &alternative)
+ .await
+ .expect_err("identity mismatch")
+ .kind(),
+ ServiceSqliteErrorKind::Metadata
+ );
+ }
+
+ let newer_binary = ServiceDatabaseIdentity::new(
+ &paths,
+ expected.source_generation(),
+ NonZeroU32::new(2).unwrap(),
+ expected.application_id(),
+ );
+ let stored = verify_database_metadata(&mut connection, &newer_binary)
+ .await
+ .expect("older schema is migration eligible");
+ assert_eq!(stored.created_at_unix_ms(), 1_700_000_000_000);
+ assert_eq!(stored.state_schema_version().get(), 1);
+
+ let mut newer_state = memory_connection().await;
+ let version_two = metadata(&paths, 7, 2, 1_700_000_000_001, 0x5244_5351);
+ write_database_metadata(&mut newer_state, &version_two)
+ .await
+ .expect("write newer metadata");
+ assert_eq!(
+ verify_database_metadata(&mut newer_state, &expected.identity())
+ .await
+ .expect_err("newer state must fail closed")
+ .kind(),
+ ServiceSqliteErrorKind::Metadata
+ );
+ }
+
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
+ #[tokio::test(flavor = "current_thread")]
+ async fn missing_duplicate_and_corrupt_metadata_fail_closed() {
+ const PERMISSIVE_TABLE: &str = "CREATE TABLE radroots_service_metadata (
+ singleton, service_id, instance_id, source_generation,
+ state_schema_version, created_at_unix_ms
+ )";
+ let paths = sqlite_paths("myc", "primary");
+ let expected = metadata(&paths, 7, 1, 1_700_000_000_000, 0x5244_5351);
+
+ let mut missing_table = memory_connection().await;
+ assert_eq!(
+ verify_database_metadata(&mut missing_table, &expected.identity())
+ .await
+ .expect_err("missing table")
+ .kind(),
+ ServiceSqliteErrorKind::Metadata
+ );
+
+ let corrupt_rows = [
+ "",
+ "INSERT INTO radroots_service_metadata VALUES
+ (1, 'myc', 'primary', randomblob(32), 1, 1700000000000),
+ (2, 'myc', 'primary', randomblob(32), 1, 1700000000000)",
+ "INSERT INTO radroots_service_metadata VALUES
+ (1, NULL, 'primary', randomblob(32), 1, 1700000000000)",
+ "INSERT INTO radroots_service_metadata VALUES
+ (1, 'Myc', 'primary', randomblob(32), 1, 1700000000000)",
+ "INSERT INTO radroots_service_metadata VALUES
+ (1, 'myc', 'Primary', randomblob(32), 1, 1700000000000)",
+ "INSERT INTO radroots_service_metadata VALUES
+ (1, 'myc', 'primary', zeroblob(31), 1, 1700000000000)",
+ "INSERT INTO radroots_service_metadata VALUES
+ (1, 'myc', 'primary', zeroblob(32), 1, 1700000000000)",
+ "INSERT INTO radroots_service_metadata VALUES
+ (1, 'myc', 'primary', randomblob(33), 1, 1700000000000)",
+ "INSERT INTO radroots_service_metadata VALUES
+ (1, 'myc', 'primary', randomblob(32), 0, 1700000000000)",
+ "INSERT INTO radroots_service_metadata VALUES
+ (1, 'myc', 'primary', randomblob(32), 1, 0)",
+ "INSERT INTO radroots_service_metadata VALUES
+ ('1', 'myc', 'primary', randomblob(32), 1, 1700000000000)",
+ ];
+ for corrupt_row in corrupt_rows {
+ let mut connection = memory_connection().await;
+ sqlx::raw_sql(PERMISSIVE_TABLE)
+ .execute(&mut connection)
+ .await
+ .expect("permissive metadata table");
+ sqlx::query("PRAGMA application_id = 1380209489")
+ .execute(&mut connection)
+ .await
+ .expect("application ID");
+ if !corrupt_row.is_empty() {
+ sqlx::raw_sql(corrupt_row)
+ .execute(&mut connection)
+ .await
+ .expect("corrupt metadata row");
+ }
+ assert_eq!(
+ verify_database_metadata(&mut connection, &expected.identity())
+ .await
+ .expect_err("corrupt metadata")
+ .kind(),
+ ServiceSqliteErrorKind::Metadata,
+ "accepted corrupt fixture `{corrupt_row}`"
+ );
+ }
+ }
+}
diff --git a/crates/service_sqlite/src/open.rs b/crates/service_sqlite/src/open.rs
@@ -30,7 +30,8 @@ use sqlx::{
#[cfg(any(target_os = "linux", target_os = "macos"))]
use crate::{
- ServiceSqliteConnectionOptions, ServiceSqliteError, ServiceSqliteErrorKind, WriterAuthority,
+ ServiceDatabaseIdentity, ServiceSqliteConnectionOptions, ServiceSqliteError,
+ ServiceSqliteErrorKind, WriterAuthority,
};
#[cfg(any(target_os = "linux", target_os = "macos"))]
@@ -191,6 +192,7 @@ struct PrivateConnectionPool {
authority: Option<WriterAuthority>,
inspection_guard: Option<ReadOnlyInspectionGuard>,
authority_failure: Arc<AtomicBool>,
+ metadata_failure: Arc<AtomicBool>,
pragma_failure: Arc<AtomicBool>,
}
@@ -207,6 +209,8 @@ impl PrivateConnectionPool {
result.map_err(|source| {
let kind = if self.authority_failure.load(Ordering::Acquire) {
ServiceSqliteErrorKind::Authority
+ } else if self.metadata_failure.load(Ordering::Acquire) {
+ ServiceSqliteErrorKind::Metadata
} else if self.pragma_failure.load(Ordering::Acquire) {
ServiceSqliteErrorKind::Pragma
} else {
@@ -230,6 +234,7 @@ impl PrivateConnectionPool {
)]
async fn open_existing_connection_pool(
paths: &ServiceSqlitePaths,
+ identity: &ServiceDatabaseIdentity,
mode: OpenMode,
policy: ServiceSqliteConnectionOptions,
) -> Result<PrivateConnectionPool, ServiceSqliteError> {
@@ -244,7 +249,7 @@ async fn open_existing_connection_pool(
OpenMode::ReadOnlyInspection => (None, Some(ReadOnlyInspectionGuard::acquire(paths)?)),
OpenMode::Initialize => unreachable!("initialize mode returned above"),
};
- open_connection_pool(paths, mode, policy, authority, inspection_guard).await
+ open_connection_pool(paths, identity, mode, policy, authority, inspection_guard).await
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
@@ -254,11 +259,20 @@ async fn open_existing_connection_pool(
)]
async fn open_initialized_connection_pool(
paths: &ServiceSqlitePaths,
+ identity: &ServiceDatabaseIdentity,
policy: ServiceSqliteConnectionOptions,
authority: WriterAuthority,
) -> Result<PrivateConnectionPool, ServiceSqliteError> {
authority.validate_for(paths)?;
- open_connection_pool(paths, OpenMode::Initialize, policy, Some(authority), None).await
+ open_connection_pool(
+ paths,
+ identity,
+ OpenMode::Initialize,
+ policy,
+ Some(authority),
+ None,
+ )
+ .await
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
@@ -268,11 +282,15 @@ async fn open_initialized_connection_pool(
)]
async fn open_connection_pool(
paths: &ServiceSqlitePaths,
+ identity: &ServiceDatabaseIdentity,
mode: OpenMode,
policy: ServiceSqliteConnectionOptions,
authority: Option<WriterAuthority>,
inspection_guard: Option<ReadOnlyInspectionGuard>,
) -> Result<PrivateConnectionPool, ServiceSqliteError> {
+ if !identity.matches_paths(paths) {
+ return Err(ServiceSqliteError::new(ServiceSqliteErrorKind::Metadata));
+ }
let binding = match (mode, authority.as_ref(), inspection_guard.as_ref()) {
(OpenMode::Initialize | OpenMode::ReadWriteExisting, Some(authority), None) => {
authority.validate_for(paths)?;
@@ -298,6 +316,10 @@ async fn open_connection_pool(
let preflight_policy = verify_connection_policy(&mut preflight, mode, policy).await;
binding.validate(paths)?;
preflight_policy.map_err(|source| connection_source(ServiceSqliteErrorKind::Pragma, source))?;
+ let preflight_metadata =
+ crate::metadata::verify_database_metadata(&mut preflight, identity).await;
+ binding.validate(paths)?;
+ preflight_metadata?;
let preflight_close = preflight.close().await;
binding.validate(paths)?;
preflight_close.map_err(|source| connection_source(ServiceSqliteErrorKind::Open, source))?;
@@ -312,11 +334,16 @@ async fn open_connection_pool(
let pool_binding = binding;
let after_paths = paths.clone();
let before_paths = paths.clone();
+ let after_metadata = identity.clone();
+ let before_metadata = identity.clone();
let authority_failure = Arc::new(AtomicBool::new(false));
+ let metadata_failure = Arc::new(AtomicBool::new(false));
let pragma_failure = Arc::new(AtomicBool::new(false));
let after_authority_failure = Arc::clone(&authority_failure);
+ let after_metadata_failure = Arc::clone(&metadata_failure);
let after_pragma_failure = Arc::clone(&pragma_failure);
let before_authority_failure = Arc::clone(&authority_failure);
+ let before_metadata_failure = Arc::clone(&metadata_failure);
let before_pragma_failure = Arc::clone(&pragma_failure);
let pool_result = SqlitePoolOptions::new()
.min_connections(1)
@@ -328,7 +355,9 @@ async fn open_connection_pool(
.after_connect(move |connection, _metadata| {
let binding = after_binding.clone();
let paths = after_paths.clone();
+ let metadata = after_metadata.clone();
let authority_failure = Arc::clone(&after_authority_failure);
+ let metadata_failure = Arc::clone(&after_metadata_failure);
let pragma_failure = Arc::clone(&after_pragma_failure);
Box::pin(async move {
if binding.validate(&paths).is_err() {
@@ -354,13 +383,29 @@ async fn open_connection_pool(
"SQLite connection policy mismatch".to_owned(),
));
}
+ let metadata_result =
+ crate::metadata::verify_database_metadata(connection, &metadata).await;
+ if binding.validate(&paths).is_err() {
+ authority_failure.store(true, Ordering::Release);
+ return Err(sqlx::Error::Protocol(
+ "SQLite connection authority mismatch".to_owned(),
+ ));
+ }
+ if metadata_result.is_err() {
+ metadata_failure.store(true, Ordering::Release);
+ return Err(sqlx::Error::Protocol(
+ "SQLite connection metadata mismatch".to_owned(),
+ ));
+ }
Ok(())
})
})
.before_acquire(move |connection, _metadata| {
let binding = before_binding.clone();
let paths = before_paths.clone();
+ let metadata = before_metadata.clone();
let authority_failure = Arc::clone(&before_authority_failure);
+ let metadata_failure = Arc::clone(&before_metadata_failure);
let pragma_failure = Arc::clone(&before_pragma_failure);
Box::pin(async move {
if binding.validate(&paths).is_err() {
@@ -384,6 +429,20 @@ async fn open_connection_pool(
pragma_failure.store(true, Ordering::Release);
return Ok(false);
}
+ let metadata_result =
+ crate::metadata::verify_database_metadata(connection, &metadata).await;
+ if binding.validate(&paths).is_err() {
+ authority_failure.store(true, Ordering::Release);
+ return Err(sqlx::Error::Protocol(
+ "SQLite connection authority mismatch".to_owned(),
+ ));
+ }
+ if metadata_result.is_err() {
+ metadata_failure.store(true, Ordering::Release);
+ return Err(sqlx::Error::Protocol(
+ "SQLite connection metadata mismatch".to_owned(),
+ ));
+ }
Ok(true)
})
})
@@ -393,6 +452,8 @@ async fn open_connection_pool(
let pool = pool_result.map_err(|source| {
let kind = if authority_failure.load(Ordering::Acquire) {
ServiceSqliteErrorKind::Authority
+ } else if metadata_failure.load(Ordering::Acquire) {
+ ServiceSqliteErrorKind::Metadata
} else if pragma_failure.load(Ordering::Acquire) {
ServiceSqliteErrorKind::Pragma
} else {
@@ -408,6 +469,7 @@ async fn open_connection_pool(
authority,
inspection_guard,
authority_failure,
+ metadata_failure,
pragma_failure,
})
}
@@ -913,7 +975,7 @@ impl DirectoryBinding {
#[cfg(test)]
mod tests {
- use std::path::PathBuf;
+ use std::{num::NonZeroU32, path::PathBuf};
#[cfg(any(target_os = "linux", target_os = "macos"))]
use std::{
@@ -928,6 +990,9 @@ mod tests {
RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver, RadrootsPlatform,
RuntimeContextBootstrap, RuntimeContextSource,
};
+ use radroots_storage::event::SourceGeneration;
+
+ use crate::{ServiceDatabaseMetadata, ServiceSqliteApplicationId};
use super::*;
@@ -960,10 +1025,22 @@ mod tests {
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
+ fn database_metadata(paths: &ServiceSqlitePaths) -> ServiceDatabaseMetadata {
+ ServiceDatabaseMetadata::new(
+ paths,
+ SourceGeneration::new([7; 32]).expect("source generation"),
+ NonZeroU32::new(1).expect("schema version"),
+ 1_700_000_000_000,
+ ServiceSqliteApplicationId::new(0x5244_5351).expect("application ID"),
+ )
+ .expect("database metadata")
+ }
+
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
async fn initialized_authority(
root: &Path,
instance: &str,
- ) -> (ServiceSqlitePaths, WriterAuthority) {
+ ) -> (ServiceSqlitePaths, ServiceDatabaseIdentity, WriterAuthority) {
let paths = ServiceSqlitePaths::from_runtime_context(&runtime_context(
RadrootsPathProfile::RepoLocal,
Some(root.to_path_buf()),
@@ -973,8 +1050,12 @@ mod tests {
.expect("SQLite paths");
fs::create_dir_all(paths.state_database().parent().expect("state directory"))
.expect("create state directory");
- let authority =
- crate::initialize_database(&paths, OpenMode::Initialize, |database_path| async move {
+ let metadata = database_metadata(&paths);
+ let authority = crate::initialize_database(
+ &paths,
+ OpenMode::Initialize,
+ &metadata,
+ |database_path| async move {
let options = SqliteConnectOptions::new()
.filename(database_path)
.create_if_missing(false);
@@ -983,10 +1064,12 @@ mod tests {
.expect("open reserved database");
connection.close().await.expect("close reserved database");
Ok::<_, Infallible>(())
- })
- .await
- .expect("initialize database");
- (paths, authority)
+ },
+ )
+ .await
+ .expect("initialize database");
+ let identity = metadata.identity();
+ (paths, identity, authority)
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
@@ -994,8 +1077,8 @@ mod tests {
root: &Path,
policy: ServiceSqliteConnectionOptions,
) -> (ServiceSqlitePaths, PrivateConnectionPool) {
- let (paths, authority) = initialized_authority(root, "primary").await;
- let pool = open_initialized_connection_pool(&paths, policy, authority)
+ let (paths, identity, authority) = initialized_authority(root, "primary").await;
+ let pool = open_initialized_connection_pool(&paths, &identity, policy, authority)
.await
.expect("open initialized pool");
(paths, pool)
@@ -1196,6 +1279,42 @@ mod tests {
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[tokio::test(flavor = "current_thread")]
+ async fn metadata_mismatch_fails_open_and_checkout_before_use() {
+ let directory = tempfile::tempdir().expect("temporary directory");
+ let policy = ServiceSqliteConnectionOptions::new(Duration::from_millis(500), 1).unwrap();
+ let (paths, pool) = initialized_pool(directory.path(), policy).await;
+
+ let mut connection = pool.acquire().await.expect("pooled connection");
+ sqlx::query("PRAGMA application_id = 1380209490")
+ .execute(&mut *connection)
+ .await
+ .expect("drift application ID");
+ drop(connection);
+ let error = pool
+ .acquire()
+ .await
+ .expect_err("metadata drift must prevent checkout");
+ assert_eq!(error.kind(), ServiceSqliteErrorKind::Metadata);
+ let authority = pool.close().await.expect("writer authority retained");
+ drop(authority);
+
+ let wrong = ServiceDatabaseIdentity::new(
+ &paths,
+ SourceGeneration::new([8; 32]).expect("wrong generation"),
+ NonZeroU32::new(1).expect("schema version"),
+ ServiceSqliteApplicationId::new(0x5244_5351).expect("application ID"),
+ );
+ let result =
+ open_existing_connection_pool(&paths, &wrong, OpenMode::ReadWriteExisting, policy)
+ .await;
+ let Err(error) = result else {
+ panic!("wrong generation must fail open");
+ };
+ assert_eq!(error.kind(), ServiceSqliteErrorKind::Metadata);
+ }
+
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
+ #[tokio::test(flavor = "current_thread")]
async fn existing_modes_never_create_missing_state_and_enforce_authority() {
let directory = tempfile::tempdir().expect("temporary directory");
let paths = ServiceSqlitePaths::from_runtime_context(&runtime_context(
@@ -1207,10 +1326,12 @@ mod tests {
.expect("SQLite paths");
fs::create_dir_all(paths.state_database().parent().expect("state directory"))
.expect("create state directory");
+ let metadata = database_metadata(&paths).identity();
for mode in [OpenMode::ReadWriteExisting, OpenMode::ReadOnlyInspection] {
let result = open_existing_connection_pool(
&paths,
+ &metadata,
mode,
ServiceSqliteConnectionOptions::reviewed(),
)
@@ -1223,6 +1344,7 @@ mod tests {
}
let result = open_existing_connection_pool(
&paths,
+ &metadata,
OpenMode::Initialize,
ServiceSqliteConnectionOptions::reviewed(),
)
@@ -1237,7 +1359,8 @@ mod tests {
#[tokio::test(flavor = "current_thread")]
async fn initialized_pool_rejects_mismatched_paths_and_rebound_directory() {
let directory = tempfile::tempdir().expect("temporary directory");
- let (_paths, authority) = initialized_authority(directory.path(), "primary").await;
+ let (_paths, metadata, authority) =
+ initialized_authority(directory.path(), "primary").await;
let other = ServiceSqlitePaths::from_runtime_context(&runtime_context(
RadrootsPathProfile::RepoLocal,
Some(directory.path().to_path_buf()),
@@ -1254,6 +1377,7 @@ mod tests {
.expect("create other state directory");
let result = open_initialized_connection_pool(
&other,
+ &metadata,
ServiceSqliteConnectionOptions::reviewed(),
authority,
)
@@ -1263,7 +1387,7 @@ mod tests {
};
assert_eq!(error.kind(), ServiceSqliteErrorKind::Authority);
- let (paths, authority) = initialized_authority(directory.path(), "rebound").await;
+ let (paths, metadata, authority) = initialized_authority(directory.path(), "rebound").await;
let state_directory = paths.state_database().parent().expect("state directory");
let displaced = directory.path().join("displaced-state");
fs::rename(state_directory, &displaced).expect("displace state directory");
@@ -1272,6 +1396,7 @@ mod tests {
.expect("copy replacement database");
let result = open_initialized_connection_pool(
&paths,
+ &metadata,
ServiceSqliteConnectionOptions::reviewed(),
authority,
)
@@ -1313,7 +1438,7 @@ mod tests {
let directory = tempfile::tempdir().expect("temporary directory");
let policy = ServiceSqliteConnectionOptions::reviewed();
- let (symlink_paths, symlink_authority) =
+ let (symlink_paths, symlink_metadata, symlink_authority) =
initialized_authority(directory.path(), "symlink-database").await;
drop(symlink_authority);
let symlink_backing = symlink_paths
@@ -1324,15 +1449,19 @@ mod tests {
fs::rename(symlink_paths.state_database(), &symlink_backing)
.expect("displace symlink database");
symlink(&symlink_backing, symlink_paths.state_database()).expect("database symlink");
- let symlink_result =
- open_existing_connection_pool(&symlink_paths, OpenMode::ReadWriteExisting, policy)
- .await;
+ let symlink_result = open_existing_connection_pool(
+ &symlink_paths,
+ &symlink_metadata,
+ OpenMode::ReadWriteExisting,
+ policy,
+ )
+ .await;
let Err(symlink_error) = symlink_result else {
panic!("database symlink must fail");
};
assert_eq!(symlink_error.kind(), ServiceSqliteErrorKind::Authority);
- let (hardlink_paths, hardlink_authority) =
+ let (hardlink_paths, hardlink_metadata, hardlink_authority) =
initialized_authority(directory.path(), "hardlink-database").await;
drop(hardlink_authority);
let hardlink_alias = hardlink_paths
@@ -1341,9 +1470,13 @@ mod tests {
.expect("state directory")
.join("alias.sqlite");
fs::hard_link(hardlink_paths.state_database(), hardlink_alias).expect("database hard link");
- let hardlink_result =
- open_existing_connection_pool(&hardlink_paths, OpenMode::ReadWriteExisting, policy)
- .await;
+ let hardlink_result = open_existing_connection_pool(
+ &hardlink_paths,
+ &hardlink_metadata,
+ OpenMode::ReadWriteExisting,
+ policy,
+ )
+ .await;
let Err(hardlink_error) = hardlink_result else {
panic!("database hard link must fail");
};
@@ -1384,6 +1517,7 @@ mod tests {
let directory = tempfile::tempdir().expect("temporary directory");
let policy = ServiceSqliteConnectionOptions::reviewed();
let (paths, writable) = initialized_pool(directory.path(), policy).await;
+ let metadata = database_metadata(&paths).identity();
let mut connection = writable.acquire().await.expect("writable connection");
sqlx::query("CREATE TABLE inspection_fixture (value INTEGER NOT NULL)")
.execute(&mut *connection)
@@ -1396,7 +1530,8 @@ mod tests {
drop(connection);
let contended =
- open_existing_connection_pool(&paths, OpenMode::ReadOnlyInspection, policy).await;
+ open_existing_connection_pool(&paths, &metadata, OpenMode::ReadOnlyInspection, policy)
+ .await;
let Err(error) = contended else {
panic!("inspection must reject an active writer");
};
@@ -1408,7 +1543,8 @@ mod tests {
let stale_wal = state_directory.join(WAL_FILE_NAME);
fs::write(&stale_wal, b"stale-wal-evidence").expect("write stale WAL evidence");
let stale =
- open_existing_connection_pool(&paths, OpenMode::ReadOnlyInspection, policy).await;
+ open_existing_connection_pool(&paths, &metadata, OpenMode::ReadOnlyInspection, policy)
+ .await;
let Err(error) = stale else {
panic!("inspection must reject stale WAL state");
};
@@ -1417,9 +1553,10 @@ mod tests {
fs::remove_file(stale_wal).expect("remove test WAL evidence");
let before = directory_snapshot(state_directory);
- let read_only = open_existing_connection_pool(&paths, OpenMode::ReadOnlyInspection, policy)
- .await
- .expect("offline read-only inspection");
+ let read_only =
+ open_existing_connection_pool(&paths, &metadata, OpenMode::ReadOnlyInspection, policy)
+ .await
+ .expect("offline read-only inspection");
let mut connection = read_only.acquire().await.expect("inspection connection");
assert_eq!(
sqlx::query_scalar::<_, i64>("SELECT value FROM inspection_fixture")
@@ -1473,6 +1610,7 @@ mod tests {
let directory = tempfile::tempdir().expect("temporary directory");
let policy = ServiceSqliteConnectionOptions::new(Duration::from_millis(500), 1).unwrap();
let (paths, pool) = initialized_pool(directory.path(), policy).await;
+ let metadata = database_metadata(&paths).identity();
let observer = pool.pool.clone();
let held = pool.acquire().await.expect("only connection");
let saturated = pool.pool.try_acquire();
@@ -1488,6 +1626,7 @@ mod tests {
let read_only = open_existing_connection_pool(
&paths,
+ &metadata,
OpenMode::ReadOnlyInspection,
ServiceSqliteConnectionOptions::reviewed(),
)
diff --git a/crates/service_sqlite/tests/package_boundary.rs b/crates/service_sqlite/tests/package_boundary.rs
@@ -6,6 +6,7 @@ const AUTHORITY_SOURCE: &str = include_str!("../src/authority.rs");
const CONFIG_SOURCE: &str = include_str!("../src/config.rs");
const ERROR_SOURCE: &str = include_str!("../src/error.rs");
const INITIALIZE_SOURCE: &str = include_str!("../src/initialize.rs");
+const METADATA_SOURCE: &str = include_str!("../src/metadata.rs");
const OPEN_SOURCE: &str = include_str!("../src/open.rs");
const STATUS_SOURCE: &str = include_str!("../src/status.rs");
@@ -25,7 +26,14 @@ fn service_sqlite_is_unpublished_lint_governed_and_dependency_bounded() {
assert_eq!(
dependency_keys(MANIFEST, "[dependencies]"),
- BTreeSet::from(["fs2", "radroots_runtime_paths", "rustix", "serde", "sqlx"])
+ BTreeSet::from([
+ "fs2",
+ "radroots_runtime_paths",
+ "radroots_storage",
+ "rustix",
+ "serde",
+ "sqlx"
+ ])
);
assert_eq!(
dependency_keys(MANIFEST, "[dev-dependencies]"),
@@ -38,6 +46,7 @@ fn service_sqlite_is_unpublished_lint_governed_and_dependency_bounded() {
"config",
"error",
"initialize",
+ "metadata",
"open",
"status"
])
@@ -61,6 +70,10 @@ fn service_sqlite_is_unpublished_lint_governed_and_dependency_bounded() {
"ServiceSqliteConnectionOptions",
"ServiceSqliteConnectionOptionsError",
"initialize_database",
+ "ServiceDatabaseIdentity",
+ "ServiceDatabaseMetadata",
+ "ServiceSqliteApplicationId",
+ "ServiceSqliteMetadataValueError",
"ServiceSqlitePathError",
"ServiceSqlitePaths",
"OpenMode",
@@ -92,6 +105,44 @@ fn service_sqlite_is_unpublished_lint_governed_and_dependency_bounded() {
}
for required in [
+ "radroots_service_metadata",
+ "PRAGMA application_id",
+ "source_generation BLOB",
+ "state_schema_version INTEGER",
+ "created_at_unix_ms INTEGER",
+ "radroots_service_metadata_guard_update",
+ "radroots_service_metadata_no_delete",
+ "LIMIT 2",
+ "SourceGeneration",
+ "NonZeroU32",
+ "pub(crate) async fn write_database_metadata",
+ "pub(crate) async fn verify_database_metadata",
+ ] {
+ assert!(
+ METADATA_SOURCE.contains(required),
+ "Step 057 metadata source is missing `{required}`"
+ );
+ }
+
+ for forbidden in [
+ "pub use sqlx",
+ "pub fn write_database_metadata",
+ "pub async fn write_database_metadata",
+ "pub fn verify_database_metadata",
+ "pub async fn verify_database_metadata",
+ "myc_",
+ "rhi_",
+ "SystemTime::now",
+ "getrandom",
+ "tokio::runtime",
+ ] {
+ assert!(
+ !METADATA_SOURCE.contains(forbidden),
+ "Step 057 metadata source contains forbidden surface `{forbidden}`"
+ );
+ }
+
+ for required in [
"journal_mode(SqliteJournalMode::Wal)",
"synchronous(SqliteSynchronous::Full)",
".foreign_keys(true)",
@@ -162,7 +213,6 @@ fn service_sqlite_is_unpublished_lint_governed_and_dependency_bounded() {
"OpenOptions::new",
"remove_file",
"tokio",
- "sqlx",
"rusqlite",
"Command::new",
"std::process",