1use std::fs::{self, File, OpenOptions};
28use std::io::{Read, Seek, SeekFrom, Write};
29use std::path::{Path, PathBuf};
30
31use reddb_file::layout::retired;
32
33const PAGE_SIZE: usize = reddb_file::PAGED_PAGE_SIZE;
34const SLOT_SIZE: usize = reddb_file::PAGED_SUPERBLOCK_SLOT_SIZE;
35const ZONE_SIZE: usize = reddb_file::PAGED_SUPERBLOCK_ZONE_SIZE;
36const SUPERBLOCK_TRAILER_OFFSET: usize = reddb_file::PAGED_SUPERBLOCK_TRAILER_OFFSET;
37
38const BACKUP_SUFFIX: &str = "pre-migration";
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct ZoneMigrationReport {
44 pub data_path: PathBuf,
46 pub backup_path: PathBuf,
48 pub removed_sidecars: Vec<PathBuf>,
50 pub header_recovered_from_shadow: bool,
52 pub manifest_recovered_from_shadow: bool,
55}
56
57#[derive(Debug)]
58pub enum ZoneMigrationError {
59 MissingStore(PathBuf),
61 NotALegacyStore(PathBuf),
63 AlreadyZoned(PathBuf),
65 HeaderUnrecoverable(PathBuf),
67 PrePhase3Unsupported {
69 path: PathBuf,
70 version: u32,
71 },
72 LegacyDwbUnsupported(PathBuf),
74 Io(std::io::Error),
75}
76
77impl std::fmt::Display for ZoneMigrationError {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 match self {
80 Self::MissingStore(path) => write!(f, "no store at {}", path.display()),
81 Self::NotALegacyStore(path) => write!(
82 f,
83 "{} carries no retired rdb-hdr/rdb-meta sidecar for this migration",
84 path.display()
85 ),
86 Self::AlreadyZoned(path) => write!(
87 f,
88 "{} already has a valid superblock zone; migrating again would discard it",
89 path.display()
90 ),
91 Self::HeaderUnrecoverable(path) => write!(
92 f,
93 "neither page 0 of {} nor its rdb-hdr shadow holds a readable database \
94 header, so no superblock can be seeded from this store. This is a damaged \
95 store. Preserve the database and its recovery files; restore from a \
96 verified backup",
97 path.display()
98 ),
99 Self::PrePhase3Unsupported { path, version } => write!(
100 f,
101 "{} uses pre-phase-3 pager version {version:#010x}; this migration tool \
102 cannot relocate its pages or recovery state. The source was not changed; \
103 retain its complete backup and use the original engine until a qualified \
104 conversion is available",
105 path.display()
106 ),
107 Self::LegacyDwbUnsupported(path) => write!(
108 f,
109 "{} is a legacy double-write sidecar; this migration tool cannot verify or \
110 recover its contents. The source was not changed; retain the complete \
111 store until a qualified conversion is available",
112 path.display()
113 ),
114 Self::Io(err) => write!(f, "io error: {err}"),
115 }
116 }
117}
118
119impl std::error::Error for ZoneMigrationError {}
120
121impl From<std::io::Error> for ZoneMigrationError {
122 fn from(err: std::io::Error) -> Self {
123 Self::Io(err)
124 }
125}
126
127type Result<T> = std::result::Result<T, ZoneMigrationError>;
128
129pub fn backup_path_for(data_path: &Path) -> PathBuf {
131 let mut path = data_path.as_os_str().to_os_string();
132 path.push(".");
133 path.push(BACKUP_SUFFIX);
134 PathBuf::from(path)
135}
136
137pub fn migrate_to_zoned(data_path: &Path) -> Result<ZoneMigrationReport> {
144 if !data_path.exists() {
145 return Err(ZoneMigrationError::MissingStore(data_path.to_path_buf()));
146 }
147 let dwb = retired::first_present_phase3_dwb_sidecar(data_path);
148 let sidecars = present_sidecars(data_path);
149 if sidecars.is_empty() {
150 if let Some(sidecar) = dwb {
153 return Err(ZoneMigrationError::LegacyDwbUnsupported(sidecar));
154 }
155 return Err(ZoneMigrationError::NotALegacyStore(data_path.to_path_buf()));
156 }
157 if has_valid_superblock_zone(data_path)? {
158 return Err(ZoneMigrationError::AlreadyZoned(data_path.to_path_buf()));
159 }
160
161 let page_zero = read_page(data_path, 0)?;
162 let (header_image, header_recovered_from_shadow) =
165 if reddb_file::database_header_magic_matches(&page_zero) {
166 (page_zero, false)
167 } else {
168 let shadow = read_sidecar_page(&retired::pager_header_shadow_path_v0(data_path))?
169 .filter(|page| reddb_file::database_header_magic_matches(page))
170 .ok_or_else(|| ZoneMigrationError::HeaderUnrecoverable(data_path.to_path_buf()))?;
171 (shadow, true)
172 };
173
174 let header = reddb_file::decode_database_header(&header_image)
179 .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))?;
180 if header.version < reddb_file::PAGE_DWB_ZONE_FILE_VERSION {
181 return Err(ZoneMigrationError::PrePhase3Unsupported {
182 path: data_path.to_path_buf(),
183 version: header.version,
184 });
185 }
186 if let Some(sidecar) = dwb {
187 return Err(ZoneMigrationError::LegacyDwbUnsupported(sidecar));
188 }
189 let backup_path = backup_path_for(data_path);
190 create_rollback_copy(data_path, &backup_path)?;
191
192 let manifest_recovered_from_shadow = restore_manifest_page_if_torn(data_path)?;
193
194 let mut slot = [0u8; SLOT_SIZE];
197 slot.copy_from_slice(&header_image[..SLOT_SIZE]);
198 let mut file = OpenOptions::new().read(true).write(true).open(data_path)?;
199 for (copy_index, generation) in [(0usize, 1u64), (1usize, 2u64)] {
200 reddb_file::seal_paged_superblock_slot(&mut slot, copy_index, generation)
201 .map_err(|err| std::io::Error::other(err.to_string()))?;
202 write_at(&mut file, superblock_offset(copy_index), &slot)?;
203 }
204 file.sync_all()?;
205 drop(file);
206
207 for sidecar in &sidecars {
210 fs::remove_file(sidecar)?;
211 }
212
213 Ok(ZoneMigrationReport {
214 data_path: data_path.to_path_buf(),
215 backup_path,
216 removed_sidecars: sidecars,
217 header_recovered_from_shadow,
218 manifest_recovered_from_shadow,
219 })
220}
221
222fn create_rollback_copy(data_path: &Path, backup_path: &Path) -> std::io::Result<()> {
225 let mut source = File::open(data_path)?;
226 let mut options = OpenOptions::new();
227 options.write(true).create_new(true);
228 #[cfg(unix)]
229 {
230 use std::os::unix::fs::OpenOptionsExt;
231 options.mode(0o600);
232 }
233 let mut backup = options.open(backup_path)?;
234 std::io::copy(&mut source, &mut backup)?;
235 backup.sync_all()?;
236 #[cfg(unix)]
237 {
238 let parent = backup_path
239 .parent()
240 .filter(|p| !p.as_os_str().is_empty())
241 .unwrap_or(Path::new("."));
242 File::open(parent)?.sync_all()?;
243 }
244 Ok(())
245}
246
247pub fn revert_to_sidecars(data_path: &Path) -> Result<ZoneMigrationReport> {
256 if !data_path.exists() {
257 return Err(ZoneMigrationError::MissingStore(data_path.to_path_buf()));
258 }
259
260 let image = newest_superblock_image(data_path)?
261 .ok_or_else(|| ZoneMigrationError::HeaderUnrecoverable(data_path.to_path_buf()))?;
262
263 let mut header_page = [0u8; PAGE_SIZE];
266 header_page[..SLOT_SIZE].copy_from_slice(&image);
267 header_page[SUPERBLOCK_TRAILER_OFFSET..SLOT_SIZE].fill(0);
268 reddb_file::clear_paged_page_checksum(&mut header_page);
269 let checksum = crate::storage::engine::crc32::crc32(&header_page);
270 reddb_file::set_paged_page_checksum(&mut header_page, checksum);
271
272 let manifest_page = read_page(data_path, 1)?;
273
274 let mut file = OpenOptions::new().read(true).write(true).open(data_path)?;
275 write_at(&mut file, 0, &header_page)?;
276 file.sync_all()?;
277 drop(file);
278
279 write_sidecar_page(
280 &retired::pager_header_shadow_path_v0(data_path),
281 &header_page,
282 )?;
283 write_sidecar_page(
284 &retired::pager_meta_shadow_path_v0(data_path),
285 &manifest_page,
286 )?;
287
288 let backup_path = backup_path_for(data_path);
289 if backup_path.exists() {
290 fs::remove_file(&backup_path)?;
291 }
292
293 Ok(ZoneMigrationReport {
294 data_path: data_path.to_path_buf(),
295 backup_path,
296 removed_sidecars: Vec::new(),
297 header_recovered_from_shadow: false,
298 manifest_recovered_from_shadow: false,
299 })
300}
301
302fn newest_superblock_image(data_path: &Path) -> Result<Option<[u8; SLOT_SIZE]>> {
305 let zone = read_superblock_zone(data_path)?;
306 let Some(selection) = reddb_file::select_paged_superblock(&zone) else {
307 return Ok(None);
308 };
309 let start = selection.copy_index * SLOT_SIZE;
310 let mut image = [0u8; SLOT_SIZE];
311 image.copy_from_slice(&zone[start..start + SLOT_SIZE]);
312 Ok(Some(image))
313}
314
315fn read_superblock_zone(data_path: &Path) -> Result<[u8; ZONE_SIZE]> {
316 let mut zone = [0u8; ZONE_SIZE];
317 let mut file = File::open(data_path)?;
318 let len = file.metadata()?.len().min(ZONE_SIZE as u64) as usize;
319 if len > 0 {
320 file.read_exact(&mut zone[..len])?;
321 }
322 Ok(zone)
323}
324
325fn present_sidecars(data_path: &Path) -> Vec<PathBuf> {
326 let mut seen: Vec<PathBuf> = Vec::new();
327 for candidate in retired::phase1_sidecar_paths(data_path) {
328 if candidate.exists() && !seen.contains(&candidate) {
329 seen.push(candidate);
330 }
331 }
332 seen
333}
334
335fn has_valid_superblock_zone(data_path: &Path) -> Result<bool> {
336 let zone = read_superblock_zone(data_path)?;
337 Ok(reddb_file::select_paged_superblock(&zone).is_some())
338}
339
340fn restore_manifest_page_if_torn(data_path: &Path) -> Result<bool> {
343 let page_one = read_page(data_path, 1)?;
344 if page_checksum_valid(&page_one) {
345 return Ok(false);
346 }
347 let Some(shadow) = read_sidecar_page(&retired::pager_meta_shadow_path_v0(data_path))? else {
348 return Ok(false);
349 };
350 if !page_checksum_valid(&shadow) {
351 return Ok(false);
352 }
353 let mut file = OpenOptions::new().read(true).write(true).open(data_path)?;
354 write_at(&mut file, PAGE_SIZE as u64, &shadow)?;
355 file.sync_all()?;
356 Ok(true)
357}
358
359fn page_checksum_valid(page: &[u8; PAGE_SIZE]) -> bool {
362 let stored = reddb_file::paged_page_checksum(page);
363 let mut scratch = *page;
364 reddb_file::clear_paged_page_checksum(&mut scratch);
365 stored == crate::storage::engine::crc32::crc32(&scratch)
366}
367
368fn superblock_offset(copy_index: usize) -> u64 {
369 reddb_file::paged_superblock_slot_offset(copy_index)
370}
371
372fn read_page(path: &Path, page_id: u64) -> Result<[u8; PAGE_SIZE]> {
373 let mut file = File::open(path)?;
374 let mut page = [0u8; PAGE_SIZE];
375 file.seek(SeekFrom::Start(page_id * PAGE_SIZE as u64))?;
376 file.read_exact(&mut page)?;
377 Ok(page)
378}
379
380fn read_sidecar_page(path: &Path) -> Result<Option<[u8; PAGE_SIZE]>> {
381 if !path.exists() {
382 return Ok(None);
383 }
384 let mut file = File::open(path)?;
385 let mut page = [0u8; PAGE_SIZE];
386 match file.read_exact(&mut page) {
387 Ok(()) => Ok(Some(page)),
388 Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => Ok(None),
390 Err(err) => Err(err.into()),
391 }
392}
393
394fn write_sidecar_page(path: &Path, page: &[u8; PAGE_SIZE]) -> Result<()> {
395 let mut file = File::create(path)?;
396 file.write_all(page)?;
397 file.sync_all()?;
398 Ok(())
399}
400
401fn write_at(file: &mut File, offset: u64, bytes: &[u8]) -> Result<()> {
402 file.seek(SeekFrom::Start(offset))?;
403 file.write_all(bytes)?;
404 Ok(())
405}