reddb-io-file 1.23.1

RedDB file artifact layer: single-file .rdb layout, WAL, snapshots, checkpoints, locks, and recovery.
Documentation
use std::fs;
use std::path::Path;

use serde::{Deserialize, Serialize};

use crate::{
    decode_native_store_header, verify_native_store_crc32_footer, EmbeddedRdbArtifact,
    RdbFileError, RdbFileResult,
};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StorageSalvageMode {
    Manifest,
    Carving,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StorageSalvageCollection {
    pub collection: String,
    pub recovered_entities: u64,
    pub skipped_entities: u64,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StorageSalvageSkippedRegion {
    pub zone_kind: String,
    pub physical_identity: String,
    pub reason: String,
    pub collection: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StorageSalvageReport {
    pub schema_version: u32,
    pub mode: StorageSalvageMode,
    pub collections: Vec<StorageSalvageCollection>,
    pub skipped_regions: Vec<StorageSalvageSkippedRegion>,
}

impl StorageSalvageReport {
    pub fn machine_json(&self) -> RdbFileResult<String> {
        serde_json::to_string(self).map_err(|err| {
            RdbFileError::InvalidOperation(format!("serialize salvage report: {err}"))
        })
    }

    pub fn human_summary(&self) -> String {
        let mode = match self.mode {
            StorageSalvageMode::Manifest => "manifest",
            StorageSalvageMode::Carving => "carving",
        };
        let mut lines = vec![format!(
            "Salvage completed in {mode} mode: {} skipped region(s).",
            self.skipped_regions.len()
        )];
        for collection in &self.collections {
            lines.push(format!(
                "{}: recovered {} entity artifact(s), skipped {}.",
                collection.collection, collection.recovered_entities, collection.skipped_entities
            ));
        }
        if self.skipped_regions.is_empty() {
            lines.push("Next steps: open the recovered store and run the query corpus before replacing any production copy.".to_string());
        } else {
            lines.push("Next steps: inspect the skipped-region list, validate the recovered store, and restore missing entities from backup or upstream systems.".to_string());
        }
        lines.join("\n")
    }
}

pub fn salvage_embedded_store(
    source: impl AsRef<Path>,
    destination: impl AsRef<Path>,
) -> RdbFileResult<StorageSalvageReport> {
    let source = source.as_ref();
    let destination = destination.as_ref();
    if destination.exists() {
        return Err(RdbFileError::InvalidOperation(format!(
            "salvage destination already exists: {}",
            destination.display()
        )));
    }

    let open = match EmbeddedRdbArtifact::open_strict_manifest(source) {
        Ok(open) => open,
        Err(_) => return salvage_by_carving(source, destination),
    };
    let mut skipped_regions = Vec::new();
    let mut skipped_entities = 0;
    let snapshot = match EmbeddedRdbArtifact::read_snapshot(&open) {
        Ok(snapshot) => snapshot.unwrap_or_default(),
        Err(_) => {
            skipped_entities = 1;
            skipped_regions.push(StorageSalvageSkippedRegion {
                zone_kind: "page".to_string(),
                physical_identity: "snapshot".to_string(),
                reason: "checksum mismatch".to_string(),
                collection: Some("embedded_snapshot".to_string()),
            });
            Vec::new()
        }
    };
    let wal_payloads = if skipped_entities == 0 {
        match EmbeddedRdbArtifact::read_wal_payloads(&open) {
            Ok(payloads) => payloads,
            Err(_) => {
                skipped_regions.push(StorageSalvageSkippedRegion {
                    zone_kind: "wal".to_string(),
                    physical_identity: "embedded-wal".to_string(),
                    reason: "checksum mismatch".to_string(),
                    collection: Some("embedded_snapshot".to_string()),
                });
                Vec::new()
            }
        }
    } else {
        Vec::new()
    };
    EmbeddedRdbArtifact::create_with_snapshot(destination, &snapshot)?;
    if !wal_payloads.is_empty() {
        EmbeddedRdbArtifact::append_wal_payloads(destination, &wal_payloads)?;
    }

    Ok(StorageSalvageReport {
        schema_version: 1,
        mode: StorageSalvageMode::Manifest,
        collections: vec![StorageSalvageCollection {
            collection: "embedded_snapshot".to_string(),
            recovered_entities: recovered_entity_artifacts(
                !snapshot.is_empty(),
                wal_payloads.len(),
            ),
            skipped_entities,
        }],
        skipped_regions,
    })
}

fn recovered_entity_artifacts(has_snapshot: bool, wal_payload_count: usize) -> u64 {
    u64::from(has_snapshot) + u64::try_from(wal_payload_count).unwrap_or(u64::MAX)
}

fn salvage_by_carving(source: &Path, destination: &Path) -> RdbFileResult<StorageSalvageReport> {
    let source_bytes = fs::read(source)?;
    let snapshot = carve_verified_native_snapshot(&source_bytes).ok_or_else(|| {
        RdbFileError::InvalidOperation(
            "salvage carving found no checksum-valid embedded snapshot".to_string(),
        )
    })?;

    EmbeddedRdbArtifact::create_with_snapshot(destination, &snapshot)?;
    Ok(StorageSalvageReport {
        schema_version: 1,
        mode: StorageSalvageMode::Carving,
        collections: vec![StorageSalvageCollection {
            collection: "embedded_snapshot".to_string(),
            recovered_entities: 1,
            skipped_entities: 0,
        }],
        skipped_regions: vec![
            StorageSalvageSkippedRegion {
                zone_kind: "superblock".to_string(),
                physical_identity: "superblock:all".to_string(),
                reason: "checksum mismatch".to_string(),
                collection: None,
            },
            StorageSalvageSkippedRegion {
                zone_kind: "manifest".to_string(),
                physical_identity: "manifest:all".to_string(),
                reason: "unreachable from manifest".to_string(),
                collection: None,
            },
            StorageSalvageSkippedRegion {
                zone_kind: "wal".to_string(),
                physical_identity: "embedded-wal".to_string(),
                reason: "unreachable from manifest".to_string(),
                collection: None,
            },
        ],
    })
}

fn carve_verified_native_snapshot(source_bytes: &[u8]) -> Option<Vec<u8>> {
    if source_bytes.len() < crate::native_store::STORE_MAGIC.len() {
        return None;
    }
    let mut carved = None;
    let last_offset = source_bytes
        .len()
        .saturating_sub(crate::native_store::STORE_MAGIC.len());
    for offset in 0..=last_offset {
        if &source_bytes[offset..offset + crate::native_store::STORE_MAGIC.len()]
            != crate::native_store::STORE_MAGIC
        {
            continue;
        }
        let candidate = source_bytes[offset..].to_vec();
        if native_snapshot_verifies(&candidate) {
            carved = Some(candidate);
        }
    }
    carved
}

fn native_snapshot_verifies(candidate: &[u8]) -> bool {
    let Ok(version) = decode_native_store_header(candidate) else {
        return false;
    };
    let mut check = candidate.to_vec();
    verify_native_store_crc32_footer(&mut check, version).is_ok()
}