use super::rtree_storage::Storage;
use super::rtree_types::{FileHeader, FreePage, PageId, SpatialError, SpatialResult};
#[derive(Debug, Clone)]
pub struct IntegrityReport {
pub pages_checked: u64,
pub corrupted_pages: Vec<PageId>,
pub orphaned_pages: Vec<PageId>,
pub is_valid: bool,
pub errors: Vec<String>,
}
impl IntegrityReport {
pub fn new() -> Self {
Self {
pages_checked: 0,
corrupted_pages: Vec::new(),
orphaned_pages: Vec::new(),
is_valid: true,
errors: Vec::new(),
}
}
}
impl Default for IntegrityReport {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct RepairOptions {
pub remove_corrupt: bool,
pub rebuild_if_needed: bool,
pub max_repairs: Option<u64>,
}
impl Default for RepairOptions {
fn default() -> Self {
Self {
remove_corrupt: true,
rebuild_if_needed: true,
max_repairs: None,
}
}
}
#[derive(Debug, Clone)]
pub struct RepairReport {
pub pages_repaired: u64,
pub pages_removed: u64,
pub rebuild_performed: bool,
pub errors: Vec<String>,
}
impl RepairReport {
pub fn new() -> Self {
Self {
pages_repaired: 0,
pages_removed: 0,
rebuild_performed: false,
errors: Vec::new(),
}
}
}
impl Default for RepairReport {
fn default() -> Self {
Self::new()
}
}
pub struct FreeListManager;
impl FreeListManager {
pub fn allocate_page(storage: &Storage, header: &mut FileHeader) -> SpatialResult<PageId> {
if header.free_list_head != 0 {
let free_page_id = header.free_list_head;
let free_page_bytes = storage.read_free_page(free_page_id)?;
let free_page: FreePage =
bincode::serde::decode_from_slice(&free_page_bytes, bincode::config::legacy())
.map(|(page, _)| page)
.map_err(|e| SpatialError::Serialization(e.to_string()))?;
header.free_list_head = free_page.next_free;
header.free_page_count = header.free_page_count.saturating_sub(1);
Ok(free_page_id)
} else {
let page_id = header.next_page_id;
header.next_page_id = header.next_page_id.saturating_add(1);
Ok(page_id)
}
}
pub fn free_page(
storage: &Storage,
header: &mut FileHeader,
page_id: PageId,
) -> SpatialResult<()> {
let free_page = FreePage {
next_free: header.free_list_head,
};
storage.write_free_page(page_id, &free_page)?;
header.free_list_head = page_id;
header.free_page_count = header.free_page_count.saturating_add(1);
Ok(())
}
pub fn free_page_count(header: &FileHeader) -> u64 {
header.free_page_count
}
pub fn free_list_head(header: &FileHeader) -> PageId {
header.free_list_head
}
}
pub trait VersionMigration {
fn from_version(&self) -> u32;
fn to_version(&self) -> u32;
fn migrate(&self, storage: &Storage, header: &mut FileHeader) -> SpatialResult<()>;
fn description(&self) -> &str;
}
pub struct V1ToV2Migration;
impl VersionMigration for V1ToV2Migration {
fn from_version(&self) -> u32 {
1
}
fn to_version(&self) -> u32 {
2
}
fn migrate(&self, storage: &Storage, header: &mut FileHeader) -> SpatialResult<()> {
header.checksum_enabled = true;
header.version = 2;
let mut current_page_id = 1;
let next_page_id = header.next_page_id;
while current_page_id < next_page_id {
match storage.read_page(current_page_id) {
Ok(node) => {
storage.write_page(current_page_id, &node)?;
}
Err(e) => {
eprintln!("Warning: Could not migrate page {}: {}", current_page_id, e);
}
}
current_page_id += 1;
}
Ok(())
}
fn description(&self) -> &str {
"Add CRC32 checksum support for corruption detection"
}
}
pub struct V2ToV3Migration;
impl VersionMigration for V2ToV3Migration {
fn from_version(&self) -> u32 {
2
}
fn to_version(&self) -> u32 {
3
}
fn migrate(&self, _storage: &Storage, header: &mut FileHeader) -> SpatialResult<()> {
header.free_list_head = 0;
header.free_page_count = 0;
header.version = 3;
Ok(())
}
fn description(&self) -> &str {
"Add free page list for improved space reuse"
}
}
pub struct MigrationManager;
impl MigrationManager {
const CURRENT_VERSION: u32 = 3;
pub fn current_version() -> u32 {
Self::CURRENT_VERSION
}
fn get_all_migrations() -> Vec<Box<dyn VersionMigration>> {
vec![Box::new(V1ToV2Migration), Box::new(V2ToV3Migration)]
}
#[allow(dead_code)]
fn get_migrations(from: u32, to: u32) -> Vec<Box<dyn VersionMigration>> {
Self::get_all_migrations()
.into_iter()
.filter(|m| m.from_version() >= from && m.to_version() <= to)
.collect()
}
pub fn needs_migration(header: &FileHeader) -> bool {
header.version < Self::CURRENT_VERSION
}
pub fn migrate(storage: &Storage, header: &mut FileHeader) -> SpatialResult<()> {
let from_version = header.version;
let to_version = Self::CURRENT_VERSION;
if from_version >= to_version {
return Ok(()); }
println!(
"Migrating file format from version {} to {}",
from_version, to_version
);
let migrations = Self::get_all_migrations();
for migration in migrations {
if migration.from_version() >= from_version && migration.from_version() < to_version {
println!(
"Applying migration: {} -> {} ({})",
migration.from_version(),
migration.to_version(),
migration.description()
);
migration.migrate(storage, header)?;
}
}
header.version = to_version;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_integrity_report_creation() {
let report = IntegrityReport::new();
assert!(report.is_valid);
assert_eq!(report.pages_checked, 0);
assert_eq!(report.corrupted_pages.len(), 0);
assert_eq!(report.orphaned_pages.len(), 0);
assert_eq!(report.errors.len(), 0);
}
#[test]
fn test_integrity_report_default() {
let report = IntegrityReport::default();
assert!(report.is_valid);
assert_eq!(report.pages_checked, 0);
}
#[test]
fn test_integrity_report_with_corrupted_pages() {
let mut report = IntegrityReport::new();
report.corrupted_pages.push(1);
report.corrupted_pages.push(2);
report.is_valid = false;
assert!(!report.is_valid);
assert_eq!(report.corrupted_pages.len(), 2);
}
#[test]
fn test_integrity_report_with_errors() {
let mut report = IntegrityReport::new();
report.errors.push("Test error".to_string());
report.is_valid = false;
assert!(!report.is_valid);
assert_eq!(report.errors.len(), 1);
assert_eq!(report.errors[0], "Test error");
}
#[test]
fn test_integrity_report_pages_checked_increment() {
let mut report = IntegrityReport::new();
assert_eq!(report.pages_checked, 0);
report.pages_checked = 100;
assert_eq!(report.pages_checked, 100);
}
#[test]
fn test_repair_options_default() {
let opts = RepairOptions::default();
assert!(opts.remove_corrupt);
assert!(opts.rebuild_if_needed);
assert_eq!(opts.max_repairs, None);
}
#[test]
fn test_repair_options_custom() {
let opts = RepairOptions {
remove_corrupt: false,
rebuild_if_needed: false,
max_repairs: Some(10),
};
assert!(!opts.remove_corrupt);
assert!(!opts.rebuild_if_needed);
assert_eq!(opts.max_repairs, Some(10));
}
#[test]
fn test_repair_options_aggressive() {
let opts = RepairOptions {
remove_corrupt: true,
rebuild_if_needed: true,
max_repairs: None,
};
assert!(opts.remove_corrupt);
assert!(opts.rebuild_if_needed);
assert_eq!(opts.max_repairs, None);
}
#[test]
fn test_repair_options_conservative() {
let opts = RepairOptions {
remove_corrupt: false,
rebuild_if_needed: false,
max_repairs: Some(1),
};
assert!(!opts.remove_corrupt);
assert!(!opts.rebuild_if_needed);
assert_eq!(opts.max_repairs, Some(1));
}
#[test]
fn test_repair_report_creation() {
let report = RepairReport::new();
assert_eq!(report.pages_repaired, 0);
assert_eq!(report.pages_removed, 0);
assert!(!report.rebuild_performed);
assert_eq!(report.errors.len(), 0);
}
#[test]
fn test_repair_report_default() {
let report = RepairReport::default();
assert_eq!(report.pages_repaired, 0);
}
#[test]
fn test_repair_report_with_repairs() {
let mut report = RepairReport::new();
report.pages_repaired = 5;
report.pages_removed = 2;
report.rebuild_performed = true;
assert_eq!(report.pages_repaired, 5);
assert_eq!(report.pages_removed, 2);
assert!(report.rebuild_performed);
}
#[test]
fn test_repair_report_with_errors() {
let mut report = RepairReport::new();
report.errors.push("Failed to repair".to_string());
assert_eq!(report.errors.len(), 1);
}
#[test]
fn test_free_list_manager_allocation() {
let mut header = FileHeader::new();
header.free_list_head = 0;
header.free_page_count = 0;
let page_id = header.next_page_id;
assert_eq!(page_id, 1);
}
#[test]
fn test_free_list_manager_free_page_count_zero() {
let header = FileHeader::new();
assert_eq!(FreeListManager::free_page_count(&header), 0);
}
#[test]
fn test_free_list_manager_free_page_count_non_zero() {
let mut header = FileHeader::new();
header.free_page_count = 10;
assert_eq!(FreeListManager::free_page_count(&header), 10);
}
#[test]
fn test_free_list_manager_free_list_head_zero() {
let header = FileHeader::new();
assert_eq!(FreeListManager::free_list_head(&header), 0);
}
#[test]
fn test_free_list_manager_free_list_head_non_zero() {
let mut header = FileHeader::new();
header.free_list_head = 5;
assert_eq!(FreeListManager::free_list_head(&header), 5);
}
#[test]
fn test_free_list_manager_large_free_page_count() {
let mut header = FileHeader::new();
header.free_page_count = u64::MAX;
assert_eq!(FreeListManager::free_page_count(&header), u64::MAX);
}
#[test]
fn test_migration_manager_current_version() {
let version = MigrationManager::current_version();
assert_eq!(version, 3);
}
#[test]
fn test_migration_manager_needs_migration_v1() {
let mut header = FileHeader::new();
header.version = 1;
assert!(MigrationManager::needs_migration(&header));
}
#[test]
fn test_migration_manager_needs_migration_v2() {
let mut header = FileHeader::new();
header.version = 2;
assert!(MigrationManager::needs_migration(&header));
}
#[test]
fn test_migration_manager_needs_migration_v3() {
let mut header = FileHeader::new();
header.version = 3;
assert!(!MigrationManager::needs_migration(&header));
}
#[test]
fn test_migration_manager_needs_migration_future_version() {
let mut header = FileHeader::new();
header.version = 10;
assert!(!MigrationManager::needs_migration(&header));
}
#[test]
fn test_migration_manager_needs_migration_zero_version() {
let mut header = FileHeader::new();
header.version = 0;
assert!(MigrationManager::needs_migration(&header));
}
#[test]
fn test_v1_to_v2_migration_info() {
let m = V1ToV2Migration;
assert_eq!(m.from_version(), 1);
assert_eq!(m.to_version(), 2);
assert!(!m.description().is_empty());
assert!(m.description().contains("checksum"));
}
#[test]
fn test_v1_to_v2_migration_version_progression() {
let m = V1ToV2Migration;
assert!(m.to_version() > m.from_version());
}
#[test]
fn test_v2_to_v3_migration_info() {
let m = V2ToV3Migration;
assert_eq!(m.from_version(), 2);
assert_eq!(m.to_version(), 3);
assert!(!m.description().is_empty());
assert!(m.description().contains("free"));
}
#[test]
fn test_v2_to_v3_migration_version_progression() {
let m = V2ToV3Migration;
assert!(m.to_version() > m.from_version());
}
#[test]
fn test_file_header_new_has_checksums_enabled() {
let header = FileHeader::new();
assert!(header.checksum_enabled);
}
#[test]
fn test_file_header_new_has_zero_free_pages() {
let header = FileHeader::new();
assert_eq!(header.free_page_count, 0);
}
#[test]
fn test_file_header_new_has_zero_free_list_head() {
let header = FileHeader::new();
assert_eq!(header.free_list_head, 0);
}
#[test]
fn test_file_header_default_version() {
let header = FileHeader::new();
assert_eq!(header.version, 1);
}
#[test]
fn test_file_header_validate() {
let header = FileHeader::new();
assert!(header.validate().is_ok());
}
#[test]
fn test_file_header_invalid_magic() {
let mut header = FileHeader::new();
header.magic = 0xDEADBEEF;
assert!(header.validate().is_err());
}
#[test]
fn test_file_header_invalid_version() {
let mut header = FileHeader::new();
header.version = 99;
assert!(header.validate().is_err());
}
#[test]
fn test_integrity_report_max_pages_checked() {
let mut report = IntegrityReport::new();
report.pages_checked = u64::MAX;
assert_eq!(report.pages_checked, u64::MAX);
}
#[test]
fn test_integrity_report_many_corrupted_pages() {
let mut report = IntegrityReport::new();
for i in 0..1000 {
report.corrupted_pages.push(i);
}
assert_eq!(report.corrupted_pages.len(), 1000);
}
#[test]
fn test_repair_report_max_pages_repaired() {
let mut report = RepairReport::new();
report.pages_repaired = u64::MAX;
assert_eq!(report.pages_repaired, u64::MAX);
}
#[test]
fn test_repair_options_max_repairs_boundary() {
let opts = RepairOptions {
remove_corrupt: true,
rebuild_if_needed: false,
max_repairs: Some(u64::MAX),
};
assert_eq!(opts.max_repairs, Some(u64::MAX));
}
#[test]
fn test_migration_version_chain() {
let v1_to_v2 = V1ToV2Migration;
let v2_to_v3 = V2ToV3Migration;
assert_eq!(v1_to_v2.to_version(), v2_to_v3.from_version());
}
#[test]
fn test_migration_all_versions_covered() {
let current = MigrationManager::current_version();
assert!(current >= 1);
let mut header = FileHeader::new();
for version in 1..=current {
header.version = version;
if version == current {
assert!(!MigrationManager::needs_migration(&header));
} else {
assert!(MigrationManager::needs_migration(&header));
}
}
}
#[test]
fn test_free_page_next_free_zero() {
let page = FreePage { next_free: 0 };
assert_eq!(page.next_free, 0);
}
#[test]
fn test_free_page_next_free_max() {
let page = FreePage {
next_free: u64::MAX,
};
assert_eq!(page.next_free, u64::MAX);
}
#[test]
fn test_free_page_chain() {
let page1 = FreePage { next_free: 2 };
let page2 = FreePage { next_free: 3 };
let page3 = FreePage { next_free: 0 };
assert_eq!(page1.next_free, 2);
assert_eq!(page2.next_free, 3);
assert_eq!(page3.next_free, 0);
}
#[test]
fn test_page_with_checksum_consistency() {
let header1 = FileHeader::new();
let header2 = FileHeader::new();
assert_eq!(header1.magic, header2.magic);
assert_eq!(header1.version, header2.version);
}
#[test]
fn test_repair_flow_no_corruption() {
let mut report = IntegrityReport::new();
report.corrupted_pages.clear();
report.is_valid = true;
let mut repair_report = RepairReport::new();
if report.is_valid {
repair_report.pages_removed = 0;
}
assert_eq!(repair_report.pages_removed, 0);
}
#[test]
fn test_repair_flow_with_corruption() {
let mut report = IntegrityReport::new();
report.corrupted_pages.push(1);
report.corrupted_pages.push(2);
report.is_valid = false;
let mut repair_report = RepairReport::new();
if !report.is_valid {
repair_report.pages_removed = report.corrupted_pages.len() as u64;
repair_report.rebuild_performed = true;
}
assert_eq!(repair_report.pages_removed, 2);
assert!(repair_report.rebuild_performed);
}
#[test]
fn test_migration_flow_v1_to_current() {
let mut header = FileHeader::new();
assert_eq!(header.version, 1);
if MigrationManager::needs_migration(&header) {
header.version = MigrationManager::current_version();
}
assert_eq!(header.version, 3);
assert!(!MigrationManager::needs_migration(&header));
}
#[test]
fn test_free_list_allocation_chain() {
let mut header = FileHeader::new();
let page1 = header.next_page_id;
header.next_page_id += 1;
let page2 = header.next_page_id;
header.next_page_id += 1;
assert_eq!(page1, 1);
assert_eq!(page2, 2);
assert_eq!(header.next_page_id, 3);
}
#[test]
fn test_multiple_corruptions_and_repairs() {
let mut integrity_reports = Vec::new();
for i in 0..5 {
let mut report = IntegrityReport::new();
report.pages_checked = (i + 1) * 10;
integrity_reports.push(report);
}
for (i, report) in integrity_reports.iter().enumerate() {
assert_eq!(report.pages_checked, ((i + 1) * 10) as u64);
}
}
#[test]
fn test_error_accumulation_in_reports() {
let mut report = IntegrityReport::new();
for i in 0..10 {
report.errors.push(format!("Error {}", i));
}
assert_eq!(report.errors.len(), 10);
assert_eq!(report.errors[0], "Error 0");
assert_eq!(report.errors[9], "Error 9");
}
#[test]
fn test_repair_options_all_combinations() {
let combinations = vec![
(true, true, None),
(true, true, Some(10)),
(true, false, None),
(true, false, Some(10)),
(false, true, None),
(false, true, Some(10)),
(false, false, None),
(false, false, Some(10)),
];
for (remove, rebuild, max) in combinations {
let opts = RepairOptions {
remove_corrupt: remove,
rebuild_if_needed: rebuild,
max_repairs: max,
};
assert_eq!(opts.remove_corrupt, remove);
assert_eq!(opts.rebuild_if_needed, rebuild);
assert_eq!(opts.max_repairs, max);
}
}
}