#![warn(missing_docs)]
use std::path::Path;
use cortex_core::{CoreError, CoreResult};
use rusqlite::{Connection, OpenFlags};
pub mod migrate;
pub mod migrate_v2;
pub mod mirror;
pub mod proof;
pub mod repo;
pub mod semantic_diff;
pub mod verify;
pub use proof::{temporal_authority_proof_report, verify_memory_proof_closure};
pub use semantic_diff::{
semantic_snapshot_from_store, ArtifactKind, ContradictionState, DoctrineForce, DoctrineState,
KeyLifecycleState, KeyState, MemoryKind, MemoryState, PrincipalTrustState, PrincipleState,
ProofState as SemanticProofState, RestoreDecision, RuntimeMode as SemanticRuntimeMode,
SalienceDistribution, SalienceScore, SemanticChange, SemanticChangeKind, SemanticDiff,
SemanticSeverity, SemanticSnapshot, TrustKeyState, TrustTier, TruthCeiling, TruthCeilingState,
TruthState,
};
pub const INITIAL_MIGRATION_SQL: &str = include_str!("../migrations/001_init.sql");
pub type Pool = Connection;
pub type StoreResult<T> = Result<T, StoreError>;
#[derive(Debug, thiserror::Error)]
pub enum StoreError {
#[error("sqlite error: {0}")]
Sqlite(#[from] rusqlite::Error),
#[error("json error: {0}")]
Json(#[from] serde_json::Error),
#[error("time parse error: {0}")]
Time(#[from] chrono::ParseError),
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("core error: {0}")]
Core(#[from] CoreError),
#[error("validation failed: {0}")]
Validation(String),
#[error(
"verify.row_counts.checked_add_overflow: table {table} pre-migrate row count {pre} plus boundary-append delta {delta} overflows u64"
)]
RowCountCheckedAddOverflow {
table: &'static str,
pre: u64,
delta: u64,
},
}
impl StoreError {
#[must_use]
pub fn invariant(&self) -> Option<&'static str> {
match self {
StoreError::RowCountCheckedAddOverflow { .. } => {
Some(VERIFY_ROW_COUNTS_CHECKED_ADD_OVERFLOW_INVARIANT)
}
_ => None,
}
}
}
pub const VERIFY_ROW_COUNTS_CHECKED_ADD_OVERFLOW_INVARIANT: &str =
"verify.row_counts.checked_add_overflow";
pub fn open(data_dir: &Path) -> StoreResult<Pool> {
std::fs::create_dir_all(data_dir)?;
let pool = Connection::open(data_dir.join("cortex.sqlite3"))?;
verify_sqlite_load_extension_disabled(&pool)?;
migrate::apply_pending(&pool)?;
Ok(pool)
}
pub fn sqlite_compile_options(pool: &Pool) -> StoreResult<Vec<String>> {
let mut stmt = pool.prepare("PRAGMA compile_options;")?;
let options = stmt
.query_map([], |row| row.get(0))?
.collect::<Result<Vec<String>, _>>()?;
Ok(options)
}
pub fn verify_sqlite_compile_options<I, S>(compile_options: I) -> StoreResult<()>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
for option in compile_options {
if is_load_extension_enabled_compile_option(option.as_ref()) {
return Err(StoreError::Validation(format!(
"sqlite compile option {option} is forbidden; Cortex refuses SQLite builds with loadable extension support",
option = option.as_ref()
)));
}
}
Ok(())
}
fn is_load_extension_enabled_compile_option(option: &str) -> bool {
let normalized = option
.trim()
.strip_prefix("SQLITE_")
.unwrap_or(option.trim());
let key = normalized
.split_once('=')
.map_or(normalized, |(key, _)| key);
key.eq_ignore_ascii_case("ENABLE_LOAD_EXTENSION")
}
pub fn verify_sqlite_load_extension_disabled(pool: &Pool) -> StoreResult<()> {
match pool.query_row(
"SELECT load_extension('cortex_extension_loading_must_remain_disabled');",
[],
|row| row.get::<_, String>(0),
) {
Ok(_) => Err(StoreError::Validation(
"sqlite load_extension unexpectedly executed".to_string(),
)),
Err(err) if sqlite_load_extension_is_disabled_error(&err) => Ok(()),
Err(err) => Err(StoreError::Validation(format!(
"sqlite load_extension preflight returned an unexpected error: {err}"
))),
}
}
fn sqlite_load_extension_is_disabled_error(err: &rusqlite::Error) -> bool {
let message = err.to_string().to_ascii_lowercase();
message.contains("not authorized") || message.contains("no such function: load_extension")
}
pub fn open_existing_readonly(path: &Path) -> StoreResult<Pool> {
Ok(Connection::open_with_flags(
path,
OpenFlags::SQLITE_OPEN_READ_ONLY,
)?)
}
pub fn open_stub(data_dir: &Path) -> CoreResult<()> {
open(data_dir).map_err(|err| CoreError::Validation(err.to_string()))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn initial_migration_runs_cleanly_on_empty_db() {
let pool = Connection::open_in_memory().expect("open in-memory sqlite");
pool.execute_batch(INITIAL_MIGRATION_SQL)
.expect("initial migration runs");
let mut stmt = pool
.prepare(
"SELECT name FROM sqlite_master \
WHERE type = 'table' \
ORDER BY name;",
)
.expect("prepare table query");
let actual: Vec<String> = stmt
.query_map([], |row| row.get(0))
.expect("list tables")
.collect::<Result<_, _>>()
.expect("read table rows");
let expected = [
"audit_records",
"context_packs",
"contradictions",
"doctrine",
"episodes",
"events",
"memories",
"principles",
"trace_events",
"traces",
];
assert_eq!(actual, expected);
}
}