use anyhow::{Context, Result};
use redb::{Database, DatabaseError};
use std::path::{Path, PathBuf};
pub(crate) fn open_corpus_db_or_recreate(path: &Path, cache_bytes: usize) -> Result<Database> {
match Database::builder().set_cache_size(cache_bytes).create(path) {
Ok(db) => Ok(db),
Err(e) if is_incompatible_corpus_format(&e) => {
let backup = backup_incompatible_corpus(path).with_context(|| {
format!(
"back up incompatible-format redb corpus {} before recreating",
path.display()
)
})?;
tracing::error!(
path = %path.display(),
backup = %backup.display(),
error = %e,
"corpus redb is in an incompatible/old format (redb 2.x); moved it aside and \
creating a fresh empty corpus — this index will be reindexed, NOT reported as \
a populated/ready corpus"
);
Database::builder()
.set_cache_size(cache_bytes)
.create(path)
.with_context(|| {
format!(
"create fresh redb corpus at {} after moving incompatible file aside",
path.display()
)
})
}
Err(e) => Err(anyhow::Error::new(e))
.with_context(|| format!("open redb corpus at {}", path.display())),
}
}
pub(crate) const INCOMPATIBLE_CORPUS_SUFFIX: &str = ".v2-incompatible";
pub(crate) fn is_incompatible_corpus_format(err: &DatabaseError) -> bool {
use redb::StorageError;
match err {
DatabaseError::UpgradeRequired(_) | DatabaseError::RepairAborted => true,
DatabaseError::Storage(StorageError::Corrupted(_)) => true,
DatabaseError::Storage(StorageError::Io(io)) => {
io.kind() == std::io::ErrorKind::InvalidData
}
_ => false,
}
}
pub(crate) fn backup_incompatible_corpus(path: &Path) -> std::io::Result<PathBuf> {
let mut base = path.as_os_str().to_os_string();
base.push(INCOMPATIBLE_CORPUS_SUFFIX);
let mut backup = PathBuf::from(base);
if backup.exists() {
for n in 1..u32::MAX {
let mut s = path.as_os_str().to_os_string();
s.push(INCOMPATIBLE_CORPUS_SUFFIX);
s.push(format!(".{n}"));
let candidate = PathBuf::from(s);
if !candidate.exists() {
backup = candidate;
break;
}
}
}
std::fs::rename(path, &backup)?;
Ok(backup)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn classifies_incompatible_corpus_format() {
use redb::StorageError;
assert!(is_incompatible_corpus_format(
&DatabaseError::UpgradeRequired(2)
));
assert!(is_incompatible_corpus_format(&DatabaseError::RepairAborted));
assert!(is_incompatible_corpus_format(&DatabaseError::Storage(
StorageError::Corrupted("x".into())
)));
let invalid = std::io::Error::new(std::io::ErrorKind::InvalidData, "not redb");
assert!(is_incompatible_corpus_format(&DatabaseError::Storage(
StorageError::Io(invalid)
)));
assert!(!is_incompatible_corpus_format(
&DatabaseError::DatabaseAlreadyOpen
));
let denied = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "nope");
assert!(!is_incompatible_corpus_format(&DatabaseError::Storage(
StorageError::Io(denied)
)));
}
#[test]
fn backup_renames_with_suffix() {
let dir = tempdir().unwrap();
let path = dir.path().join("index.redb");
std::fs::write(&path, b"old corpus bytes").unwrap();
let backup = backup_incompatible_corpus(&path).expect("backup");
assert!(backup
.to_string_lossy()
.ends_with(INCOMPATIBLE_CORPUS_SUFFIX));
assert!(backup.exists());
assert!(
!path.exists(),
"original path should be freed for a fresh corpus"
);
assert_eq!(std::fs::read(&backup).unwrap(), b"old corpus bytes");
}
#[test]
fn backup_path_avoids_clobber() {
let dir = tempdir().unwrap();
let path = dir.path().join("index.redb");
std::fs::write(&path, b"second").unwrap();
let mut first = path.as_os_str().to_os_string();
first.push(INCOMPATIBLE_CORPUS_SUFFIX);
std::fs::write(PathBuf::from(&first), b"first").unwrap();
let backup = backup_incompatible_corpus(&path).expect("backup");
assert!(backup.to_string_lossy().ends_with(".1"));
}
#[test]
fn incompatible_corpus_is_backed_up_and_recreated() {
use crate::core::corpus::CorpusStore;
use std::io::Write;
let dir = tempdir().unwrap();
let path = dir.path().join("index.redb");
std::fs::File::create(&path)
.and_then(|mut f| f.write_all(&[0xABu8; 4096]))
.unwrap();
let store = CorpusStore::open(&path).expect("incompatible corpus must recover, not error");
assert!(
path.with_file_name("index.redb.v2-incompatible").exists(),
"incompatible corpus file must be backed up"
);
assert_eq!(
store.chunk_count().unwrap(),
0,
"recreated corpus must be empty so warm-boot reindexes it"
);
}
}