1use serde::{Deserialize, Serialize};
2use std::io;
3use std::path::Path;
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct FileEntry {
7 pub name: String,
8 pub len: u64,
9 pub blake3_hex: String,
10}
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct BackupManifest {
14 pub format_version: u32,
15 pub created_unix_secs: u64,
16 pub source_lsn: u64,
18 pub files: Vec<FileEntry>,
19}
20
21impl BackupManifest {
22 pub const FORMAT_VERSION: u32 = 1;
23 pub const FILE_NAME: &'static str = "manifest.json";
24
25 pub fn validate_version(&self) -> io::Result<()> {
26 if self.format_version != Self::FORMAT_VERSION {
27 return Err(io::Error::other(format!(
28 "unsupported backup format {} (this build understands {})",
29 self.format_version,
30 Self::FORMAT_VERSION
31 )));
32 }
33 Ok(())
34 }
35
36 pub fn write(&self, dir: &Path) -> io::Result<()> {
37 let json = serde_json::to_vec_pretty(self).map_err(io::Error::other)?;
38 std::fs::write(dir.join(Self::FILE_NAME), json)
39 }
40
41 pub fn read(dir: &Path) -> io::Result<Self> {
42 let bytes = std::fs::read(dir.join(Self::FILE_NAME))?;
43 let m: BackupManifest = serde_json::from_slice(&bytes).map_err(io::Error::other)?;
44 m.validate_version()?;
45 Ok(m)
46 }
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51pub enum ChangedFile {
52 Whole {
54 name: String,
55 len: u64,
56 blake3_hex: String,
57 },
58 Pages {
62 name: String,
63 total_pages: u32,
65 page_indices: Vec<u32>,
67 delta_file: String,
69 delta_len: u64,
70 delta_blake3_hex: String,
71 },
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct IncrementManifest {
77 pub format_version: u32,
79 pub created_unix_secs: u64,
80 pub base_source_lsn: u64,
82 pub source_lsn: u64,
84 pub changed: Vec<ChangedFile>,
85}
86
87impl IncrementManifest {
88 pub const FORMAT_VERSION: u32 = 1;
89 pub const FILE_NAME: &'static str = "increment.json";
90
91 pub fn validate_version(&self) -> io::Result<()> {
92 if self.format_version != Self::FORMAT_VERSION {
93 return Err(io::Error::other(format!(
94 "unsupported increment format {} (this build understands {})",
95 self.format_version,
96 Self::FORMAT_VERSION
97 )));
98 }
99 Ok(())
100 }
101
102 pub fn write(&self, dir: &Path) -> io::Result<()> {
103 let json = serde_json::to_vec_pretty(self).map_err(io::Error::other)?;
104 std::fs::write(dir.join(Self::FILE_NAME), json)
105 }
106
107 pub fn read(dir: &Path) -> io::Result<Self> {
108 let bytes = std::fs::read(dir.join(Self::FILE_NAME))?;
109 let m: IncrementManifest = serde_json::from_slice(&bytes).map_err(io::Error::other)?;
110 m.validate_version()?;
111 Ok(m)
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 #[test]
119 fn manifest_round_trips_and_rejects_bad_version() {
120 let m = BackupManifest {
121 format_version: BackupManifest::FORMAT_VERSION,
122 created_unix_secs: 1_700_000_000,
123 source_lsn: 42,
124 files: vec![FileEntry {
125 name: "catalog.bin".into(),
126 len: 10,
127 blake3_hex: "ab".into(),
128 }],
129 };
130 let json = serde_json::to_string(&m).unwrap();
131 let back: BackupManifest = serde_json::from_str(&json).unwrap();
132 assert_eq!(back.source_lsn, 42);
133 assert_eq!(back.files.len(), 1);
134
135 let mut bad = m.clone();
136 bad.format_version = 999;
137 assert!(
138 bad.validate_version().is_err(),
139 "unknown format must be rejected"
140 );
141 }
142}