metadata.rs (49050B)
1 //! Immutable database identity metadata for one service instance. 2 3 use core::{fmt, num::NonZeroU32}; 4 use std::error::Error; 5 6 use radroots_runtime_paths::{InstanceId, ServiceId}; 7 use radroots_storage::event::SourceGeneration; 8 9 use crate::ServiceSqlitePaths; 10 11 #[cfg(any(target_os = "linux", target_os = "macos"))] 12 use crate::{ServiceSqliteError, ServiceSqliteErrorKind}; 13 14 #[cfg(any(target_os = "linux", target_os = "macos"))] 15 use sqlx::{Row, SqliteConnection}; 16 17 #[cfg(all(test, any(target_os = "linux", target_os = "macos")))] 18 use sqlx::Connection; 19 20 const MAX_APPLICATION_ID: u32 = i32::MAX as u32; 21 const MAX_CREATED_AT_UNIX_MS: u64 = i64::MAX as u64; 22 23 const fn valid_creation_time(value: u64) -> bool { 24 value != 0 && value <= MAX_CREATED_AT_UNIX_MS 25 } 26 27 /// A validated nonzero SQLite application identifier. 28 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 29 pub struct ServiceSqliteApplicationId(u32); 30 31 impl ServiceSqliteApplicationId { 32 /// Validates a caller-owned application identifier for SQLite's signed range. 33 pub const fn new(value: u32) -> Result<Self, ServiceSqliteMetadataValueError> { 34 if value == 0 || value > MAX_APPLICATION_ID { 35 return Err(ServiceSqliteMetadataValueError::InvalidApplicationId); 36 } 37 Ok(Self(value)) 38 } 39 40 /// Returns the validated application identifier. 41 #[must_use] 42 pub const fn get(self) -> u32 { 43 self.0 44 } 45 } 46 47 /// Invalid caller-supplied database metadata. 48 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 49 pub enum ServiceSqliteMetadataValueError { 50 InvalidApplicationId, 51 InvalidCreationTime, 52 } 53 54 impl fmt::Display for ServiceSqliteMetadataValueError { 55 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 56 formatter.write_str(match self { 57 Self::InvalidApplicationId => "SQLite application ID is out of range", 58 Self::InvalidCreationTime => "SQLite creation time is out of range", 59 }) 60 } 61 } 62 63 impl Error for ServiceSqliteMetadataValueError {} 64 65 /// Exact immutable identity expected from one service database. 66 #[derive(Clone, PartialEq, Eq)] 67 pub struct ServiceDatabaseMetadata { 68 service: ServiceId, 69 instance: InstanceId, 70 source_generation: SourceGeneration, 71 state_schema_version: NonZeroU32, 72 created_at_unix_ms: u64, 73 application_id: ServiceSqliteApplicationId, 74 } 75 76 /// Exact mount identity and maximum schema version accepted by one service binary. 77 #[derive(Clone, PartialEq, Eq)] 78 pub struct ServiceDatabaseIdentity { 79 service: ServiceId, 80 instance: InstanceId, 81 source_generation: SourceGeneration, 82 supported_state_schema_version: NonZeroU32, 83 application_id: ServiceSqliteApplicationId, 84 } 85 86 /// Sealed expectation for opening an existing database without guessing its generation. 87 #[derive(Clone, PartialEq, Eq)] 88 pub struct ExistingServiceDatabaseIntent { 89 service: ServiceId, 90 instance: InstanceId, 91 supported_state_schema_version: NonZeroU32, 92 application_id: ServiceSqliteApplicationId, 93 } 94 95 impl ServiceDatabaseMetadata { 96 #[cfg(any(target_os = "linux", target_os = "macos"))] 97 pub(crate) fn from_verified_backup( 98 service: ServiceId, 99 instance: InstanceId, 100 source_generation: SourceGeneration, 101 state_schema_version: NonZeroU32, 102 created_at_unix_ms: u64, 103 application_id: ServiceSqliteApplicationId, 104 ) -> Result<Self, ServiceSqliteMetadataValueError> { 105 if !valid_creation_time(created_at_unix_ms) { 106 return Err(ServiceSqliteMetadataValueError::InvalidCreationTime); 107 } 108 Ok(Self { 109 service, 110 instance, 111 source_generation, 112 state_schema_version, 113 created_at_unix_ms, 114 application_id, 115 }) 116 } 117 118 /// Constructs metadata bound to the service and instance in canonical paths. 119 pub fn new( 120 paths: &ServiceSqlitePaths, 121 source_generation: SourceGeneration, 122 state_schema_version: NonZeroU32, 123 created_at_unix_ms: u64, 124 application_id: ServiceSqliteApplicationId, 125 ) -> Result<Self, ServiceSqliteMetadataValueError> { 126 if !valid_creation_time(created_at_unix_ms) { 127 return Err(ServiceSqliteMetadataValueError::InvalidCreationTime); 128 } 129 Ok(Self { 130 service: paths.service().clone(), 131 instance: paths.instance().clone(), 132 source_generation, 133 state_schema_version, 134 created_at_unix_ms, 135 application_id, 136 }) 137 } 138 139 /// Returns the bound service identity. 140 #[must_use] 141 pub fn service(&self) -> &ServiceId { 142 &self.service 143 } 144 145 /// Returns the bound instance identity. 146 #[must_use] 147 pub fn instance(&self) -> &InstanceId { 148 &self.instance 149 } 150 151 /// Returns the opaque nonzero source generation. 152 #[must_use] 153 pub const fn source_generation(&self) -> SourceGeneration { 154 self.source_generation 155 } 156 157 /// Returns the expected nonzero state schema version. 158 #[must_use] 159 pub const fn state_schema_version(&self) -> NonZeroU32 { 160 self.state_schema_version 161 } 162 163 /// Returns the injected positive creation time in Unix milliseconds. 164 #[must_use] 165 pub const fn created_at_unix_ms(&self) -> u64 { 166 self.created_at_unix_ms 167 } 168 169 /// Returns the caller-owned SQLite application identifier. 170 #[must_use] 171 pub const fn application_id(&self) -> ServiceSqliteApplicationId { 172 self.application_id 173 } 174 175 /// Returns the reopen identity derived from this initialization record. 176 #[must_use] 177 pub fn identity(&self) -> ServiceDatabaseIdentity { 178 ServiceDatabaseIdentity { 179 service: self.service.clone(), 180 instance: self.instance.clone(), 181 source_generation: self.source_generation, 182 supported_state_schema_version: self.state_schema_version, 183 application_id: self.application_id, 184 } 185 } 186 187 pub(crate) fn matches_paths(&self, paths: &ServiceSqlitePaths) -> bool { 188 crate::all_constraints([ 189 self.service == *paths.service(), 190 self.instance == *paths.instance(), 191 ]) 192 } 193 } 194 195 impl ServiceDatabaseIdentity { 196 /// Constructs a reopen expectation bound to canonical service-instance paths. 197 #[must_use] 198 pub fn new( 199 paths: &ServiceSqlitePaths, 200 source_generation: SourceGeneration, 201 supported_state_schema_version: NonZeroU32, 202 application_id: ServiceSqliteApplicationId, 203 ) -> Self { 204 Self { 205 service: paths.service().clone(), 206 instance: paths.instance().clone(), 207 source_generation, 208 supported_state_schema_version, 209 application_id, 210 } 211 } 212 213 /// Returns the bound service identity. 214 #[must_use] 215 pub fn service(&self) -> &ServiceId { 216 &self.service 217 } 218 219 /// Returns the bound instance identity. 220 #[must_use] 221 pub fn instance(&self) -> &InstanceId { 222 &self.instance 223 } 224 225 /// Returns the expected opaque source generation. 226 #[must_use] 227 pub const fn source_generation(&self) -> SourceGeneration { 228 self.source_generation 229 } 230 231 /// Returns the newest state schema version this binary accepts. 232 #[must_use] 233 pub const fn supported_state_schema_version(&self) -> NonZeroU32 { 234 self.supported_state_schema_version 235 } 236 237 /// Returns the expected SQLite application identifier. 238 #[must_use] 239 pub const fn application_id(&self) -> ServiceSqliteApplicationId { 240 self.application_id 241 } 242 243 #[cfg(any(target_os = "linux", target_os = "macos"))] 244 pub(crate) fn matches_paths(&self, paths: &ServiceSqlitePaths) -> bool { 245 crate::all_constraints([ 246 self.service == *paths.service(), 247 self.instance == *paths.instance(), 248 ]) 249 } 250 } 251 252 impl ExistingServiceDatabaseIntent { 253 /// Binds an existing-only open to canonical paths and the binary's fixed contract. 254 #[must_use] 255 pub fn new( 256 paths: &ServiceSqlitePaths, 257 supported_state_schema_version: NonZeroU32, 258 application_id: ServiceSqliteApplicationId, 259 ) -> Self { 260 Self { 261 service: paths.service().clone(), 262 instance: paths.instance().clone(), 263 supported_state_schema_version, 264 application_id, 265 } 266 } 267 268 /// Returns the bound service identity. 269 #[must_use] 270 pub fn service(&self) -> &ServiceId { 271 &self.service 272 } 273 274 /// Returns the bound instance identity. 275 #[must_use] 276 pub fn instance(&self) -> &InstanceId { 277 &self.instance 278 } 279 280 /// Returns the newest state schema version this binary accepts. 281 #[must_use] 282 pub const fn supported_state_schema_version(&self) -> NonZeroU32 { 283 self.supported_state_schema_version 284 } 285 286 /// Returns the expected SQLite application identifier. 287 #[must_use] 288 pub const fn application_id(&self) -> ServiceSqliteApplicationId { 289 self.application_id 290 } 291 292 pub(crate) fn matches_paths(&self, paths: &ServiceSqlitePaths) -> bool { 293 crate::all_constraints([ 294 self.service == *paths.service(), 295 self.instance == *paths.instance(), 296 ]) 297 } 298 299 pub(crate) fn identity_for( 300 &self, 301 metadata: &ServiceDatabaseMetadata, 302 ) -> ServiceDatabaseIdentity { 303 ServiceDatabaseIdentity { 304 service: self.service.clone(), 305 instance: self.instance.clone(), 306 source_generation: metadata.source_generation, 307 supported_state_schema_version: self.supported_state_schema_version, 308 application_id: self.application_id, 309 } 310 } 311 } 312 313 impl fmt::Debug for ServiceDatabaseIdentity { 314 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 315 formatter 316 .debug_struct("ServiceDatabaseIdentity") 317 .field("service", &"[redacted]") 318 .field("instance", &"[redacted]") 319 .field("source_generation", &"[redacted]") 320 .field( 321 "supported_state_schema_version", 322 &self.supported_state_schema_version, 323 ) 324 .field("application_id", &self.application_id) 325 .finish() 326 } 327 } 328 329 impl fmt::Debug for ExistingServiceDatabaseIntent { 330 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 331 formatter 332 .debug_struct("ExistingServiceDatabaseIntent") 333 .field("service", &"[redacted]") 334 .field("instance", &"[redacted]") 335 .field( 336 "supported_state_schema_version", 337 &self.supported_state_schema_version, 338 ) 339 .field("application_id", &self.application_id) 340 .finish() 341 } 342 } 343 344 impl fmt::Debug for ServiceDatabaseMetadata { 345 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 346 formatter 347 .debug_struct("ServiceDatabaseMetadata") 348 .field("service", &"[redacted]") 349 .field("instance", &"[redacted]") 350 .field("source_generation", &"[redacted]") 351 .field("state_schema_version", &self.state_schema_version) 352 .field("created_at_unix_ms", &self.created_at_unix_ms) 353 .field("application_id", &self.application_id) 354 .finish() 355 } 356 } 357 358 #[cfg(any(target_os = "linux", target_os = "macos"))] 359 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 360 enum MetadataFailureKind { 361 AlreadyPresent, 362 Missing, 363 Corrupt, 364 Mismatch, 365 Storage, 366 } 367 368 #[cfg(any(target_os = "linux", target_os = "macos"))] 369 #[derive(Debug)] 370 struct MetadataFailure(MetadataFailureKind); 371 372 #[cfg(any(target_os = "linux", target_os = "macos"))] 373 impl fmt::Display for MetadataFailure { 374 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 375 formatter.write_str(match self.0 { 376 MetadataFailureKind::AlreadyPresent => "SQLite metadata already exists", 377 MetadataFailureKind::Missing => "SQLite metadata is missing", 378 MetadataFailureKind::Corrupt => "SQLite metadata is corrupt", 379 MetadataFailureKind::Mismatch => "SQLite metadata identity does not match", 380 MetadataFailureKind::Storage => "SQLite metadata could not be accessed", 381 }) 382 } 383 } 384 385 #[cfg(any(target_os = "linux", target_os = "macos"))] 386 impl Error for MetadataFailure {} 387 388 #[cfg(any(target_os = "linux", target_os = "macos"))] 389 fn metadata_error(kind: MetadataFailureKind) -> ServiceSqliteError { 390 ServiceSqliteError::with_source(ServiceSqliteErrorKind::Metadata, MetadataFailure(kind)) 391 } 392 393 #[cfg(any(target_os = "linux", target_os = "macos"))] 394 fn require_metadata_condition( 395 condition: bool, 396 kind: MetadataFailureKind, 397 ) -> Result<(), ServiceSqliteError> { 398 condition.then_some(()).ok_or_else(|| metadata_error(kind)) 399 } 400 401 #[cfg(any(target_os = "linux", target_os = "macos"))] 402 #[derive(Debug)] 403 struct MigrationLedgerInitializationFailure; 404 405 #[cfg(any(target_os = "linux", target_os = "macos"))] 406 impl fmt::Display for MigrationLedgerInitializationFailure { 407 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 408 formatter.write_str("SQLite migration ledger could not be initialized") 409 } 410 } 411 412 #[cfg(any(target_os = "linux", target_os = "macos"))] 413 impl Error for MigrationLedgerInitializationFailure {} 414 415 #[cfg(all(test, any(target_os = "linux", target_os = "macos")))] 416 pub(crate) async fn write_database_metadata( 417 connection: &mut SqliteConnection, 418 expected: &ServiceDatabaseMetadata, 419 schema_catalog: &crate::SchemaCatalog, 420 ) -> Result<(), ServiceSqliteError> { 421 let mut transaction = connection 422 .begin() 423 .await 424 .map_err(|_| metadata_error(MetadataFailureKind::Storage))?; 425 write_database_metadata_in_transaction(&mut transaction, expected, schema_catalog).await?; 426 transaction 427 .commit() 428 .await 429 .map_err(|_| metadata_error(MetadataFailureKind::Storage))?; 430 431 let actual = read_database_metadata(connection).await?; 432 require_metadata_condition(actual == *expected, MetadataFailureKind::Mismatch)?; 433 Ok(()) 434 } 435 436 #[cfg(any(target_os = "linux", target_os = "macos"))] 437 pub(crate) async fn write_database_metadata_in_transaction( 438 connection: &mut SqliteConnection, 439 expected: &ServiceDatabaseMetadata, 440 schema_catalog: &crate::SchemaCatalog, 441 ) -> Result<(), ServiceSqliteError> { 442 if expected.state_schema_version().get() != 1 { 443 return Err(metadata_error(MetadataFailureKind::Mismatch)); 444 } 445 if read_application_id(connection).await? != 0 { 446 return Err(metadata_error(MetadataFailureKind::AlreadyPresent)); 447 } 448 449 for statement in crate::integrity::catalog::METADATA_SCHEMA_SQL { 450 sqlx::query(statement) 451 .execute(&mut *connection) 452 .await 453 .map_err(|_| metadata_error(MetadataFailureKind::AlreadyPresent))?; 454 } 455 for statement in crate::integrity::catalog::MIGRATION_LEDGER_SCHEMA_SQL { 456 sqlx::query(statement) 457 .execute(&mut *connection) 458 .await 459 .map_err(|_source| { 460 ServiceSqliteError::with_source( 461 ServiceSqliteErrorKind::Migration, 462 MigrationLedgerInitializationFailure, 463 ) 464 })?; 465 } 466 sqlx::query( 467 "INSERT INTO radroots_service_metadata ( 468 singleton, service_id, instance_id, source_generation, 469 state_schema_version, created_at_unix_ms 470 ) VALUES (1, ?, ?, ?, ?, ?)", 471 ) 472 .bind(expected.service().as_str()) 473 .bind(expected.instance().as_str()) 474 .bind(expected.source_generation().as_bytes().as_slice()) 475 .bind(i64::from(expected.state_schema_version().get())) 476 .bind( 477 i64::try_from(expected.created_at_unix_ms()) 478 .map_err(|_| metadata_error(MetadataFailureKind::Corrupt))?, 479 ) 480 .execute(&mut *connection) 481 .await 482 .map_err(|_| metadata_error(MetadataFailureKind::Storage))?; 483 let set_application_id = format!( 484 "PRAGMA application_id = {}", 485 expected.application_id().get() 486 ); 487 // The only dynamic token is a validated decimal u31 value. 488 sqlx::query(sqlx::AssertSqlSafe(set_application_id.as_str())) 489 .execute(&mut *connection) 490 .await 491 .map_err(|_| metadata_error(MetadataFailureKind::Storage))?; 492 crate::integrity::verify_schema_catalog( 493 &mut *connection, 494 schema_catalog, 495 expected.state_schema_version().get(), 496 ) 497 .await?; 498 let actual = read_database_metadata(connection).await?; 499 require_metadata_condition(actual == *expected, MetadataFailureKind::Mismatch)?; 500 Ok(()) 501 } 502 503 #[cfg(any(target_os = "linux", target_os = "macos"))] 504 pub(crate) async fn verify_database_metadata( 505 connection: &mut SqliteConnection, 506 expected: &ServiceDatabaseIdentity, 507 ) -> Result<ServiceDatabaseMetadata, ServiceSqliteError> { 508 let actual = read_database_metadata(connection).await?; 509 require_metadata_condition( 510 crate::all_constraints([ 511 actual.service == expected.service, 512 actual.instance == expected.instance, 513 actual.source_generation == expected.source_generation, 514 actual.application_id == expected.application_id, 515 actual.state_schema_version <= expected.supported_state_schema_version, 516 ]), 517 MetadataFailureKind::Mismatch, 518 )?; 519 Ok(actual) 520 } 521 522 #[cfg(any(target_os = "linux", target_os = "macos"))] 523 pub(crate) async fn verify_existing_database_intent( 524 connection: &mut SqliteConnection, 525 intent: &ExistingServiceDatabaseIntent, 526 ) -> Result<ServiceDatabaseMetadata, ServiceSqliteError> { 527 let actual = read_database_metadata(connection).await?; 528 require_metadata_condition( 529 crate::all_constraints([ 530 actual.service == intent.service, 531 actual.instance == intent.instance, 532 actual.application_id == intent.application_id, 533 actual.state_schema_version <= intent.supported_state_schema_version, 534 ]), 535 MetadataFailureKind::Mismatch, 536 )?; 537 Ok(actual) 538 } 539 540 #[cfg(any(target_os = "linux", target_os = "macos"))] 541 async fn read_database_metadata( 542 connection: &mut SqliteConnection, 543 ) -> Result<ServiceDatabaseMetadata, ServiceSqliteError> { 544 let application_id = read_application_id(connection).await?; 545 let application_id = ServiceSqliteApplicationId::new( 546 u32::try_from(application_id).map_err(|_| metadata_error(MetadataFailureKind::Corrupt))?, 547 ) 548 .map_err(|_| metadata_error(MetadataFailureKind::Corrupt))?; 549 let rows = sqlx::query( 550 "SELECT 551 singleton, 552 state_schema_version, created_at_unix_ms, 553 typeof(singleton) = 'integer' AS singleton_type_ok, 554 typeof(service_id) = 'text' AS service_id_type_ok, 555 length(CAST(service_id AS BLOB)) AS service_id_length, 556 substr(CAST(service_id AS BLOB), 1, 129) AS service_id_prefix, 557 typeof(instance_id) = 'text' AS instance_id_type_ok, 558 length(CAST(instance_id AS BLOB)) AS instance_id_length, 559 substr(CAST(instance_id AS BLOB), 1, 129) AS instance_id_prefix, 560 typeof(source_generation) = 'blob' AS source_generation_type_ok, 561 length(source_generation) AS source_generation_length, 562 substr(source_generation, 1, 33) AS source_generation_prefix, 563 typeof(state_schema_version) = 'integer' AS state_schema_version_type_ok, 564 typeof(created_at_unix_ms) = 'integer' AS created_at_unix_ms_type_ok 565 FROM radroots_service_metadata 566 LIMIT 2", 567 ) 568 .fetch_all(&mut *connection) 569 .await 570 .map_err(|_| metadata_error(MetadataFailureKind::Missing))?; 571 let [row] = rows.as_slice() else { 572 return Err(metadata_error(if rows.is_empty() { 573 MetadataFailureKind::Missing 574 } else { 575 MetadataFailureKind::Corrupt 576 })); 577 }; 578 for column in [ 579 "singleton_type_ok", 580 "state_schema_version_type_ok", 581 "created_at_unix_ms_type_ok", 582 ] { 583 require_metadata_condition( 584 row.try_get::<i64, _>(column) 585 .map_err(|_| metadata_error(MetadataFailureKind::Corrupt))? 586 == 1, 587 MetadataFailureKind::Corrupt, 588 )?; 589 } 590 require_metadata_condition( 591 row.try_get::<i64, _>("singleton") 592 .map_err(|_| metadata_error(MetadataFailureKind::Corrupt))? 593 == 1, 594 MetadataFailureKind::Corrupt, 595 )?; 596 let service = crate::persisted_value::bounded_utf8( 597 row, 598 "service_id_type_ok", 599 "service_id_length", 600 "service_id_prefix", 601 1, 602 crate::persisted_value::MAX_IDENTIFIER_UTF8_BYTES, 603 ) 604 .and_then(|value| ServiceId::new(value).ok()) 605 .ok_or_else(|| metadata_error(MetadataFailureKind::Corrupt))?; 606 let instance = crate::persisted_value::bounded_utf8( 607 row, 608 "instance_id_type_ok", 609 "instance_id_length", 610 "instance_id_prefix", 611 1, 612 crate::persisted_value::MAX_IDENTIFIER_UTF8_BYTES, 613 ) 614 .and_then(|value| InstanceId::new(value).ok()) 615 .ok_or_else(|| metadata_error(MetadataFailureKind::Corrupt))?; 616 let source_generation = crate::persisted_value::bounded_bytes( 617 row, 618 "source_generation_type_ok", 619 "source_generation_length", 620 "source_generation_prefix", 621 32, 622 32, 623 ) 624 .and_then(|value| <[u8; 32]>::try_from(value).ok()) 625 .and_then(|value| SourceGeneration::new(value).ok()) 626 .ok_or_else(|| metadata_error(MetadataFailureKind::Corrupt))?; 627 let state_schema_version = NonZeroU32::new( 628 u32::try_from( 629 row.try_get::<i64, _>("state_schema_version") 630 .map_err(|_| metadata_error(MetadataFailureKind::Corrupt))?, 631 ) 632 .map_err(|_| metadata_error(MetadataFailureKind::Corrupt))?, 633 ) 634 .ok_or_else(|| metadata_error(MetadataFailureKind::Corrupt))?; 635 let created_at_unix_ms = u64::try_from( 636 row.try_get::<i64, _>("created_at_unix_ms") 637 .map_err(|_| metadata_error(MetadataFailureKind::Corrupt))?, 638 ) 639 .map_err(|_| metadata_error(MetadataFailureKind::Corrupt))?; 640 if created_at_unix_ms == 0 { 641 return Err(metadata_error(MetadataFailureKind::Corrupt)); 642 } 643 644 Ok(ServiceDatabaseMetadata { 645 service, 646 instance, 647 source_generation, 648 state_schema_version, 649 created_at_unix_ms, 650 application_id, 651 }) 652 } 653 654 #[cfg(any(target_os = "linux", target_os = "macos"))] 655 async fn read_application_id(connection: &mut SqliteConnection) -> Result<i64, ServiceSqliteError> { 656 sqlx::query_scalar::<_, i64>("PRAGMA application_id") 657 .fetch_one(connection) 658 .await 659 .map_err(|_| metadata_error(MetadataFailureKind::Storage)) 660 } 661 662 #[cfg(test)] 663 mod tests { 664 use std::path::PathBuf; 665 666 use radroots_runtime_paths::{ 667 RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver, RadrootsPlatform, 668 RuntimeContext, RuntimeContextBootstrap, RuntimeContextSource, 669 }; 670 671 use super::*; 672 673 fn sqlite_paths(service: &str, instance: &str) -> ServiceSqlitePaths { 674 let context = RuntimeContext::resolve( 675 &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()), 676 RuntimeContextBootstrap::new( 677 RadrootsPathProfile::RepoLocal, 678 Some(PathBuf::from("/isolated/service-metadata")), 679 RuntimeContextSource::BootstrapCli, 680 RuntimeContextSource::BootstrapCli, 681 ) 682 .expect("bootstrap"), 683 ServiceId::new(service).expect("service"), 684 InstanceId::new(instance).expect("instance"), 685 ) 686 .expect("runtime context"); 687 ServiceSqlitePaths::from_runtime_context(&context).expect("SQLite paths") 688 } 689 690 #[cfg(any(target_os = "linux", target_os = "macos"))] 691 fn metadata( 692 paths: &ServiceSqlitePaths, 693 generation_byte: u8, 694 schema_version: u32, 695 creation_time: u64, 696 application_id: u32, 697 ) -> ServiceDatabaseMetadata { 698 ServiceDatabaseMetadata::new( 699 paths, 700 SourceGeneration::new([generation_byte; 32]).expect("source generation"), 701 NonZeroU32::new(schema_version).expect("schema version"), 702 creation_time, 703 ServiceSqliteApplicationId::new(application_id).expect("application ID"), 704 ) 705 .expect("database metadata") 706 } 707 708 #[cfg(any(target_os = "linux", target_os = "macos"))] 709 async fn memory_connection() -> SqliteConnection { 710 SqliteConnection::connect("sqlite::memory:") 711 .await 712 .expect("memory SQLite") 713 } 714 715 #[cfg(any(target_os = "linux", target_os = "macos"))] 716 fn base_schema_catalog() -> crate::SchemaCatalog { 717 let migrations = crate::MigrationCatalog::new([]).expect("empty migration catalog"); 718 let digest = crate::SchemaVersionCatalog::computed_digest(1, []) 719 .expect("base schema snapshot digest"); 720 let version = 721 crate::SchemaVersionCatalog::new(1, [], digest).expect("base schema version catalog"); 722 crate::SchemaCatalog::new(&migrations, [version]).expect("base schema catalog") 723 } 724 725 #[cfg(any(target_os = "linux", target_os = "macos"))] 726 #[test] 727 fn metadata_failure_inventory_preserves_kind_and_trusted_source() { 728 for kind in [ 729 MetadataFailureKind::AlreadyPresent, 730 MetadataFailureKind::Missing, 731 MetadataFailureKind::Corrupt, 732 MetadataFailureKind::Mismatch, 733 MetadataFailureKind::Storage, 734 ] { 735 assert!(require_metadata_condition(true, kind).is_ok()); 736 let error = require_metadata_condition(false, kind).expect_err("false condition"); 737 assert_eq!(error.kind(), ServiceSqliteErrorKind::Metadata); 738 assert!(error.source().is_some()); 739 assert!(!error.to_string().contains('/')); 740 assert!(format!("{error:?}").contains("Metadata")); 741 } 742 } 743 744 #[test] 745 fn application_id_and_creation_time_bounds_are_exact() { 746 assert!(valid_creation_time(1)); 747 assert!(valid_creation_time(MAX_CREATED_AT_UNIX_MS)); 748 assert!(!valid_creation_time(0)); 749 assert!(!valid_creation_time(MAX_CREATED_AT_UNIX_MS + 1)); 750 assert_eq!( 751 ServiceSqliteApplicationId::new(0), 752 Err(ServiceSqliteMetadataValueError::InvalidApplicationId) 753 ); 754 assert_eq!( 755 ServiceSqliteApplicationId::new(MAX_APPLICATION_ID + 1), 756 Err(ServiceSqliteMetadataValueError::InvalidApplicationId) 757 ); 758 assert_eq!( 759 ServiceSqliteApplicationId::new(MAX_APPLICATION_ID) 760 .expect("maximum application ID") 761 .get(), 762 MAX_APPLICATION_ID 763 ); 764 765 let paths = sqlite_paths("myc", "primary"); 766 let generation = SourceGeneration::new([7; 32]).expect("source generation"); 767 let schema = NonZeroU32::new(1).expect("schema version"); 768 let application = ServiceSqliteApplicationId::new(1).expect("application ID"); 769 assert_eq!( 770 ServiceDatabaseMetadata::new(&paths, generation, schema, 0, application), 771 Err(ServiceSqliteMetadataValueError::InvalidCreationTime) 772 ); 773 assert_eq!( 774 ServiceDatabaseMetadata::new( 775 &paths, 776 generation, 777 schema, 778 MAX_CREATED_AT_UNIX_MS + 1, 779 application, 780 ), 781 Err(ServiceSqliteMetadataValueError::InvalidCreationTime) 782 ); 783 assert!( 784 ServiceDatabaseMetadata::new( 785 &paths, 786 generation, 787 schema, 788 MAX_CREATED_AT_UNIX_MS, 789 application, 790 ) 791 .is_ok() 792 ); 793 assert!(SourceGeneration::new([0; 32]).is_err()); 794 assert!(NonZeroU32::new(0).is_none()); 795 } 796 797 #[cfg(any(target_os = "linux", target_os = "macos"))] 798 #[tokio::test(flavor = "current_thread")] 799 async fn fresh_metadata_write_read_and_immutability_are_exact() { 800 let paths = sqlite_paths("myc", "primary"); 801 let expected = metadata(&paths, 7, 1, 1_700_000_000_000, 0x5244_5351); 802 let mut connection = memory_connection().await; 803 804 write_database_metadata(&mut connection, &expected, &base_schema_catalog()) 805 .await 806 .expect("write metadata"); 807 verify_database_metadata(&mut connection, &expected.identity()) 808 .await 809 .expect("verify metadata"); 810 let actual = read_database_metadata(&mut connection) 811 .await 812 .expect("read metadata"); 813 assert_eq!(actual, expected); 814 assert_eq!(actual.service().as_str(), "myc"); 815 assert_eq!(actual.instance().as_str(), "primary"); 816 assert_eq!(actual.source_generation().as_bytes(), &[7; 32]); 817 assert_eq!(actual.state_schema_version().get(), 1); 818 assert_eq!(actual.created_at_unix_ms(), 1_700_000_000_000); 819 assert_eq!(actual.application_id().get(), 0x5244_5351); 820 assert_eq!( 821 read_application_id(&mut connection).await.unwrap(), 822 0x5244_5351 823 ); 824 assert_eq!( 825 sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM schema_migrations") 826 .fetch_one(&mut connection) 827 .await 828 .unwrap(), 829 0 830 ); 831 832 sqlx::query( 833 "UPDATE radroots_service_metadata SET state_schema_version = 2 WHERE singleton = 1", 834 ) 835 .execute(&mut connection) 836 .await 837 .expect("monotonic schema advance"); 838 assert_eq!( 839 read_database_metadata(&mut connection) 840 .await 841 .expect("advanced metadata") 842 .state_schema_version() 843 .get(), 844 2 845 ); 846 for statement in [ 847 "UPDATE radroots_service_metadata SET service_id = 'rhi' WHERE singleton = 1", 848 "UPDATE radroots_service_metadata SET state_schema_version = 2 WHERE singleton = 1", 849 "UPDATE radroots_service_metadata SET state_schema_version = 1 WHERE singleton = 1", 850 "DELETE FROM radroots_service_metadata WHERE singleton = 1", 851 "INSERT INTO radroots_service_metadata VALUES (2, 'rhi', 'default', zeroblob(32), 1, 1)", 852 ] { 853 assert!( 854 sqlx::query(statement) 855 .execute(&mut connection) 856 .await 857 .is_err(), 858 "immutable metadata accepted `{statement}`" 859 ); 860 } 861 assert_eq!( 862 write_database_metadata(&mut connection, &expected, &base_schema_catalog()) 863 .await 864 .expect_err("second write must fail") 865 .kind(), 866 ServiceSqliteErrorKind::Metadata 867 ); 868 869 let debug = format!("{actual:?}"); 870 for sensitive in ["myc", "primary", "07070707"] { 871 assert!(!debug.contains(sensitive)); 872 } 873 assert!(debug.contains("source_generation: \"[redacted]\"")); 874 } 875 876 #[cfg(any(target_os = "linux", target_os = "macos"))] 877 #[tokio::test(flavor = "current_thread")] 878 async fn existing_intent_discovers_generation_and_binds_every_trusted_dimension() { 879 let paths = sqlite_paths("myc", "primary"); 880 let expected = metadata(&paths, 7, 1, 1_700_000_000_000, 0x5244_5351); 881 let mut connection = memory_connection().await; 882 write_database_metadata(&mut connection, &expected, &base_schema_catalog()) 883 .await 884 .expect("write metadata"); 885 886 let intent = ExistingServiceDatabaseIntent::new( 887 &paths, 888 NonZeroU32::new(2).expect("schema ceiling"), 889 expected.application_id(), 890 ); 891 let actual = verify_existing_database_intent(&mut connection, &intent) 892 .await 893 .expect("discover metadata"); 894 assert_eq!(actual, expected); 895 assert_eq!(actual.source_generation().as_bytes(), &[7; 32]); 896 assert_eq!( 897 intent.identity_for(&actual).source_generation(), 898 actual.source_generation() 899 ); 900 901 let debug = format!("{intent:?}"); 902 assert!(debug.contains("ExistingServiceDatabaseIntent")); 903 assert!(!debug.contains("myc")); 904 assert!(!debug.contains("primary")); 905 assert!(!debug.contains("07070707")); 906 907 let other_paths = sqlite_paths("rhi", "primary"); 908 let wrong_service = ExistingServiceDatabaseIntent::new( 909 &other_paths, 910 intent.supported_state_schema_version(), 911 intent.application_id(), 912 ); 913 let other_instance_paths = sqlite_paths("myc", "secondary"); 914 let wrong_instance = ExistingServiceDatabaseIntent::new( 915 &other_instance_paths, 916 intent.supported_state_schema_version(), 917 intent.application_id(), 918 ); 919 let wrong_application = ExistingServiceDatabaseIntent::new( 920 &paths, 921 intent.supported_state_schema_version(), 922 ServiceSqliteApplicationId::new(7).expect("other application"), 923 ); 924 for rejected in [&wrong_service, &wrong_instance, &wrong_application] { 925 assert_eq!( 926 verify_existing_database_intent(&mut connection, rejected) 927 .await 928 .expect_err("intent mismatch") 929 .kind(), 930 ServiceSqliteErrorKind::Metadata 931 ); 932 } 933 934 sqlx::query( 935 "UPDATE radroots_service_metadata SET state_schema_version = 2 WHERE singleton = 1", 936 ) 937 .execute(&mut connection) 938 .await 939 .expect("advance stored schema"); 940 let older_binary = ExistingServiceDatabaseIntent::new( 941 &paths, 942 NonZeroU32::new(1).expect("older ceiling"), 943 expected.application_id(), 944 ); 945 assert_eq!( 946 verify_existing_database_intent(&mut connection, &older_binary) 947 .await 948 .expect_err("newer stored schema") 949 .kind(), 950 ServiceSqliteErrorKind::Metadata 951 ); 952 } 953 954 #[cfg(any(target_os = "linux", target_os = "macos"))] 955 #[tokio::test(flavor = "current_thread")] 956 async fn schema_mismatch_rolls_back_shared_objects_metadata_and_application_id() { 957 let paths = sqlite_paths("myc", "primary"); 958 let expected = metadata(&paths, 7, 1, 1_700_000_000_000, 0x5244_5351); 959 let mut connection = memory_connection().await; 960 sqlx::query("CREATE TABLE unlisted_service_object (id INTEGER PRIMARY KEY)") 961 .execute(&mut connection) 962 .await 963 .expect("pre-existing service object"); 964 965 let error = write_database_metadata(&mut connection, &expected, &base_schema_catalog()) 966 .await 967 .expect_err("schema mismatch must roll back initialization transaction"); 968 assert_eq!(error.kind(), ServiceSqliteErrorKind::Integrity); 969 assert_eq!(read_application_id(&mut connection).await.unwrap(), 0); 970 assert_eq!( 971 sqlx::query_scalar::<_, i64>( 972 "SELECT COUNT(*) FROM sqlite_schema 973 WHERE name IN ( 974 'radroots_service_metadata', 975 'radroots_service_metadata_guard_update', 976 'radroots_service_metadata_no_delete', 977 'schema_migrations', 978 'schema_migrations_no_update', 979 'schema_migrations_no_delete' 980 )", 981 ) 982 .fetch_one(&mut connection) 983 .await 984 .expect("shared schema rollback evidence"), 985 0 986 ); 987 assert_eq!( 988 sqlx::query_scalar::<_, i64>( 989 "SELECT COUNT(*) FROM sqlite_schema 990 WHERE type = 'table' AND name = 'unlisted_service_object'", 991 ) 992 .fetch_one(&mut connection) 993 .await 994 .expect("pre-existing object evidence"), 995 1 996 ); 997 } 998 999 #[cfg(any(target_os = "linux", target_os = "macos"))] 1000 #[tokio::test(flavor = "current_thread")] 1001 async fn every_identity_dimension_must_match() { 1002 let paths = sqlite_paths("myc", "primary"); 1003 let expected = metadata(&paths, 7, 1, 1_700_000_000_000, 0x5244_5351); 1004 let mut connection = memory_connection().await; 1005 write_database_metadata(&mut connection, &expected, &base_schema_catalog()) 1006 .await 1007 .expect("write metadata"); 1008 1009 let alternatives = [ 1010 ServiceDatabaseIdentity::new( 1011 &sqlite_paths("rhi", "primary"), 1012 SourceGeneration::new([7; 32]).unwrap(), 1013 NonZeroU32::new(1).unwrap(), 1014 ServiceSqliteApplicationId::new(0x5244_5351).unwrap(), 1015 ), 1016 ServiceDatabaseIdentity::new( 1017 &sqlite_paths("myc", "secondary"), 1018 SourceGeneration::new([7; 32]).unwrap(), 1019 NonZeroU32::new(1).unwrap(), 1020 ServiceSqliteApplicationId::new(0x5244_5351).unwrap(), 1021 ), 1022 ServiceDatabaseIdentity::new( 1023 &paths, 1024 SourceGeneration::new([8; 32]).unwrap(), 1025 NonZeroU32::new(1).unwrap(), 1026 ServiceSqliteApplicationId::new(0x5244_5351).unwrap(), 1027 ), 1028 ServiceDatabaseIdentity::new( 1029 &paths, 1030 SourceGeneration::new([7; 32]).unwrap(), 1031 NonZeroU32::new(1).unwrap(), 1032 ServiceSqliteApplicationId::new(0x5244_5352).unwrap(), 1033 ), 1034 ]; 1035 for alternative in alternatives { 1036 assert_eq!( 1037 verify_database_metadata(&mut connection, &alternative) 1038 .await 1039 .expect_err("identity mismatch") 1040 .kind(), 1041 ServiceSqliteErrorKind::Metadata 1042 ); 1043 } 1044 1045 let newer_binary = ServiceDatabaseIdentity::new( 1046 &paths, 1047 expected.source_generation(), 1048 NonZeroU32::new(2).unwrap(), 1049 expected.application_id(), 1050 ); 1051 let stored = verify_database_metadata(&mut connection, &newer_binary) 1052 .await 1053 .expect("older schema is migration eligible"); 1054 assert_eq!(stored.created_at_unix_ms(), 1_700_000_000_000); 1055 assert_eq!(stored.state_schema_version().get(), 1); 1056 1057 let mut newer_state = memory_connection().await; 1058 let version_two = metadata(&paths, 7, 2, 1_700_000_000_001, 0x5244_5351); 1059 assert_eq!( 1060 write_database_metadata(&mut newer_state, &version_two, &base_schema_catalog()) 1061 .await 1062 .expect_err("fresh metadata must start at v1") 1063 .kind(), 1064 ServiceSqliteErrorKind::Metadata 1065 ); 1066 let version_one = metadata(&paths, 7, 1, 1_700_000_000_001, 0x5244_5351); 1067 write_database_metadata(&mut newer_state, &version_one, &base_schema_catalog()) 1068 .await 1069 .expect("write v1 metadata"); 1070 sqlx::query( 1071 "UPDATE radroots_service_metadata SET state_schema_version = 2 WHERE singleton = 1", 1072 ) 1073 .execute(&mut newer_state) 1074 .await 1075 .expect("advance newer state"); 1076 assert_eq!( 1077 verify_database_metadata(&mut newer_state, &expected.identity()) 1078 .await 1079 .expect_err("newer state must fail closed") 1080 .kind(), 1081 ServiceSqliteErrorKind::Metadata 1082 ); 1083 } 1084 1085 #[cfg(any(target_os = "linux", target_os = "macos"))] 1086 #[tokio::test(flavor = "current_thread")] 1087 async fn missing_duplicate_and_corrupt_metadata_fail_closed() { 1088 const PERMISSIVE_TABLE: &str = "CREATE TABLE radroots_service_metadata ( 1089 singleton, service_id, instance_id, source_generation, 1090 state_schema_version, created_at_unix_ms 1091 )"; 1092 let paths = sqlite_paths("myc", "primary"); 1093 let expected = metadata(&paths, 7, 1, 1_700_000_000_000, 0x5244_5351); 1094 1095 let mut missing_table = memory_connection().await; 1096 assert_eq!( 1097 verify_database_metadata(&mut missing_table, &expected.identity()) 1098 .await 1099 .expect_err("missing table") 1100 .kind(), 1101 ServiceSqliteErrorKind::Metadata 1102 ); 1103 1104 let corrupt_rows = [ 1105 "", 1106 "INSERT INTO radroots_service_metadata VALUES 1107 (1, 'myc', 'primary', randomblob(32), 1, 1700000000000), 1108 (2, 'myc', 'primary', randomblob(32), 1, 1700000000000)", 1109 "INSERT INTO radroots_service_metadata VALUES 1110 (1, NULL, 'primary', randomblob(32), 1, 1700000000000)", 1111 "INSERT INTO radroots_service_metadata VALUES 1112 (1, 'Myc', 'primary', randomblob(32), 1, 1700000000000)", 1113 "INSERT INTO radroots_service_metadata VALUES 1114 (1, 'myc', 'Primary', randomblob(32), 1, 1700000000000)", 1115 "INSERT INTO radroots_service_metadata VALUES 1116 (1, 'myc', 'primary', zeroblob(31), 1, 1700000000000)", 1117 "INSERT INTO radroots_service_metadata VALUES 1118 (1, 'myc', 'primary', zeroblob(32), 1, 1700000000000)", 1119 "INSERT INTO radroots_service_metadata VALUES 1120 (1, 'myc', 'primary', randomblob(33), 1, 1700000000000)", 1121 "INSERT INTO radroots_service_metadata VALUES 1122 (1, 'myc', 'primary', randomblob(32), 0, 1700000000000)", 1123 "INSERT INTO radroots_service_metadata VALUES 1124 (1, 'myc', 'primary', randomblob(32), 1, 0)", 1125 "INSERT INTO radroots_service_metadata VALUES 1126 ('1', 'myc', 'primary', randomblob(32), 1, 1700000000000)", 1127 "INSERT INTO radroots_service_metadata VALUES 1128 (1, 7, 'primary', randomblob(32), 1, 1700000000000)", 1129 "INSERT INTO radroots_service_metadata VALUES 1130 (1, 'myc', 7, randomblob(32), 1, 1700000000000)", 1131 "INSERT INTO radroots_service_metadata VALUES 1132 (1, 'myc', 'primary', 'not-a-generation', 1, 1700000000000)", 1133 "INSERT INTO radroots_service_metadata VALUES 1134 (1, 'myc', 'primary', randomblob(32), '1', 1700000000000)", 1135 "INSERT INTO radroots_service_metadata VALUES 1136 (1, 'myc', 'primary', randomblob(32), 1, '1700000000000')", 1137 "INSERT INTO radroots_service_metadata VALUES 1138 (1, 'myc', 'primary', randomblob(32), -1, 1700000000000)", 1139 "INSERT INTO radroots_service_metadata VALUES 1140 (1, 'myc', 'primary', randomblob(32), 4294967296, 1700000000000)", 1141 "INSERT INTO radroots_service_metadata VALUES 1142 (1, 'myc', 'primary', randomblob(32), 1, -1)", 1143 ]; 1144 for corrupt_row in corrupt_rows { 1145 let mut connection = memory_connection().await; 1146 sqlx::raw_sql(PERMISSIVE_TABLE) 1147 .execute(&mut connection) 1148 .await 1149 .expect("permissive metadata table"); 1150 sqlx::query("PRAGMA application_id = 1380209489") 1151 .execute(&mut connection) 1152 .await 1153 .expect("application ID"); 1154 if !corrupt_row.is_empty() { 1155 sqlx::raw_sql(corrupt_row) 1156 .execute(&mut connection) 1157 .await 1158 .expect("corrupt metadata row"); 1159 } 1160 assert_eq!( 1161 verify_database_metadata(&mut connection, &expected.identity()) 1162 .await 1163 .expect_err("corrupt metadata") 1164 .kind(), 1165 ServiceSqliteErrorKind::Metadata, 1166 "accepted corrupt fixture `{corrupt_row}`" 1167 ); 1168 } 1169 1170 let oversized_text = "a".repeat(4 * 1024 * 1024); 1171 for column in ["service_id", "instance_id"] { 1172 let mut connection = memory_connection().await; 1173 sqlx::raw_sql(PERMISSIVE_TABLE) 1174 .execute(&mut connection) 1175 .await 1176 .expect("permissive metadata table"); 1177 sqlx::query("PRAGMA application_id = 1380209489") 1178 .execute(&mut connection) 1179 .await 1180 .expect("application ID"); 1181 sqlx::raw_sql( 1182 "INSERT INTO radroots_service_metadata VALUES 1183 (1, 'myc', 'primary', randomblob(32), 1, 1700000000000)", 1184 ) 1185 .execute(&mut connection) 1186 .await 1187 .expect("metadata row"); 1188 let statement = match column { 1189 "service_id" => "UPDATE radroots_service_metadata SET service_id = ?", 1190 "instance_id" => "UPDATE radroots_service_metadata SET instance_id = ?", 1191 _ => unreachable!("fixed oversized identifier column inventory"), 1192 }; 1193 sqlx::query(statement) 1194 .bind(&oversized_text) 1195 .execute(&mut connection) 1196 .await 1197 .expect("oversized persisted identifier"); 1198 assert_eq!( 1199 verify_database_metadata(&mut connection, &expected.identity()) 1200 .await 1201 .expect_err("oversized identifier must fail before decode") 1202 .kind(), 1203 ServiceSqliteErrorKind::Metadata 1204 ); 1205 } 1206 1207 let mut oversized_generation = memory_connection().await; 1208 sqlx::raw_sql(PERMISSIVE_TABLE) 1209 .execute(&mut oversized_generation) 1210 .await 1211 .expect("permissive metadata table"); 1212 sqlx::query("PRAGMA application_id = 1380209489") 1213 .execute(&mut oversized_generation) 1214 .await 1215 .expect("application ID"); 1216 sqlx::query( 1217 "INSERT INTO radroots_service_metadata VALUES 1218 (1, 'myc', 'primary', zeroblob(4194304), 1, 1700000000000)", 1219 ) 1220 .execute(&mut oversized_generation) 1221 .await 1222 .expect("oversized persisted generation"); 1223 assert_eq!( 1224 verify_database_metadata(&mut oversized_generation, &expected.identity()) 1225 .await 1226 .expect_err("oversized generation must fail before decode") 1227 .kind(), 1228 ServiceSqliteErrorKind::Metadata 1229 ); 1230 1231 for (application_id, statement) in [ 1232 (0_i64, "PRAGMA application_id = 0"), 1233 (-1, "PRAGMA application_id = -1"), 1234 ] { 1235 let mut connection = memory_connection().await; 1236 sqlx::raw_sql(PERMISSIVE_TABLE) 1237 .execute(&mut connection) 1238 .await 1239 .expect("permissive metadata table"); 1240 sqlx::query(statement) 1241 .execute(&mut connection) 1242 .await 1243 .expect("invalid application ID fixture"); 1244 sqlx::query( 1245 "INSERT INTO radroots_service_metadata VALUES 1246 (1, 'myc', 'primary', randomblob(32), 1, 1700000000000)", 1247 ) 1248 .execute(&mut connection) 1249 .await 1250 .expect("otherwise valid metadata row"); 1251 assert_eq!( 1252 verify_database_metadata(&mut connection, &expected.identity()) 1253 .await 1254 .expect_err("invalid application ID") 1255 .kind(), 1256 ServiceSqliteErrorKind::Metadata, 1257 "accepted application ID {application_id}" 1258 ); 1259 } 1260 } 1261 1262 #[cfg(any(target_os = "linux", target_os = "macos"))] 1263 #[test] 1264 fn verified_backup_metadata_rejects_invalid_creation_time() { 1265 let service = ServiceId::new("myc").expect("service"); 1266 let instance = InstanceId::new("primary").expect("instance"); 1267 let generation = SourceGeneration::new([7; 32]).expect("generation"); 1268 let schema = NonZeroU32::new(1).expect("schema"); 1269 let application = ServiceSqliteApplicationId::new(0x5244_5351).expect("application"); 1270 assert!( 1271 ServiceDatabaseMetadata::from_verified_backup( 1272 service.clone(), 1273 instance.clone(), 1274 generation, 1275 schema, 1276 0, 1277 application, 1278 ) 1279 .is_err() 1280 ); 1281 assert!( 1282 ServiceDatabaseMetadata::from_verified_backup( 1283 service, 1284 instance, 1285 generation, 1286 schema, 1287 i64::MAX as u64 + 1, 1288 application, 1289 ) 1290 .is_err() 1291 ); 1292 } 1293 }