1#![warn(missing_docs)]
3
4use std::path::Path;
5
6use cortex_core::{CoreError, CoreResult};
7use rusqlite::{Connection, OpenFlags};
8
9pub mod migrate;
10pub mod migrate_v2;
11pub mod mirror;
12pub mod proof;
13pub mod repo;
14pub mod semantic_diff;
15pub mod verify;
16
17pub use proof::{temporal_authority_proof_report, verify_memory_proof_closure};
18pub use semantic_diff::{
19 semantic_snapshot_from_store, ArtifactKind, ContradictionState, DoctrineForce, DoctrineState,
20 KeyLifecycleState, KeyState, MemoryKind, MemoryState, PrincipalTrustState, PrincipleState,
21 ProofState as SemanticProofState, RestoreDecision, RuntimeMode as SemanticRuntimeMode,
22 SalienceDistribution, SalienceScore, SemanticChange, SemanticChangeKind, SemanticDiff,
23 SemanticSeverity, SemanticSnapshot, TrustKeyState, TrustTier, TruthCeiling, TruthCeilingState,
24 TruthState,
25};
26
27pub const INITIAL_MIGRATION_SQL: &str = include_str!("../migrations/001_init.sql");
29
30pub type Pool = Connection;
32
33pub type StoreResult<T> = Result<T, StoreError>;
35
36#[derive(Debug, thiserror::Error)]
38pub enum StoreError {
39 #[error("sqlite error: {0}")]
41 Sqlite(#[from] rusqlite::Error),
42 #[error("json error: {0}")]
44 Json(#[from] serde_json::Error),
45 #[error("time parse error: {0}")]
47 Time(#[from] chrono::ParseError),
48 #[error("io error: {0}")]
50 Io(#[from] std::io::Error),
51 #[error("core error: {0}")]
53 Core(#[from] CoreError),
54 #[error("validation failed: {0}")]
56 Validation(String),
57 #[error(
65 "verify.row_counts.checked_add_overflow: table {table} pre-migrate row count {pre} plus boundary-append delta {delta} overflows u64"
66 )]
67 RowCountCheckedAddOverflow {
68 table: &'static str,
70 pre: u64,
72 delta: u64,
74 },
75}
76
77impl StoreError {
78 #[must_use]
82 pub fn invariant(&self) -> Option<&'static str> {
83 match self {
84 StoreError::RowCountCheckedAddOverflow { .. } => {
85 Some(VERIFY_ROW_COUNTS_CHECKED_ADD_OVERFLOW_INVARIANT)
86 }
87 _ => None,
88 }
89 }
90}
91
92pub const VERIFY_ROW_COUNTS_CHECKED_ADD_OVERFLOW_INVARIANT: &str =
100 "verify.row_counts.checked_add_overflow";
101
102pub fn open(data_dir: &Path) -> StoreResult<Pool> {
104 std::fs::create_dir_all(data_dir)?;
105 let pool = Connection::open(data_dir.join("cortex.sqlite3"))?;
106 verify_sqlite_load_extension_disabled(&pool)?;
107 migrate::apply_pending(&pool)?;
108 Ok(pool)
109}
110
111pub fn sqlite_compile_options(pool: &Pool) -> StoreResult<Vec<String>> {
113 let mut stmt = pool.prepare("PRAGMA compile_options;")?;
114 let options = stmt
115 .query_map([], |row| row.get(0))?
116 .collect::<Result<Vec<String>, _>>()?;
117 Ok(options)
118}
119
120pub fn verify_sqlite_compile_options<I, S>(compile_options: I) -> StoreResult<()>
122where
123 I: IntoIterator<Item = S>,
124 S: AsRef<str>,
125{
126 for option in compile_options {
127 if is_load_extension_enabled_compile_option(option.as_ref()) {
128 return Err(StoreError::Validation(format!(
129 "sqlite compile option {option} is forbidden; Cortex refuses SQLite builds with loadable extension support",
130 option = option.as_ref()
131 )));
132 }
133 }
134 Ok(())
135}
136
137fn is_load_extension_enabled_compile_option(option: &str) -> bool {
138 let normalized = option
139 .trim()
140 .strip_prefix("SQLITE_")
141 .unwrap_or(option.trim());
142 let key = normalized
143 .split_once('=')
144 .map_or(normalized, |(key, _)| key);
145 key.eq_ignore_ascii_case("ENABLE_LOAD_EXTENSION")
146}
147
148pub fn verify_sqlite_load_extension_disabled(pool: &Pool) -> StoreResult<()> {
156 match pool.query_row(
157 "SELECT load_extension('cortex_extension_loading_must_remain_disabled');",
158 [],
159 |row| row.get::<_, String>(0),
160 ) {
161 Ok(_) => Err(StoreError::Validation(
162 "sqlite load_extension unexpectedly executed".to_string(),
163 )),
164 Err(err) if sqlite_load_extension_is_disabled_error(&err) => Ok(()),
165 Err(err) => Err(StoreError::Validation(format!(
166 "sqlite load_extension preflight returned an unexpected error: {err}"
167 ))),
168 }
169}
170
171fn sqlite_load_extension_is_disabled_error(err: &rusqlite::Error) -> bool {
172 let message = err.to_string().to_ascii_lowercase();
173 message.contains("not authorized") || message.contains("no such function: load_extension")
174}
175
176pub fn open_existing_readonly(path: &Path) -> StoreResult<Pool> {
181 Ok(Connection::open_with_flags(
182 path,
183 OpenFlags::SQLITE_OPEN_READ_ONLY,
184 )?)
185}
186
187pub fn open_stub(data_dir: &Path) -> CoreResult<()> {
189 open(data_dir).map_err(|err| CoreError::Validation(err.to_string()))?;
190 Ok(())
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
198 fn initial_migration_runs_cleanly_on_empty_db() {
199 let pool = Connection::open_in_memory().expect("open in-memory sqlite");
200 pool.execute_batch(INITIAL_MIGRATION_SQL)
201 .expect("initial migration runs");
202
203 let mut stmt = pool
204 .prepare(
205 "SELECT name FROM sqlite_master \
206 WHERE type = 'table' \
207 ORDER BY name;",
208 )
209 .expect("prepare table query");
210 let actual: Vec<String> = stmt
211 .query_map([], |row| row.get(0))
212 .expect("list tables")
213 .collect::<Result<_, _>>()
214 .expect("read table rows");
215 let expected = [
216 "audit_records",
217 "context_packs",
218 "contradictions",
219 "doctrine",
220 "episodes",
221 "events",
222 "memories",
223 "principles",
224 "trace_events",
225 "traces",
226 ];
227 assert_eq!(actual, expected);
228 }
229}