use std::fs;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use kmp_domain::PortError;
pub const SUPPORTED_FORMAT_VERSION: u32 = StorageEngine::Sqlite.format_version();
pub const EVENT_FORMAT_VERSION: u32 = 1;
const FORMAT_VERSION_FILE: &str = "FORMAT_VERSION";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StorageEngine {
Sqlite,
}
impl StorageEngine {
pub const fn format_version(self) -> u32 {
match self {
StorageEngine::Sqlite => 2,
}
}
pub(crate) const NEWEST_KNOWN_FORMAT_VERSION: u32 = StorageEngine::Sqlite.format_version();
pub(crate) const fn from_format_version(version: u32) -> Option<Self> {
match version {
2 => Some(StorageEngine::Sqlite),
_ => None,
}
}
pub const fn name(self) -> &'static str {
match self {
StorageEngine::Sqlite => "sqlite",
}
}
const fn store_file_name(self) -> &'static str {
match self {
StorageEngine::Sqlite => "kernel.sqlite3",
}
}
}
impl std::fmt::Display for StorageEngine {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.name())
}
}
pub fn format_version_path(data_dir: &Path) -> PathBuf {
data_dir.join(FORMAT_VERSION_FILE)
}
pub fn read_stamped_version(data_dir: &Path) -> Result<u32, PortError> {
let version_path = format_version_path(data_dir);
let raw = fs::read_to_string(&version_path).map_err(|error| {
PortError::Unavailable(format!(
"could not read FORMAT_VERSION at `{}`: {error}",
version_path.display()
))
})?;
raw.trim().parse().map_err(|_| {
PortError::InvalidState(format!(
"FORMAT_VERSION at `{}` is corrupt (`{}`)",
version_path.display(),
raw.trim()
))
})
}
pub fn store_file_path_for(data_dir: &Path, engine: StorageEngine) -> PathBuf {
data_dir.join("store").join(engine.store_file_name())
}
fn any_store_file_exists(data_dir: &Path) -> bool {
fs::read_dir(data_dir.join("store"))
.is_ok_and(|entries| entries.flatten().any(|entry| entry.path().is_file()))
}
fn unsupported_store_files(data_dir: &Path) -> Vec<PathBuf> {
let sqlite = store_file_path_for(data_dir, StorageEngine::Sqlite);
let wal = sqlite.with_file_name("kernel.sqlite3-wal");
let shm = sqlite.with_file_name("kernel.sqlite3-shm");
let rollback_journal = sqlite.with_file_name("kernel.sqlite3-journal");
let mut paths = fs::read_dir(data_dir.join("store"))
.into_iter()
.flatten()
.flatten()
.map(|entry| entry.path())
.filter(|path| {
path.is_file()
&& path != &sqlite
&& path != &wal
&& path != &shm
&& path != &rollback_journal
})
.collect::<Vec<_>>();
paths.sort();
paths
}
pub fn validate_store_layout(data_dir: &Path) -> Result<Option<StorageEngine>, PortError> {
let version_path = format_version_path(data_dir);
match fs::read_to_string(&version_path) {
Ok(raw) => {
let version: u32 = raw.trim().parse().map_err(|_| {
PortError::InvalidState(format!(
"embedded store at `{}` has a corrupt FORMAT_VERSION (`{}`); refusing to open",
data_dir.display(),
raw.trim()
))
})?;
let stamped = resolve_stamped(data_dir, version)?;
let unsupported = unsupported_store_files(data_dir);
if !unsupported.is_empty() {
return Err(PortError::InvalidState(format!(
"embedded store at `{}` says format version {} ({stamped}), but `store/` contains unsupported storage artifacts: {}; refusing to open memory under an unknown layout",
data_dir.display(),
stamped.format_version(),
unsupported
.iter()
.map(|path| path.display().to_string())
.collect::<Vec<_>>()
.join(", ")
)));
}
Ok(Some(stamped))
}
Err(error) if error.kind() == ErrorKind::NotFound => {
if any_store_file_exists(data_dir) {
return Err(PortError::InvalidState(format!(
"embedded store at `{}` has a store file but no FORMAT_VERSION; the data \
directory layout is corrupt, refusing to open",
data_dir.display()
)));
}
Ok(None)
}
Err(error) => Err(PortError::Unavailable(format!(
"embedded store could not read FORMAT_VERSION at `{}`: {error}",
version_path.display()
))),
}
}
pub(crate) fn existing_store_file(data_dir: &Path) -> Option<(StorageEngine, PathBuf)> {
let version = read_stamped_version(data_dir).ok()?;
let engine = StorageEngine::from_format_version(version)?;
let path = store_file_path_for(data_dir, engine);
path.exists().then_some((engine, path))
}
pub(crate) fn check_or_stamp(data_dir: &Path) -> Result<StorageEngine, PortError> {
check_or_stamp_as(data_dir, None)
}
pub(crate) fn check_or_stamp_as(
data_dir: &Path,
wanted: Option<StorageEngine>,
) -> Result<StorageEngine, PortError> {
match validate_store_layout(data_dir)? {
Some(stamped) => {
if let Some(wanted) = wanted
&& wanted != stamped
{
return Err(PortError::InvalidState(format!(
"embedded store at `{}` is a {stamped} store (format version {}), not {wanted}; \
a store is never reopened under another layout; unset the engine selector \
to open the stamped SQLite layout",
data_dir.display(),
stamped.format_version()
)));
}
Ok(stamped)
}
None => {
let engine = wanted.unwrap_or(StorageEngine::Sqlite);
let version_path = format_version_path(data_dir);
fs::write(&version_path, format!("{}\n", engine.format_version())).map_err(
|error| {
PortError::Unavailable(format!(
"embedded store could not stamp FORMAT_VERSION at `{}`: {error}",
version_path.display()
))
},
)?;
Ok(engine)
}
}
}
fn resolve_stamped(data_dir: &Path, version: u32) -> Result<StorageEngine, PortError> {
if version > StorageEngine::NEWEST_KNOWN_FORMAT_VERSION {
return Err(PortError::InvalidState(format!(
"embedded store at `{}` uses format version {version}, newer than this \
binary supports ({}); upgrade the binary",
data_dir.display(),
StorageEngine::NEWEST_KNOWN_FORMAT_VERSION
)));
}
if version < SUPPORTED_FORMAT_VERSION {
return Err(PortError::InvalidState(format!(
"embedded store at `{}` uses unsupported format version {version}; current KMP \
opens format {SUPPORTED_FORMAT_VERSION} only and left the directory untouched. \
Preserve the source, use an explicitly archived compatible exporter to create \
`.kmp/memory.jsonl`, then import that bundle into an empty current store",
data_dir.display(),
)));
}
StorageEngine::from_format_version(version).ok_or_else(|| {
PortError::InvalidState(format!(
"embedded store at `{}` uses unsupported format version {version}; this binary \
only opens format {SUPPORTED_FORMAT_VERSION} and left the store untouched",
data_dir.display()
))
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fresh_directory_is_stamped_with_supported_version() {
let dir = tempfile::tempdir().expect("tempdir");
let engine = check_or_stamp(dir.path()).expect("fresh directory should stamp");
assert_eq!(engine, StorageEngine::Sqlite);
let stamped = fs::read_to_string(format_version_path(dir.path())).expect("read stamp");
assert_eq!(stamped.trim(), SUPPORTED_FORMAT_VERSION.to_string());
check_or_stamp(dir.path()).expect("stamped directory should reopen");
}
#[test]
fn newer_format_version_fails_fast() {
let dir = tempfile::tempdir().expect("tempdir");
fs::write(format_version_path(dir.path()), "999\n").expect("write");
let error = check_or_stamp(dir.path()).expect_err("newer version must fail");
assert!(error.to_string().contains("upgrade the binary"));
}
#[test]
fn unknown_older_format_version_is_rejected_untouched() {
let dir = tempfile::tempdir().expect("tempdir");
fs::write(format_version_path(dir.path()), "0\n").expect("write");
let error = check_or_stamp(dir.path()).expect_err("older version must fail");
let message = error.to_string();
assert!(
message.contains("unsupported format version 0"),
"{message}"
);
assert!(
message.contains("left the directory untouched"),
"{message}"
);
}
#[test]
fn corrupt_version_content_fails_fast() {
let dir = tempfile::tempdir().expect("tempdir");
fs::write(format_version_path(dir.path()), "not-a-number\n").expect("write");
let error = check_or_stamp(dir.path()).expect_err("corrupt version must fail");
assert!(error.to_string().contains("corrupt FORMAT_VERSION"));
}
#[test]
fn store_without_version_stamp_is_a_corrupt_layout() {
let dir = tempfile::tempdir().expect("tempdir");
let store = dir.path().join("store/unknown-store.bin");
fs::create_dir_all(store.parent().expect("parent")).expect("mkdir");
fs::write(&store, b"stub").expect("write store stub");
let error = check_or_stamp(dir.path()).expect_err("missing stamp must fail");
assert!(error.to_string().contains("corrupt"));
}
#[test]
fn diagnostics_can_apply_the_open_gate_without_stamping_a_fresh_directory() {
let fresh = tempfile::tempdir().expect("tempdir");
assert_eq!(
validate_store_layout(fresh.path()).expect("fresh layout is valid"),
None
);
assert!(!format_version_path(fresh.path()).exists());
let invalid = tempfile::tempdir().expect("tempdir");
let store = store_file_path_for(invalid.path(), StorageEngine::Sqlite);
fs::create_dir_all(store.parent().expect("parent")).expect("mkdir");
fs::write(&store, b"memory remains here").expect("store marker");
for stamp in [Some("3\n"), Some("banana\n"), None] {
match stamp {
Some(stamp) => fs::write(format_version_path(invalid.path()), stamp)
.expect("write invalid stamp"),
None => fs::remove_file(format_version_path(invalid.path())).expect("remove stamp"),
}
let error = validate_store_layout(invalid.path())
.expect_err("the same gate as real open must refuse this layout");
let message = error.to_string();
assert!(
message.contains("upgrade the binary")
|| message.contains("corrupt FORMAT_VERSION")
|| message.contains("store file but no FORMAT_VERSION"),
"{message}"
);
assert!(
store.exists(),
"the read-only probe preserves the memory file"
);
}
}
#[test]
fn a_stamp_cannot_hide_an_unsupported_storage_artifact() {
let dir = tempfile::tempdir().expect("tempdir");
fs::write(format_version_path(dir.path()), "2\n").expect("sqlite stamp");
let unsupported = dir.path().join("store/retired-layout.bin");
fs::create_dir_all(unsupported.parent().expect("parent")).expect("mkdir");
fs::write(unsupported, b"legacy memory").expect("legacy marker");
let error = check_or_stamp(dir.path()).expect_err("mismatched engine must fail");
assert!(
error.to_string().contains("unsupported storage artifacts"),
"{error}"
);
}
#[test]
fn a_transient_sqlite_rollback_journal_is_part_of_the_supported_layout() {
let dir = tempfile::tempdir().expect("tempdir");
fs::write(format_version_path(dir.path()), "2\n").expect("sqlite stamp");
let journal = dir.path().join("store/kernel.sqlite3-journal");
fs::create_dir_all(journal.parent().expect("parent")).expect("mkdir");
fs::write(&journal, b"startup in progress").expect("journal marker");
assert_eq!(
validate_store_layout(dir.path()).expect("SQLite journal is recognized"),
Some(StorageEngine::Sqlite)
);
assert!(journal.exists(), "validation is read-only");
}
#[test]
fn a_format_one_store_is_rejected_without_being_opened() {
let dir = tempfile::tempdir().expect("tempdir");
fs::write(format_version_path(dir.path()), "1\n").expect("legacy stamp");
let store = dir.path().join("store/retired-layout.bin");
fs::create_dir_all(store.parent().expect("parent")).expect("store dir");
fs::write(&store, b"legacy bytes").expect("legacy bytes");
let error = check_or_stamp(dir.path()).expect_err("format 1 must not open");
let message = error.to_string();
assert!(
message.contains("unsupported format version 1"),
"{message}"
);
assert!(
message.contains("archived compatible exporter"),
"{message}"
);
assert_eq!(fs::read(&store).expect("source remains"), b"legacy bytes");
}
#[test]
fn sqlite_layout_is_always_available() {
let dir = tempfile::tempdir().expect("tempdir");
fs::write(format_version_path(dir.path()), "2\n").expect("write");
assert_eq!(
check_or_stamp(dir.path()).expect("sqlite is always compiled in"),
StorageEngine::Sqlite
);
}
}