use redb::{Database, DatabaseError};
use std::path::{Path, PathBuf};
pub const INCOMPATIBLE_SUFFIX: &str = ".v2-incompatible";
pub fn is_incompatible_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 fn incompatible_backup_path(path: &Path) -> PathBuf {
let base = {
let mut s = path.as_os_str().to_os_string();
s.push(INCOMPATIBLE_SUFFIX);
PathBuf::from(s)
};
if !base.exists() {
return base;
}
for n in 1..u32::MAX {
let mut s = base.as_os_str().to_os_string();
s.push(format!(".{n}"));
let candidate = PathBuf::from(s);
if !candidate.exists() {
return candidate;
}
}
base
}
pub fn backup_incompatible_file(path: &Path) -> std::io::Result<PathBuf> {
let backup = incompatible_backup_path(path);
std::fs::rename(path, &backup)?;
Ok(backup)
}
pub fn open_or_recreate(path: &Path) -> Result<Database, DatabaseError> {
match Database::create(path) {
Ok(db) => Ok(db),
Err(e) if is_incompatible_format(&e) => {
match backup_incompatible_file(path) {
Ok(backup) => {
tracing::error!(
path = %path.display(),
backup = %backup.display(),
error = %e,
"redb file is in an incompatible/old format (redb 2.x); \
moved it aside and creating a fresh empty database — \
this store must be rebuilt/reindexed, not treated as ready"
);
}
Err(io) => {
tracing::error!(
path = %path.display(),
error = %e,
backup_error = %io,
"redb file is incompatible AND could not be backed up; refusing to recreate"
);
return Err(e);
}
}
Database::create(path)
}
Err(e) => Err(e),
}
}
#[cfg(test)]
mod tests {
use super::*;
use redb::StorageError;
use std::io::Write;
use tempfile::tempdir;
fn expected_verdict(err: &DatabaseError) -> bool {
match err {
DatabaseError::UpgradeRequired(_) => true,
DatabaseError::RepairAborted => true,
DatabaseError::Storage(StorageError::Corrupted(_)) => true,
DatabaseError::Storage(StorageError::Io(io)) => {
io.kind() == std::io::ErrorKind::InvalidData
}
_ => false,
}
}
fn every_constructible_variant() -> Vec<(&'static str, DatabaseError)> {
vec![
("UpgradeRequired", DatabaseError::UpgradeRequired(2)),
("RepairAborted", DatabaseError::RepairAborted),
(
"Storage(Corrupted)",
DatabaseError::Storage(StorageError::Corrupted("bad".into())),
),
(
"Storage(Io(InvalidData))",
DatabaseError::Storage(StorageError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"not a redb file",
))),
),
("DatabaseAlreadyOpen", DatabaseError::DatabaseAlreadyOpen),
(
"TransactionInProgress",
DatabaseError::TransactionInProgress,
),
(
"Storage(Io(PermissionDenied))",
DatabaseError::Storage(StorageError::Io(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"nope",
))),
),
(
"Storage(Io(StorageFull))",
DatabaseError::Storage(StorageError::Io(std::io::Error::new(
std::io::ErrorKind::StorageFull,
"disk full",
))),
),
(
"Storage(PreviousIo)",
DatabaseError::Storage(StorageError::PreviousIo),
),
(
"Storage(DatabaseClosed)",
DatabaseError::Storage(StorageError::DatabaseClosed),
),
(
"Storage(ValueTooLarge)",
DatabaseError::Storage(StorageError::ValueTooLarge(1 << 32)),
),
]
}
#[test]
fn classifier_pins_the_four_recoverable_arms() {
let mut recoverable: Vec<&'static str> = Vec::new();
for (name, err) in every_constructible_variant() {
let got = is_incompatible_format(&err);
assert_eq!(
got,
expected_verdict(&err),
"{name}: classifier and the independent verdict table disagree"
);
if got {
recoverable.push(name);
}
}
assert_eq!(
recoverable,
vec![
"UpgradeRequired",
"RepairAborted",
"Storage(Corrupted)",
"Storage(Io(InvalidData))",
],
"the recoverable-by-rebuild set must stay exactly these four arms; \
a change here alters the recovery behaviour of trusty-common, \
trusty-search, trusty-review, trusty-analyze and trusty-agents at once (#5063)"
);
}
#[test]
fn backup_renames_with_suffix() {
let dir = tempdir().unwrap();
let path = dir.path().join("index.redb");
std::fs::write(&path, b"old bytes").unwrap();
let backup = backup_incompatible_file(&path).expect("backup");
assert!(backup.to_string_lossy().ends_with(INCOMPATIBLE_SUFFIX));
assert!(backup.exists(), "backup file should exist");
assert!(!path.exists(), "original path should be freed");
assert_eq!(std::fs::read(&backup).unwrap(), b"old bytes");
}
#[test]
fn backup_path_avoids_clobber() {
let dir = tempdir().unwrap();
let path = dir.path().join("index.redb");
let first = incompatible_backup_path(&path);
std::fs::write(&first, b"first").unwrap();
let second = incompatible_backup_path(&path);
assert_ne!(first, second);
assert!(second.to_string_lossy().ends_with(".1"));
}
#[test]
fn open_or_recreate_handles_garbage_file() {
let dir = tempdir().unwrap();
let path = dir.path().join("facts.redb");
{
let mut f = std::fs::File::create(&path).unwrap();
f.write_all(&[0xABu8; 4096]).unwrap();
f.flush().unwrap();
}
let db = open_or_recreate(&path).expect("recovery should not panic or error");
let backup = {
let mut s = path.as_os_str().to_os_string();
s.push(INCOMPATIBLE_SUFFIX);
PathBuf::from(s)
};
assert!(backup.exists(), "incompatible file should be backed up");
let wtx = db.begin_write().unwrap();
wtx.commit().unwrap();
}
#[test]
fn open_or_recreate_passes_through_clean_open() {
let dir = tempdir().unwrap();
let path = dir.path().join("clean.redb");
let _db = open_or_recreate(&path).expect("clean open");
let backup = incompatible_backup_path(&path);
assert!(
!backup.exists(),
"no backup should be created for a clean open"
);
}
}