use std::fs::{self, File, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use reddb_file::layout::retired;
const PAGE_SIZE: usize = reddb_file::PAGED_PAGE_SIZE;
const SLOT_SIZE: usize = reddb_file::PAGED_SUPERBLOCK_SLOT_SIZE;
const ZONE_SIZE: usize = reddb_file::PAGED_SUPERBLOCK_ZONE_SIZE;
const SUPERBLOCK_TRAILER_OFFSET: usize = reddb_file::PAGED_SUPERBLOCK_TRAILER_OFFSET;
const BACKUP_SUFFIX: &str = "pre-migration";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ZoneMigrationReport {
pub data_path: PathBuf,
pub backup_path: PathBuf,
pub removed_sidecars: Vec<PathBuf>,
pub header_recovered_from_shadow: bool,
pub manifest_recovered_from_shadow: bool,
}
#[derive(Debug)]
pub enum ZoneMigrationError {
MissingStore(PathBuf),
NotALegacyStore(PathBuf),
AlreadyZoned(PathBuf),
HeaderUnrecoverable(PathBuf),
Io(std::io::Error),
}
impl std::fmt::Display for ZoneMigrationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MissingStore(path) => write!(f, "no store at {}", path.display()),
Self::NotALegacyStore(path) => write!(
f,
"{} carries no retired rdb-hdr/rdb-meta sidecar, so there is nothing to \
migrate; a zoned store opens directly",
path.display()
),
Self::AlreadyZoned(path) => write!(
f,
"{} already has a valid superblock zone; migrating again would discard it",
path.display()
),
Self::HeaderUnrecoverable(path) => write!(
f,
"neither page 0 of {} nor its rdb-hdr shadow holds a readable database \
header, so no superblock can be seeded from this store. This is a damaged \
store, not a legacy one: reach for red salvage (ADR 0074 §4)",
path.display()
),
Self::Io(err) => write!(f, "io error: {err}"),
}
}
}
impl std::error::Error for ZoneMigrationError {}
impl From<std::io::Error> for ZoneMigrationError {
fn from(err: std::io::Error) -> Self {
Self::Io(err)
}
}
type Result<T> = std::result::Result<T, ZoneMigrationError>;
pub fn backup_path_for(data_path: &Path) -> PathBuf {
let mut path = data_path.as_os_str().to_os_string();
path.push(".");
path.push(BACKUP_SUFFIX);
PathBuf::from(path)
}
pub fn migrate_to_zoned(data_path: &Path) -> Result<ZoneMigrationReport> {
if !data_path.exists() {
return Err(ZoneMigrationError::MissingStore(data_path.to_path_buf()));
}
let sidecars = present_sidecars(data_path);
if sidecars.is_empty() {
return Err(ZoneMigrationError::NotALegacyStore(data_path.to_path_buf()));
}
if has_valid_superblock_zone(data_path)? {
return Err(ZoneMigrationError::AlreadyZoned(data_path.to_path_buf()));
}
let backup_path = backup_path_for(data_path);
fs::copy(data_path, &backup_path)?;
let page_zero = read_page(data_path, 0)?;
let (header_image, header_recovered_from_shadow) =
if reddb_file::database_header_magic_matches(&page_zero) {
(page_zero, false)
} else {
let shadow = read_sidecar_page(&retired::pager_header_shadow_path_v0(data_path))?
.filter(|page| reddb_file::database_header_magic_matches(page))
.ok_or_else(|| ZoneMigrationError::HeaderUnrecoverable(data_path.to_path_buf()))?;
(shadow, true)
};
let manifest_recovered_from_shadow = restore_manifest_page_if_torn(data_path)?;
let mut slot = [0u8; SLOT_SIZE];
slot.copy_from_slice(&header_image[..SLOT_SIZE]);
let mut file = OpenOptions::new().read(true).write(true).open(data_path)?;
for (copy_index, generation) in [(0usize, 1u64), (1usize, 2u64)] {
reddb_file::seal_paged_superblock_slot(&mut slot, copy_index, generation)
.map_err(|err| std::io::Error::other(err.to_string()))?;
write_at(&mut file, superblock_offset(copy_index), &slot)?;
}
file.sync_all()?;
drop(file);
for sidecar in &sidecars {
fs::remove_file(sidecar)?;
}
Ok(ZoneMigrationReport {
data_path: data_path.to_path_buf(),
backup_path,
removed_sidecars: sidecars,
header_recovered_from_shadow,
manifest_recovered_from_shadow,
})
}
pub fn revert_to_sidecars(data_path: &Path) -> Result<ZoneMigrationReport> {
if !data_path.exists() {
return Err(ZoneMigrationError::MissingStore(data_path.to_path_buf()));
}
let image = newest_superblock_image(data_path)?
.ok_or_else(|| ZoneMigrationError::HeaderUnrecoverable(data_path.to_path_buf()))?;
let mut header_page = [0u8; PAGE_SIZE];
header_page[..SLOT_SIZE].copy_from_slice(&image);
header_page[SUPERBLOCK_TRAILER_OFFSET..SLOT_SIZE].fill(0);
reddb_file::clear_paged_page_checksum(&mut header_page);
let checksum = crate::storage::engine::crc32::crc32(&header_page);
reddb_file::set_paged_page_checksum(&mut header_page, checksum);
let manifest_page = read_page(data_path, 1)?;
let mut file = OpenOptions::new().read(true).write(true).open(data_path)?;
write_at(&mut file, 0, &header_page)?;
file.sync_all()?;
drop(file);
write_sidecar_page(
&retired::pager_header_shadow_path_v0(data_path),
&header_page,
)?;
write_sidecar_page(
&retired::pager_meta_shadow_path_v0(data_path),
&manifest_page,
)?;
let backup_path = backup_path_for(data_path);
if backup_path.exists() {
fs::remove_file(&backup_path)?;
}
Ok(ZoneMigrationReport {
data_path: data_path.to_path_buf(),
backup_path,
removed_sidecars: Vec::new(),
header_recovered_from_shadow: false,
manifest_recovered_from_shadow: false,
})
}
fn newest_superblock_image(data_path: &Path) -> Result<Option<[u8; SLOT_SIZE]>> {
let zone = read_superblock_zone(data_path)?;
let Some(selection) = reddb_file::select_paged_superblock(&zone) else {
return Ok(None);
};
let start = selection.copy_index * SLOT_SIZE;
let mut image = [0u8; SLOT_SIZE];
image.copy_from_slice(&zone[start..start + SLOT_SIZE]);
Ok(Some(image))
}
fn read_superblock_zone(data_path: &Path) -> Result<[u8; ZONE_SIZE]> {
let mut zone = [0u8; ZONE_SIZE];
let mut file = File::open(data_path)?;
let len = file.metadata()?.len().min(ZONE_SIZE as u64) as usize;
if len > 0 {
file.read_exact(&mut zone[..len])?;
}
Ok(zone)
}
fn present_sidecars(data_path: &Path) -> Vec<PathBuf> {
let mut seen: Vec<PathBuf> = Vec::new();
for candidate in retired::phase1_sidecar_paths(data_path) {
if candidate.exists() && !seen.contains(&candidate) {
seen.push(candidate);
}
}
seen
}
fn has_valid_superblock_zone(data_path: &Path) -> Result<bool> {
let zone = read_superblock_zone(data_path)?;
Ok(reddb_file::select_paged_superblock(&zone).is_some())
}
fn restore_manifest_page_if_torn(data_path: &Path) -> Result<bool> {
let page_one = read_page(data_path, 1)?;
if page_checksum_valid(&page_one) {
return Ok(false);
}
let Some(shadow) = read_sidecar_page(&retired::pager_meta_shadow_path_v0(data_path))? else {
return Ok(false);
};
if !page_checksum_valid(&shadow) {
return Ok(false);
}
let mut file = OpenOptions::new().read(true).write(true).open(data_path)?;
write_at(&mut file, PAGE_SIZE as u64, &shadow)?;
file.sync_all()?;
Ok(true)
}
fn page_checksum_valid(page: &[u8; PAGE_SIZE]) -> bool {
let stored = reddb_file::paged_page_checksum(page);
let mut scratch = *page;
reddb_file::clear_paged_page_checksum(&mut scratch);
stored == crate::storage::engine::crc32::crc32(&scratch)
}
fn superblock_offset(copy_index: usize) -> u64 {
reddb_file::paged_superblock_slot_offset(copy_index)
}
fn read_page(path: &Path, page_id: u64) -> Result<[u8; PAGE_SIZE]> {
let mut file = File::open(path)?;
let mut page = [0u8; PAGE_SIZE];
file.seek(SeekFrom::Start(page_id * PAGE_SIZE as u64))?;
file.read_exact(&mut page)?;
Ok(page)
}
fn read_sidecar_page(path: &Path) -> Result<Option<[u8; PAGE_SIZE]>> {
if !path.exists() {
return Ok(None);
}
let mut file = File::open(path)?;
let mut page = [0u8; PAGE_SIZE];
match file.read_exact(&mut page) {
Ok(()) => Ok(Some(page)),
Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => Ok(None),
Err(err) => Err(err.into()),
}
}
fn write_sidecar_page(path: &Path, page: &[u8; PAGE_SIZE]) -> Result<()> {
let mut file = File::create(path)?;
file.write_all(page)?;
file.sync_all()?;
Ok(())
}
fn write_at(file: &mut File, offset: u64, bytes: &[u8]) -> Result<()> {
file.seek(SeekFrom::Start(offset))?;
file.write_all(bytes)?;
Ok(())
}