1use crate::manifest::{BackupManifest, SyncSnapshotMetadata};
2use powdb_storage::catalog::{Catalog, CATALOG_LSN_FILE};
3use std::io;
4use std::path::Path;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum RestoreSyncMode {
9 StripSyncIdentity,
13 PreserveSyncIdentity,
17 ForkWithNewSyncIdentity,
21}
22
23pub(crate) fn ensure_empty_dir(dest_data_dir: &Path) -> io::Result<()> {
27 if dest_data_dir.exists() && dest_data_dir.read_dir()?.next().is_some() {
28 return Err(io::Error::other(format!(
29 "restore destination {} is not empty; restore requires a fresh or empty directory",
30 dest_data_dir.display()
31 )));
32 }
33 std::fs::create_dir_all(dest_data_dir)?;
34 Ok(())
35}
36
37fn is_plain_manifest_name(name: &str) -> bool {
38 !name.is_empty()
39 && name != "."
40 && name != ".."
41 && !name.contains('/')
42 && !name.contains('\\')
43 && !name.contains(':')
44 && name
45 .chars()
46 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.'))
47}
48
49pub(crate) fn validate_backup_file_name(name: &str) -> io::Result<()> {
50 let durable_name = name == "catalog.bin"
51 || name == CATALOG_LSN_FILE
52 || (name.ends_with(".heap") && name.len() > ".heap".len())
53 || (name.ends_with(".idx") && name.len() > ".idx".len())
54 || (name.ends_with(".eidx") && name.len() > ".eidx".len());
55 if !is_plain_manifest_name(name) || !durable_name {
56 return Err(io::Error::other(format!(
57 "invalid backup manifest file name: {name}"
58 )));
59 }
60 Ok(())
61}
62
63pub(crate) fn validate_delta_file_name(delta_file: &str, data_file: &str) -> io::Result<()> {
64 validate_backup_file_name(data_file)?;
65 if !data_file.ends_with(".heap") {
66 return Err(io::Error::other(format!(
67 "invalid backup manifest delta target file name: {data_file}"
68 )));
69 }
70 let expected = format!("{data_file}.delta");
71 if !is_plain_manifest_name(delta_file) || delta_file != expected {
72 return Err(io::Error::other(format!(
73 "invalid backup manifest delta file name: {delta_file}"
74 )));
75 }
76 Ok(())
77}
78
79pub(crate) fn verify_and_copy_full(
83 manifest: &BackupManifest,
84 backup_dir: &Path,
85 dest_data_dir: &Path,
86) -> io::Result<()> {
87 for f in &manifest.files {
88 validate_backup_file_name(&f.name)?;
89 let bytes = std::fs::read(backup_dir.join(&f.name))?;
90 let hash = blake3::hash(&bytes).to_hex().to_string();
91 if hash != f.blake3_hex {
92 return Err(io::Error::other(format!(
93 "integrity check failed for {}: blake3 mismatch (backup is corrupt)",
94 f.name
95 )));
96 }
97 std::fs::write(dest_data_dir.join(&f.name), &bytes)?;
98 }
99 Ok(())
100}
101
102pub(crate) fn verify_restored_sync_catalog(
103 sync: &SyncSnapshotMetadata,
104 dest_data_dir: &Path,
105) -> io::Result<()> {
106 let catalog_bytes = std::fs::read(dest_data_dir.join("catalog.bin"))?;
107 let catalog_hash = blake3::hash(&catalog_bytes).to_hex().to_string();
108 if catalog_hash != sync.catalog_blake3_hex {
109 return Err(io::Error::new(
110 io::ErrorKind::InvalidData,
111 "restored catalog hash does not match sync snapshot metadata",
112 ));
113 }
114 Ok(())
115}
116
117pub(crate) fn apply_restore_sync_mode(
118 sync: Option<&SyncSnapshotMetadata>,
119 dest_data_dir: &Path,
120 sync_mode: RestoreSyncMode,
121) -> io::Result<()> {
122 match sync_mode {
123 RestoreSyncMode::StripSyncIdentity => {
124 if let Some(sync) = sync {
125 verify_restored_sync_catalog(sync, dest_data_dir)?;
126 }
127 }
128 RestoreSyncMode::PreserveSyncIdentity => {
129 let sync = sync.ok_or_else(|| {
130 io::Error::new(
131 io::ErrorKind::InvalidInput,
132 "preserve sync identity restore requires sync snapshot metadata",
133 )
134 })?;
135 verify_restored_sync_catalog(sync, dest_data_dir)?;
136 powdb_sync::write_identity_snapshot(dest_data_dir, &sync.identity)?;
137 }
138 RestoreSyncMode::ForkWithNewSyncIdentity => {
139 if let Some(sync) = sync {
140 verify_restored_sync_catalog(sync, dest_data_dir)?;
141 }
142 let _ = powdb_sync::open_or_create_identity(dest_data_dir)?;
143 }
144 }
145 Ok(())
146}
147
148pub fn restore(backup_dir: &Path, dest_data_dir: &Path) -> io::Result<()> {
153 restore_with_sync_mode(
154 backup_dir,
155 dest_data_dir,
156 RestoreSyncMode::StripSyncIdentity,
157 )
158}
159
160pub fn restore_with_sync_mode(
164 backup_dir: &Path,
165 dest_data_dir: &Path,
166 sync_mode: RestoreSyncMode,
167) -> io::Result<()> {
168 let manifest = BackupManifest::read(backup_dir)?;
169 ensure_empty_dir(dest_data_dir)?;
170 verify_and_copy_full(&manifest, backup_dir, dest_data_dir)?;
171 apply_restore_sync_mode(manifest.sync.as_ref(), dest_data_dir, sync_mode)?;
172 let cat = Catalog::open(dest_data_dir)?;
174 if cat.active_catalog_version() != manifest.catalog_version {
175 return Err(io::Error::new(
176 io::ErrorKind::InvalidData,
177 "restored catalog format does not match backup manifest",
178 ));
179 }
180 drop(cat);
181 Ok(())
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn manifest_file_names_reject_path_traversal() {
190 for bad in [
191 "../catalog.bin",
192 "/tmp/catalog.bin",
193 "nested/catalog.bin",
194 "nested\\catalog.bin",
195 "C:\\tmp\\catalog.bin",
196 "",
197 ".",
198 "..",
199 ".heap",
200 ".idx",
201 ".eidx",
202 "wal.log",
203 ] {
204 assert!(
205 validate_backup_file_name(bad).is_err(),
206 "{bad:?} must be rejected"
207 );
208 }
209 }
210
211 #[test]
212 fn manifest_file_names_accept_only_durable_root_files() {
213 for good in [
214 "catalog.bin",
215 CATALOG_LSN_FILE,
216 "User.heap",
217 "User_email.idx",
218 "User_7.eidx",
219 ] {
220 validate_backup_file_name(good).unwrap();
221 }
222 }
223
224 #[test]
225 fn delta_file_must_match_paged_file_name() {
226 validate_delta_file_name("User.heap.delta", "User.heap").unwrap();
227 assert!(validate_delta_file_name("User_email.idx.delta", "User_email.idx").is_err());
228 assert!(validate_delta_file_name("../User.heap.delta", "User.heap").is_err());
229 assert!(validate_delta_file_name("Other.heap.delta", "User.heap").is_err());
230 assert!(validate_delta_file_name("catalog.bin.delta", "catalog.bin").is_err());
231 assert!(validate_delta_file_name("catalog.lsn.delta", CATALOG_LSN_FILE).is_err());
232 }
233}