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 if !is_plain_manifest_name(name) || !durable_name {
55 return Err(io::Error::other(format!(
56 "invalid backup manifest file name: {name}"
57 )));
58 }
59 Ok(())
60}
61
62pub(crate) fn validate_delta_file_name(delta_file: &str, data_file: &str) -> io::Result<()> {
63 validate_backup_file_name(data_file)?;
64 if !(data_file.ends_with(".heap") || data_file.ends_with(".idx")) {
65 return Err(io::Error::other(format!(
66 "invalid backup manifest delta target file name: {data_file}"
67 )));
68 }
69 let expected = format!("{data_file}.delta");
70 if !is_plain_manifest_name(delta_file) || delta_file != expected {
71 return Err(io::Error::other(format!(
72 "invalid backup manifest delta file name: {delta_file}"
73 )));
74 }
75 Ok(())
76}
77
78pub(crate) fn verify_and_copy_full(
82 manifest: &BackupManifest,
83 backup_dir: &Path,
84 dest_data_dir: &Path,
85) -> io::Result<()> {
86 for f in &manifest.files {
87 validate_backup_file_name(&f.name)?;
88 let bytes = std::fs::read(backup_dir.join(&f.name))?;
89 let hash = blake3::hash(&bytes).to_hex().to_string();
90 if hash != f.blake3_hex {
91 return Err(io::Error::other(format!(
92 "integrity check failed for {}: blake3 mismatch (backup is corrupt)",
93 f.name
94 )));
95 }
96 std::fs::write(dest_data_dir.join(&f.name), &bytes)?;
97 }
98 Ok(())
99}
100
101pub(crate) fn verify_restored_sync_catalog(
102 sync: &SyncSnapshotMetadata,
103 dest_data_dir: &Path,
104) -> io::Result<()> {
105 let catalog_bytes = std::fs::read(dest_data_dir.join("catalog.bin"))?;
106 let catalog_hash = blake3::hash(&catalog_bytes).to_hex().to_string();
107 if catalog_hash != sync.catalog_blake3_hex {
108 return Err(io::Error::new(
109 io::ErrorKind::InvalidData,
110 "restored catalog hash does not match sync snapshot metadata",
111 ));
112 }
113 Ok(())
114}
115
116pub(crate) fn apply_restore_sync_mode(
117 sync: Option<&SyncSnapshotMetadata>,
118 dest_data_dir: &Path,
119 sync_mode: RestoreSyncMode,
120) -> io::Result<()> {
121 match sync_mode {
122 RestoreSyncMode::StripSyncIdentity => {
123 if let Some(sync) = sync {
124 verify_restored_sync_catalog(sync, dest_data_dir)?;
125 }
126 }
127 RestoreSyncMode::PreserveSyncIdentity => {
128 let sync = sync.ok_or_else(|| {
129 io::Error::new(
130 io::ErrorKind::InvalidInput,
131 "preserve sync identity restore requires sync snapshot metadata",
132 )
133 })?;
134 verify_restored_sync_catalog(sync, dest_data_dir)?;
135 powdb_sync::write_identity_snapshot(dest_data_dir, &sync.identity)?;
136 }
137 RestoreSyncMode::ForkWithNewSyncIdentity => {
138 if let Some(sync) = sync {
139 verify_restored_sync_catalog(sync, dest_data_dir)?;
140 }
141 let _ = powdb_sync::open_or_create_identity(dest_data_dir)?;
142 }
143 }
144 Ok(())
145}
146
147pub fn restore(backup_dir: &Path, dest_data_dir: &Path) -> io::Result<()> {
152 restore_with_sync_mode(
153 backup_dir,
154 dest_data_dir,
155 RestoreSyncMode::StripSyncIdentity,
156 )
157}
158
159pub fn restore_with_sync_mode(
163 backup_dir: &Path,
164 dest_data_dir: &Path,
165 sync_mode: RestoreSyncMode,
166) -> io::Result<()> {
167 let manifest = BackupManifest::read(backup_dir)?;
168 ensure_empty_dir(dest_data_dir)?;
169 verify_and_copy_full(&manifest, backup_dir, dest_data_dir)?;
170 apply_restore_sync_mode(manifest.sync.as_ref(), dest_data_dir, sync_mode)?;
171 let cat = Catalog::open(dest_data_dir)?;
173 drop(cat);
174 Ok(())
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn manifest_file_names_reject_path_traversal() {
183 for bad in [
184 "../catalog.bin",
185 "/tmp/catalog.bin",
186 "nested/catalog.bin",
187 "nested\\catalog.bin",
188 "C:\\tmp\\catalog.bin",
189 "",
190 ".",
191 "..",
192 ".heap",
193 ".idx",
194 "wal.log",
195 ] {
196 assert!(
197 validate_backup_file_name(bad).is_err(),
198 "{bad:?} must be rejected"
199 );
200 }
201 }
202
203 #[test]
204 fn manifest_file_names_accept_only_durable_root_files() {
205 for good in [
206 "catalog.bin",
207 CATALOG_LSN_FILE,
208 "User.heap",
209 "User_email.idx",
210 ] {
211 validate_backup_file_name(good).unwrap();
212 }
213 }
214
215 #[test]
216 fn delta_file_must_match_paged_file_name() {
217 validate_delta_file_name("User.heap.delta", "User.heap").unwrap();
218 assert!(validate_delta_file_name("../User.heap.delta", "User.heap").is_err());
219 assert!(validate_delta_file_name("Other.heap.delta", "User.heap").is_err());
220 assert!(validate_delta_file_name("catalog.bin.delta", "catalog.bin").is_err());
221 assert!(validate_delta_file_name("catalog.lsn.delta", CATALOG_LSN_FILE).is_err());
222 }
223}