reddb_server/storage/compact/
salvage.rs1use super::*;
2
3#[derive(Debug)]
4pub struct PagedIndexSalvageFinding {
5 pub collection: String,
6 pub index: String,
7 pub root_page: u32,
8 pub reason: String,
9}
10
11#[derive(Debug)]
12pub struct PagedIndexSalvageReport {
13 pub indexes: Vec<PagedIndexSalvageFinding>,
14}
15
16impl PagedIndexSalvageReport {
17 pub fn machine_json(&self) -> String {
18 let indexes: Vec<_> = self
19 .indexes
20 .iter()
21 .map(|finding| {
22 crate::serde_json::json!({
23 "collection": finding.collection.clone(),
24 "index": finding.index.clone(),
25 "root_page": finding.root_page,
26 "reason": finding.reason.clone(),
27 })
28 })
29 .collect();
30 crate::serde_json::json!({
31 "scope": "secondary_indexes_only",
32 "action": "discarded_for_rebuild",
33 "page_generation_freshness_verified": false,
34 "indexes": indexes,
35 })
36 .to_string_compact()
37 }
38}
39
40pub fn salvage_paged_indexes(source: &Path, destination: &Path) -> Result<PagedIndexSalvageReport> {
45 let canonical_path = source.canonicalize()?;
46 let source = canonical_path.as_path();
47 let guard = Source::open(source)?;
49 verify_supported_source(source, &guard)?;
50 let scrub = scrub_paged_trees(source)?;
51 let mut indexes = Vec::new();
52 for finding in scrub.findings {
53 let index = finding
54 .tree
55 .strip_prefix("secondary index ")
56 .ok_or_else(|| {
57 format!(
58 "cannot safely recover {} tree of {}: {}",
59 finding.tree, finding.collection, finding.reason
60 )
61 })?;
62 indexes.push(PagedIndexSalvageFinding {
63 collection: finding.collection,
64 index: index.to_owned(),
65 root_page: finding.root_page,
66 reason: finding.reason,
67 });
68 }
69 ensure(!indexes.is_empty(), "no damaged secondary index to recover")?;
70 let report = PagedIndexSalvageReport { indexes };
71 compact_paged_inner(source, destination, Some(&report), None, false)?;
72 Ok(report)
73}
74
75#[derive(Debug)]
76pub struct PagedCollectionLoss {
77 pub collection: String,
78 pub root_page: u32,
79 pub skipped_pages: Vec<u32>,
80}
81
82#[derive(Debug)]
83pub struct PagedCollectionSalvageReport {
84 pub collections: Vec<PagedCollectionLoss>,
85}
86
87impl PagedCollectionSalvageReport {
88 pub fn machine_json(&self) -> String {
89 let collections: Vec<_> = self
90 .collections
91 .iter()
92 .map(|loss| {
93 crate::serde_json::json!({
94 "collection": loss.collection.clone(), "root_page": loss.root_page,
95 "action": if loss.skipped_pages == [loss.root_page] { "collection_emptied" } else { "damaged_leaves_skipped" },
96 "reason": "page_checksum_mismatch", "skipped_pages": loss.skipped_pages.clone(),
97 "skipped_entities": crate::serde_json::Value::Null,
98 })
99 })
100 .collect();
101 crate::serde_json::json!({"scope": "legacy_collection_trees", "page_generation_freshness_verified": false, "identity_high_water_verified": false, "safe_for_writes": false, "collections": collections}).to_string_compact()
102 }
103}
104
105pub fn salvage_paged_collections(
108 source: &Path,
109 destination: &Path,
110) -> Result<PagedCollectionSalvageReport> {
111 let canonical_path = source.canonicalize()?;
112 let source = canonical_path.as_path();
113 let guard = Arc::new(Source::open(source)?);
114 verify_supported_source(source, &guard)?;
115 ensure(
116 guard.header.physical == f::PhysicalFileHeader::default(),
117 "physical metadata requires separate data recovery verification",
118 )?;
119 ensure(
120 !f::physical_metadata_binary_path(source).exists()
121 && !f::physical_metadata_json_path(source).exists(),
122 "metadata sidecar requires separate data recovery verification",
123 )?;
124 let catalog = Catalog::read(&guard)?;
125 ensure(
126 catalog.resume.is_none() && !catalog.has_cross_references(),
127 "collection recovery requires a reference-free legacy catalog",
128 )?;
129 let scrub = scrub_paged_trees(source)?;
130 let mut collections = Vec::new();
131 for finding in scrub.findings {
132 ensure(
133 finding.tree == "collection",
134 "only collection root damage is supported",
135 )?;
136 let canonical =
137 reddb_engine::storage::system_collection_names::canonical_or_self(&finding.collection);
138 ensure(
139 !canonical.starts_with("red.") && !canonical.starts_with('_'),
140 "system collections cannot be dropped",
141 )?;
142 let skipped_pages = match guard.raw_page(finding.root_page, true) {
143 Ok(_) => {
144 let tree = BTree::with_root(Arc::clone(&guard), finding.root_page);
145 tree.visit_recoverable_leaves(
146 |id| {
147 guard.claim(id).map_err(|error| {
148 reddb_engine::storage::btree::BTreeError::Corrupted(error.to_string())
149 })
150 },
151 |_, _| Ok(()),
152 )?
153 }
154 Err(error) => {
155 ensure(
156 matches!(
157 error.downcast_ref::<reddb_engine::storage::page::PageError>(),
158 Some(reddb_engine::storage::page::PageError::ChecksumMismatch { .. })
159 ),
160 "collection root failure is not a checksum mismatch",
161 )?;
162 ensure(
163 guard.raw_page(finding.root_page, false)?.page_type()? == PageType::BTreeLeaf,
164 "only leaf-root collection damage is supported",
165 )?;
166 vec![finding.root_page]
167 }
168 };
169 ensure(!skipped_pages.is_empty(), "no checksum-damaged leaf found")?;
170 collections.push(PagedCollectionLoss {
171 collection: finding.collection,
172 root_page: finding.root_page,
173 skipped_pages,
174 });
175 }
176 ensure(
177 !collections.is_empty(),
178 "no damaged collection root to recover",
179 )?;
180 let report = PagedCollectionSalvageReport { collections };
181 compact_paged_inner(source, destination, None, Some(&report), false)?;
182 Ok(report)
183}
184
185pub(super) fn verify_supported_source(source: &Path, guard: &Source) -> Result<()> {
186 verify_supported_source_inner(source, guard, false)
187}
188
189pub(super) fn verify_supported_source_with_audit(source: &Path, guard: &Source) -> Result<()> {
190 verify_supported_source_inner(source, guard, true)
191}
192
193fn verify_supported_source_inner(
194 source: &Path,
195 guard: &Source,
196 preserve_audit: bool,
197) -> Result<()> {
198 guard.verify_superblocks()?;
199 verify_standalone_sidecars_inner(source, preserve_audit)?;
200 let physical = guard.header.physical;
201 for (page_id, expected) in [
202 (physical.manifest_page, physical.manifest_checksum),
203 (
204 physical.collection_roots_page,
205 physical.collection_roots_checksum,
206 ),
207 (physical.registry_page, physical.registry_checksum),
208 (physical.recovery_page, physical.recovery_checksum),
209 (physical.catalog_page, physical.catalog_checksum),
210 (
211 physical.metadata_state_page,
212 physical.metadata_state_checksum,
213 ),
214 (
215 physical.vector_artifact_page,
216 physical.vector_artifact_checksum,
217 ),
218 ] {
219 if page_id == 0 {
220 continue;
221 }
222 let page = guard.raw_page(page_id, true)?;
223 ensure(
224 page.page_type()? == PageType::NativeMeta,
225 "invalid physical metadata page",
226 )?;
227 let content = page.content();
232 let minimum = content
233 .iter()
234 .rposition(|byte| *byte != 0)
235 .map_or(0, |position| position + 1);
236 let mut checksum = crc32fast::Hasher::new();
237 checksum.update(&content[..minimum]);
238 let mut matches = u64::from(checksum.clone().finalize()) == expected;
239 for byte in &content[minimum..] {
240 checksum.update(std::slice::from_ref(byte));
241 matches |= u64::from(checksum.clone().finalize()) == expected;
242 }
243 ensure(
244 matches,
245 "physical metadata differs from its published checksum",
246 )?;
247 }
248 verify_vault(source, guard)?;
249 Ok(())
250}
251
252pub(super) fn verify_wal_recovery_sidecars(source: &Path) -> Result<()> {
253 verify_standalone_sidecars_inner(source, true)
254}
255
256fn verify_standalone_sidecars_inner(source: &Path, preserve_audit: bool) -> Result<()> {
257 let operational = source.with_file_name(format!("{}.ops", f::data_file_name(source)));
258 if operational.exists() {
259 f::OperationalManifest::for_db_path(source).verify_owned_for_copy()?;
260 f::OperationalManifest::for_db_path(source).verify_marker_only_for_recovery()?;
261 }
262 for path in [
263 f::logical_wal_path(source),
264 f::legacy_logical_slots_path(source),
265 ] {
266 match std::fs::symlink_metadata(path) {
267 Ok(metadata) => ensure(
268 metadata.is_file() && metadata.len() == 0,
269 "logical WAL, slot or audit data requires separate recovery verification",
270 )?,
271 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
272 Err(error) => return Err(error.into()),
273 }
274 }
275 let audit = f::legacy_audit_log_path(source);
276 match std::fs::symlink_metadata(&audit) {
277 Ok(metadata) if preserve_audit => {
278 ensure(metadata.is_file(), "audit log must be a regular file")?;
279 super::verify_audit_log_family(source)?;
280 }
281 Ok(metadata) => ensure(
282 metadata.is_file() && metadata.len() == 0,
283 "logical WAL, slot or audit data requires separate recovery verification",
284 )?,
285 Err(error) if error.kind() == std::io::ErrorKind::NotFound && preserve_audit => {
286 super::verify_audit_log_family(source)?;
287 }
288 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
289 Err(error) => return Err(error.into()),
290 }
291 Ok(())
292}
293
294pub(super) fn verify_vault(source: &Path, guard: &Source) -> Result<()> {
295 let vault = guard.raw_page(2, false)?;
296 if !vault.as_bytes().iter().all(|byte| *byte == 0) {
297 ensure(vault.page_type()? == PageType::Vault, "invalid vault page")?;
298 if vault.content().iter().all(|byte| *byte == 0) {
299 vault.verify_checksum()?;
300 return Ok(());
301 }
302 let content = vault.content();
303 ensure(
304 &content[..4] == b"RDVT" && matches!(content[4], 2 | 3),
305 "unsupported vault header",
306 )?;
307 let payload = u32::from_le_bytes(content[21..25].try_into()?);
308 ensure(
309 payload <= 64 * 1024 * 1024,
310 "vault exceeds verification budget",
311 )?;
312 let pager = Pager::open(
313 source,
314 PagerConfig {
315 read_only: true,
316 create: false,
317 ..PagerConfig::default()
318 },
319 )?;
320 let vault =
321 crate::auth::vault::Vault::open(&pager).map_err(|_| "vault authentication failed")?;
322 ensure(
323 matches!(vault.load(&pager), Ok(Some(_))),
324 "vault authentication failed",
325 )?;
326 }
327 Ok(())
328}