1use std::{
4 fs::{self, File, OpenOptions},
5 io::{self, Read, Write},
6 path::{Path, PathBuf},
7};
8
9use hyphae_core::DISK_FORMAT_VERSION;
10use thiserror::Error;
11
12const MAGIC: [u8; 8] = *b"HYMNFST1";
13const MANIFEST_FORMAT_VERSION: u16 = 1;
14const ENCODED_LENGTH: usize = 140;
15const ENCODED_LENGTH_U64: u64 = 140;
16const CHECKSUM_PREFIX_LENGTH: usize = 104;
17const DIGEST_PREFIX_LENGTH: usize = 108;
18const MANIFEST_EXTENSION: &str = "hymanifest";
19
20#[derive(Debug, Error)]
22pub enum ManifestError {
23 #[error(transparent)]
25 Io(#[from] io::Error),
26
27 #[error("invalid storage manifest {path}: {reason}")]
29 Invalid {
30 path: PathBuf,
32 reason: &'static str,
34 },
35
36 #[error(
38 "unsupported storage manifest version {found}; supported manifest version is {supported}"
39 )]
40 UnsupportedVersion {
41 found: u16,
43 supported: u16,
45 },
46
47 #[error("storage manifest generation {generation} already exists with different content")]
49 GenerationConflict {
50 generation: u64,
52 },
53}
54
55#[derive(Clone, Copy, Debug, Eq, PartialEq)]
56pub(crate) struct StorageManifest {
57 pub(crate) generation: u64,
58 pub(crate) active_segment: u64,
59 pub(crate) base_sequence: u64,
60 pub(crate) base_digest: [u8; 32],
61 pub(crate) snapshot_digest: [u8; 32],
62}
63
64impl StorageManifest {
65 fn initial() -> Self {
66 Self {
67 generation: 1,
68 active_segment: 1,
69 base_sequence: 0,
70 base_digest: [0; 32],
71 snapshot_digest: [0; 32],
72 }
73 }
74
75 pub(crate) fn load_or_initialize(root: &Path) -> Result<Self, ManifestError> {
76 let directory = root.join("manifest");
77 let mut generations = Vec::new();
78 for entry in fs::read_dir(&directory)? {
79 let path = entry?.path();
80 if let Some(generation) = generation_from_path(&path)? {
81 generations.push((generation, path));
82 }
83 }
84 generations.sort_unstable_by_key(|(generation, _)| *generation);
85 if let Some((generation, path)) = generations.last() {
86 return decode_manifest(path, *generation);
87 }
88
89 let initial = Self::initial();
90 initial.write_new(root)?;
91 Ok(initial)
92 }
93
94 pub(crate) fn write_new(&self, root: &Path) -> Result<(), ManifestError> {
95 validate_semantics(self, &root.join(manifest_filename(self.generation)))?;
96 let manifest_directory = root.join("manifest");
97 let final_path = manifest_directory.join(manifest_filename(self.generation));
98 if final_path.exists() {
99 let existing = decode_manifest(&final_path, self.generation)?;
100 return if existing == *self {
101 Ok(())
102 } else {
103 Err(ManifestError::GenerationConflict {
104 generation: self.generation,
105 })
106 };
107 }
108
109 let temporary_path = root.join("tmp").join(format!(
110 "manifest-{:020}-{}.tmp",
111 self.generation,
112 uuid::Uuid::now_v7()
113 ));
114 let encoded = encode_manifest(self);
115 let mut file = OpenOptions::new()
116 .create_new(true)
117 .write(true)
118 .open(&temporary_path)?;
119 file.write_all(&encoded)?;
120 file.sync_all()?;
121 drop(file);
122 fs::rename(&temporary_path, &final_path)?;
123 #[cfg(unix)]
124 sync_directory(&manifest_directory)?;
125 Ok(())
126 }
127}
128
129fn manifest_filename(generation: u64) -> String {
130 format!("{generation:020}.{MANIFEST_EXTENSION}")
131}
132
133fn generation_from_path(path: &Path) -> Result<Option<u64>, ManifestError> {
134 if path.extension().and_then(|extension| extension.to_str()) != Some(MANIFEST_EXTENSION) {
135 return Ok(None);
136 }
137 let Some(filename) = path.file_name().and_then(|name| name.to_str()) else {
138 return Err(invalid(path, "manifest filename is not UTF-8"));
139 };
140 let Some(raw_generation) = filename.strip_suffix(&format!(".{MANIFEST_EXTENSION}")) else {
141 return Err(invalid(path, "malformed manifest filename"));
142 };
143 let generation = raw_generation
144 .parse::<u64>()
145 .map_err(|_| invalid(path, "malformed manifest generation"))?;
146 if manifest_filename(generation) != filename {
147 return Err(invalid(path, "noncanonical manifest filename"));
148 }
149 Ok(Some(generation))
150}
151
152fn encode_manifest(manifest: &StorageManifest) -> [u8; ENCODED_LENGTH] {
153 let mut encoded = [0_u8; ENCODED_LENGTH];
154 encoded[..8].copy_from_slice(&MAGIC);
155 encoded[8..10].copy_from_slice(&MANIFEST_FORMAT_VERSION.to_le_bytes());
156 encoded[10..12].copy_from_slice(&DISK_FORMAT_VERSION.to_le_bytes());
157 encoded[12..16].copy_from_slice(&0_u32.to_le_bytes());
158 encoded[16..24].copy_from_slice(&manifest.generation.to_le_bytes());
159 encoded[24..32].copy_from_slice(&manifest.active_segment.to_le_bytes());
160 encoded[32..40].copy_from_slice(&manifest.base_sequence.to_le_bytes());
161 encoded[40..72].copy_from_slice(&manifest.base_digest);
162 encoded[72..104].copy_from_slice(&manifest.snapshot_digest);
163 let checksum = crc32c::crc32c(&encoded[..CHECKSUM_PREFIX_LENGTH]);
164 encoded[104..108].copy_from_slice(&checksum.to_le_bytes());
165 let digest = blake3::hash(&encoded[..DIGEST_PREFIX_LENGTH]);
166 encoded[108..140].copy_from_slice(digest.as_bytes());
167 encoded
168}
169
170fn decode_manifest(
171 path: &Path,
172 filename_generation: u64,
173) -> Result<StorageManifest, ManifestError> {
174 let mut file = File::open(path)?;
175 if file.metadata()?.len() != ENCODED_LENGTH_U64 {
176 return Err(invalid(path, "file length mismatch"));
177 }
178 let mut encoded = [0_u8; ENCODED_LENGTH];
179 file.read_exact(&mut encoded)?;
180 if encoded[..8] != MAGIC {
181 return Err(invalid(path, "bad magic"));
182 }
183 let manifest_version = u16::from_le_bytes(copy_array(&encoded[8..10]));
184 if manifest_version != MANIFEST_FORMAT_VERSION {
185 return Err(ManifestError::UnsupportedVersion {
186 found: manifest_version,
187 supported: MANIFEST_FORMAT_VERSION,
188 });
189 }
190 if u16::from_le_bytes(copy_array(&encoded[10..12])) != DISK_FORMAT_VERSION {
191 return Err(invalid(path, "disk format mismatch"));
192 }
193 if u32::from_le_bytes(copy_array(&encoded[12..16])) != 0 {
194 return Err(invalid(path, "unsupported flags"));
195 }
196 let expected_checksum = u32::from_le_bytes(copy_array(&encoded[104..108]));
197 if crc32c::crc32c(&encoded[..CHECKSUM_PREFIX_LENGTH]) != expected_checksum {
198 return Err(invalid(path, "CRC32C mismatch"));
199 }
200 let expected_digest: [u8; 32] = copy_array(&encoded[108..140]);
201 if *blake3::hash(&encoded[..DIGEST_PREFIX_LENGTH]).as_bytes() != expected_digest {
202 return Err(invalid(path, "BLAKE3 digest mismatch"));
203 }
204
205 let manifest = StorageManifest {
206 generation: u64::from_le_bytes(copy_array(&encoded[16..24])),
207 active_segment: u64::from_le_bytes(copy_array(&encoded[24..32])),
208 base_sequence: u64::from_le_bytes(copy_array(&encoded[32..40])),
209 base_digest: copy_array(&encoded[40..72]),
210 snapshot_digest: copy_array(&encoded[72..104]),
211 };
212 if manifest.generation != filename_generation {
213 return Err(invalid(path, "filename generation mismatch"));
214 }
215 validate_semantics(&manifest, path)?;
216 Ok(manifest)
217}
218
219fn validate_semantics(manifest: &StorageManifest, path: &Path) -> Result<(), ManifestError> {
220 if manifest.generation == 0 || manifest.active_segment != manifest.generation {
221 return Err(invalid(path, "invalid generation or active segment"));
222 }
223 let empty_anchor = manifest.base_sequence == 0;
224 if empty_anchor != (manifest.base_digest == [0; 32])
225 || empty_anchor != (manifest.snapshot_digest == [0; 32])
226 {
227 return Err(invalid(path, "inconsistent snapshot anchor"));
228 }
229 if manifest.generation == 1 && !empty_anchor {
230 return Err(invalid(path, "initial generation has a snapshot anchor"));
231 }
232 if manifest.generation > 1 && empty_anchor {
233 return Err(invalid(
234 path,
235 "compacted generation lacks a snapshot anchor",
236 ));
237 }
238 Ok(())
239}
240
241fn invalid(path: &Path, reason: &'static str) -> ManifestError {
242 ManifestError::Invalid {
243 path: path.to_path_buf(),
244 reason,
245 }
246}
247
248#[cfg(unix)]
249fn sync_directory(path: &Path) -> Result<(), ManifestError> {
250 File::open(path)?.sync_all()?;
251 Ok(())
252}
253
254fn copy_array<const N: usize>(source: &[u8]) -> [u8; N] {
255 let mut output = [0_u8; N];
256 output.copy_from_slice(source);
257 output
258}
259
260#[cfg(test)]
261mod tests {
262 use std::{error::Error, fs, io::Write};
263
264 use super::{ManifestError, StorageManifest};
265 use crate::test_support::TestDirectory;
266
267 fn initialize_layout(root: &std::path::Path) -> Result<(), Box<dyn Error>> {
268 fs::create_dir_all(root.join("manifest"))?;
269 fs::create_dir_all(root.join("tmp"))?;
270 Ok(())
271 }
272
273 #[test]
274 fn initializes_and_reloads_an_immutable_manifest() -> Result<(), Box<dyn Error>> {
275 let temporary = TestDirectory::new("manifest-initial")?;
276 initialize_layout(temporary.path())?;
277
278 let created = StorageManifest::load_or_initialize(temporary.path())?;
279 let reopened = StorageManifest::load_or_initialize(temporary.path())?;
280 assert_eq!(created, reopened);
281 assert_eq!(created.generation, 1);
282 assert_eq!(created.base_sequence, 0);
283 Ok(())
284 }
285
286 #[test]
287 fn ignores_an_interrupted_temporary_manifest() -> Result<(), Box<dyn Error>> {
288 let temporary = TestDirectory::new("manifest-interrupted")?;
289 initialize_layout(temporary.path())?;
290 let mut partial = fs::File::create(temporary.path().join("tmp/manifest-partial.tmp"))?;
291 partial.write_all(b"partial")?;
292 partial.sync_all()?;
293
294 let manifest = StorageManifest::load_or_initialize(temporary.path())?;
295 assert_eq!(manifest.generation, 1);
296 Ok(())
297 }
298
299 #[test]
300 fn rejects_corruption_in_the_latest_generation() -> Result<(), Box<dyn Error>> {
301 let temporary = TestDirectory::new("manifest-corrupt")?;
302 initialize_layout(temporary.path())?;
303 StorageManifest::load_or_initialize(temporary.path())?;
304 let path = temporary
305 .path()
306 .join("manifest/00000000000000000001.hymanifest");
307 let mut bytes = fs::read(&path)?;
308 bytes[40] ^= 1;
309 fs::write(&path, bytes)?;
310
311 assert!(matches!(
312 StorageManifest::load_or_initialize(temporary.path()),
313 Err(ManifestError::Invalid {
314 reason: "CRC32C mismatch",
315 ..
316 })
317 ));
318 Ok(())
319 }
320}