1use std::{
4 fs::{self, File, OpenOptions},
5 io::{self, Read, Write},
6 path::{Path, PathBuf},
7};
8
9use hyphae_core::{DISK_FORMAT_VERSION, MIN_DISK_FORMAT_VERSION};
10use thiserror::Error;
11
12use crate::{
13 StorageLimitError,
14 limits::{OperationDeadline, limit_io_error},
15};
16
17const MAGIC: [u8; 8] = *b"HYMNFST1";
18const MANIFEST_FORMAT_VERSION: u16 = 1;
19const ENCODED_LENGTH: usize = 140;
20const ENCODED_LENGTH_U64: u64 = 140;
21const CHECKSUM_PREFIX_LENGTH: usize = 104;
22const DIGEST_PREFIX_LENGTH: usize = 108;
23const MANIFEST_EXTENSION: &str = "hymanifest";
24
25#[derive(Debug, Error)]
27pub enum ManifestError {
28 #[error(transparent)]
30 Io(#[from] io::Error),
31
32 #[error("invalid storage manifest {path}: {reason}")]
34 Invalid {
35 path: PathBuf,
37 reason: &'static str,
39 },
40
41 #[error(
43 "unsupported storage manifest version {found}; supported manifest version is {supported}"
44 )]
45 UnsupportedVersion {
46 found: u16,
48 supported: u16,
50 },
51
52 #[error("storage manifest generation {generation} already exists with different content")]
54 GenerationConflict {
55 generation: u64,
57 },
58}
59
60impl From<StorageLimitError> for ManifestError {
61 fn from(source: StorageLimitError) -> Self {
62 Self::Io(limit_io_error(source))
63 }
64}
65
66#[derive(Clone, Copy, Debug, Eq, PartialEq)]
67pub(crate) struct StorageManifest {
68 pub(crate) generation: u64,
69 pub(crate) active_segment: u64,
70 pub(crate) base_sequence: u64,
71 pub(crate) base_digest: [u8; 32],
72 pub(crate) snapshot_digest: [u8; 32],
73}
74
75impl StorageManifest {
76 fn initial() -> Self {
77 Self {
78 generation: 1,
79 active_segment: 1,
80 base_sequence: 0,
81 base_digest: [0; 32],
82 snapshot_digest: [0; 32],
83 }
84 }
85
86 #[cfg(test)]
87 pub(crate) fn load_or_initialize(root: &Path) -> Result<Self, ManifestError> {
88 let deadline = OperationDeadline::new(std::time::Duration::from_secs(60));
89 Self::load_or_initialize_with_limits(root, 1_000_000, &deadline)
90 }
91
92 pub(crate) fn load_or_initialize_with_limits(
93 root: &Path,
94 max_directory_entries: u64,
95 deadline: &OperationDeadline,
96 ) -> Result<Self, ManifestError> {
97 deadline.check()?;
98 let directory = root.join("manifest");
99 let mut generations = Vec::new();
100 let mut entry_count = 0_u64;
101 for entry in fs::read_dir(&directory)? {
102 deadline.check()?;
103 entry_count =
104 entry_count
105 .checked_add(1)
106 .ok_or(StorageLimitError::DirectoryEntriesExceeded {
107 maximum: max_directory_entries,
108 })?;
109 if entry_count > max_directory_entries {
110 return Err(StorageLimitError::DirectoryEntriesExceeded {
111 maximum: max_directory_entries,
112 }
113 .into());
114 }
115 let path = entry?.path();
116 if let Some(generation) = generation_from_path(&path)? {
117 generations.push((generation, path));
118 }
119 }
120 generations.sort_unstable_by_key(|(generation, _)| *generation);
121 if let Some((generation, path)) = generations.last() {
122 return decode_manifest(path, *generation);
123 }
124
125 let required_entries =
126 entry_count
127 .checked_add(1)
128 .ok_or(StorageLimitError::DirectoryEntriesExceeded {
129 maximum: max_directory_entries,
130 })?;
131 if required_entries > max_directory_entries {
132 return Err(StorageLimitError::DirectoryEntriesExceeded {
133 maximum: max_directory_entries,
134 }
135 .into());
136 }
137 let initial = Self::initial();
138 initial.write_new(root)?;
139 Ok(initial)
140 }
141
142 pub(crate) fn write_new(&self, root: &Path) -> Result<(), ManifestError> {
143 self.write_new_inner(root, false)
144 }
145
146 #[cfg(test)]
147 fn write_new_with_injected_temporary_failure(&self, root: &Path) -> Result<(), ManifestError> {
148 self.write_new_inner(root, true)
149 }
150
151 fn write_new_inner(
152 &self,
153 root: &Path,
154 inject_temporary_failure: bool,
155 ) -> Result<(), ManifestError> {
156 validate_semantics(self, &root.join(manifest_filename(self.generation)))?;
157 let manifest_directory = root.join("manifest");
158 let final_path = manifest_directory.join(manifest_filename(self.generation));
159 if final_path.exists() {
160 let existing = decode_manifest(&final_path, self.generation)?;
161 return if existing == *self {
162 Ok(())
163 } else {
164 Err(ManifestError::GenerationConflict {
165 generation: self.generation,
166 })
167 };
168 }
169
170 let temporary_path = root.join("tmp").join(format!(
171 "manifest-{:020}-{}.tmp",
172 self.generation,
173 uuid::Uuid::now_v7()
174 ));
175 let mut temporary_guard = TemporaryManifestGuard::new(temporary_path.clone());
176 let encoded = encode_manifest(self);
177 let mut file = OpenOptions::new()
178 .create_new(true)
179 .write(true)
180 .open(&temporary_path)?;
181 file.write_all(&encoded)?;
182 file.sync_all()?;
183 drop(file);
184 if inject_temporary_failure {
185 return Err(io::Error::other("injected temporary manifest failure").into());
186 }
187 fs::rename(&temporary_path, &final_path)?;
188 temporary_guard.disarm();
189 #[cfg(unix)]
190 sync_directory(&manifest_directory)?;
191 Ok(())
192 }
193
194 pub(crate) fn path(&self, root: &Path) -> PathBuf {
195 root.join("manifest")
196 .join(manifest_filename(self.generation))
197 }
198}
199
200struct TemporaryManifestGuard {
201 path: PathBuf,
202 armed: bool,
203}
204
205impl TemporaryManifestGuard {
206 fn new(path: PathBuf) -> Self {
207 Self { path, armed: true }
208 }
209
210 fn disarm(&mut self) {
211 self.armed = false;
212 }
213}
214
215impl Drop for TemporaryManifestGuard {
216 fn drop(&mut self) {
217 if self.armed {
218 let _ignored = fs::remove_file(&self.path);
219 }
220 }
221}
222
223fn manifest_filename(generation: u64) -> String {
224 format!("{generation:020}.{MANIFEST_EXTENSION}")
225}
226
227fn generation_from_path(path: &Path) -> Result<Option<u64>, ManifestError> {
228 if path.extension().and_then(|extension| extension.to_str()) != Some(MANIFEST_EXTENSION) {
229 return Ok(None);
230 }
231 let Some(filename) = path.file_name().and_then(|name| name.to_str()) else {
232 return Err(invalid(path, "manifest filename is not UTF-8"));
233 };
234 let Some(raw_generation) = filename.strip_suffix(&format!(".{MANIFEST_EXTENSION}")) else {
235 return Err(invalid(path, "malformed manifest filename"));
236 };
237 let generation = raw_generation
238 .parse::<u64>()
239 .map_err(|_| invalid(path, "malformed manifest generation"))?;
240 if manifest_filename(generation) != filename {
241 return Err(invalid(path, "noncanonical manifest filename"));
242 }
243 Ok(Some(generation))
244}
245
246fn encode_manifest(manifest: &StorageManifest) -> [u8; ENCODED_LENGTH] {
247 let mut encoded = [0_u8; ENCODED_LENGTH];
248 encoded[..8].copy_from_slice(&MAGIC);
249 encoded[8..10].copy_from_slice(&MANIFEST_FORMAT_VERSION.to_le_bytes());
250 encoded[10..12].copy_from_slice(&DISK_FORMAT_VERSION.to_le_bytes());
251 encoded[12..16].copy_from_slice(&0_u32.to_le_bytes());
252 encoded[16..24].copy_from_slice(&manifest.generation.to_le_bytes());
253 encoded[24..32].copy_from_slice(&manifest.active_segment.to_le_bytes());
254 encoded[32..40].copy_from_slice(&manifest.base_sequence.to_le_bytes());
255 encoded[40..72].copy_from_slice(&manifest.base_digest);
256 encoded[72..104].copy_from_slice(&manifest.snapshot_digest);
257 let checksum = crc32c::crc32c(&encoded[..CHECKSUM_PREFIX_LENGTH]);
258 encoded[104..108].copy_from_slice(&checksum.to_le_bytes());
259 let digest = blake3::hash(&encoded[..DIGEST_PREFIX_LENGTH]);
260 encoded[108..140].copy_from_slice(digest.as_bytes());
261 encoded
262}
263
264fn decode_manifest(
265 path: &Path,
266 filename_generation: u64,
267) -> Result<StorageManifest, ManifestError> {
268 let mut file = File::open(path)?;
269 if file.metadata()?.len() != ENCODED_LENGTH_U64 {
270 return Err(invalid(path, "file length mismatch"));
271 }
272 let mut encoded = [0_u8; ENCODED_LENGTH];
273 file.read_exact(&mut encoded)?;
274 if encoded[..8] != MAGIC {
275 return Err(invalid(path, "bad magic"));
276 }
277 let manifest_version = u16::from_le_bytes(copy_array(&encoded[8..10]));
278 if manifest_version != MANIFEST_FORMAT_VERSION {
279 return Err(ManifestError::UnsupportedVersion {
280 found: manifest_version,
281 supported: MANIFEST_FORMAT_VERSION,
282 });
283 }
284 let disk_format = u16::from_le_bytes(copy_array(&encoded[10..12]));
285 if !(MIN_DISK_FORMAT_VERSION..=DISK_FORMAT_VERSION).contains(&disk_format) {
286 return Err(invalid(path, "disk format mismatch"));
287 }
288 if u32::from_le_bytes(copy_array(&encoded[12..16])) != 0 {
289 return Err(invalid(path, "unsupported flags"));
290 }
291 let expected_checksum = u32::from_le_bytes(copy_array(&encoded[104..108]));
292 if crc32c::crc32c(&encoded[..CHECKSUM_PREFIX_LENGTH]) != expected_checksum {
293 return Err(invalid(path, "CRC32C mismatch"));
294 }
295 let expected_digest: [u8; 32] = copy_array(&encoded[108..140]);
296 if *blake3::hash(&encoded[..DIGEST_PREFIX_LENGTH]).as_bytes() != expected_digest {
297 return Err(invalid(path, "BLAKE3 digest mismatch"));
298 }
299
300 let manifest = StorageManifest {
301 generation: u64::from_le_bytes(copy_array(&encoded[16..24])),
302 active_segment: u64::from_le_bytes(copy_array(&encoded[24..32])),
303 base_sequence: u64::from_le_bytes(copy_array(&encoded[32..40])),
304 base_digest: copy_array(&encoded[40..72]),
305 snapshot_digest: copy_array(&encoded[72..104]),
306 };
307 if manifest.generation != filename_generation {
308 return Err(invalid(path, "filename generation mismatch"));
309 }
310 validate_semantics(&manifest, path)?;
311 Ok(manifest)
312}
313
314fn validate_semantics(manifest: &StorageManifest, path: &Path) -> Result<(), ManifestError> {
315 if manifest.generation == 0 || manifest.active_segment != manifest.generation {
316 return Err(invalid(path, "invalid generation or active segment"));
317 }
318 let empty_anchor = manifest.base_sequence == 0;
319 if empty_anchor != (manifest.base_digest == [0; 32])
320 || empty_anchor != (manifest.snapshot_digest == [0; 32])
321 {
322 return Err(invalid(path, "inconsistent snapshot anchor"));
323 }
324 if manifest.generation == 1 && !empty_anchor {
325 return Err(invalid(path, "initial generation has a snapshot anchor"));
326 }
327 if manifest.generation > 1 && empty_anchor {
328 return Err(invalid(
329 path,
330 "compacted generation lacks a snapshot anchor",
331 ));
332 }
333 Ok(())
334}
335
336fn invalid(path: &Path, reason: &'static str) -> ManifestError {
337 ManifestError::Invalid {
338 path: path.to_path_buf(),
339 reason,
340 }
341}
342
343#[cfg(unix)]
344fn sync_directory(path: &Path) -> Result<(), ManifestError> {
345 File::open(path)?.sync_all()?;
346 Ok(())
347}
348
349fn copy_array<const N: usize>(source: &[u8]) -> [u8; N] {
350 let mut output = [0_u8; N];
351 output.copy_from_slice(source);
352 output
353}
354
355#[cfg(test)]
356mod tests {
357 use std::{error::Error, fs, io::Write};
358
359 use super::{ManifestError, StorageManifest};
360 use crate::{StorageLimitError, storage_limit_from_io, test_support::TestDirectory};
361
362 fn initialize_layout(root: &std::path::Path) -> Result<(), Box<dyn Error>> {
363 fs::create_dir_all(root.join("manifest"))?;
364 fs::create_dir_all(root.join("tmp"))?;
365 Ok(())
366 }
367
368 #[test]
369 fn initializes_and_reloads_an_immutable_manifest() -> Result<(), Box<dyn Error>> {
370 let temporary = TestDirectory::new("manifest-initial")?;
371 initialize_layout(temporary.path())?;
372
373 let created = StorageManifest::load_or_initialize(temporary.path())?;
374 let reopened = StorageManifest::load_or_initialize(temporary.path())?;
375 assert_eq!(created, reopened);
376 assert_eq!(created.generation, 1);
377 assert_eq!(created.base_sequence, 0);
378 Ok(())
379 }
380
381 #[test]
382 fn ignores_an_interrupted_temporary_manifest() -> Result<(), Box<dyn Error>> {
383 let temporary = TestDirectory::new("manifest-interrupted")?;
384 initialize_layout(temporary.path())?;
385 let mut partial = fs::File::create(temporary.path().join("tmp/manifest-partial.tmp"))?;
386 partial.write_all(b"partial")?;
387 partial.sync_all()?;
388
389 let manifest = StorageManifest::load_or_initialize(temporary.path())?;
390 assert_eq!(manifest.generation, 1);
391 Ok(())
392 }
393
394 #[test]
395 fn rejects_corruption_in_the_latest_generation() -> Result<(), Box<dyn Error>> {
396 let temporary = TestDirectory::new("manifest-corrupt")?;
397 initialize_layout(temporary.path())?;
398 StorageManifest::load_or_initialize(temporary.path())?;
399 let path = temporary
400 .path()
401 .join("manifest/00000000000000000001.hymanifest");
402 let mut bytes = fs::read(&path)?;
403 bytes[40] ^= 1;
404 fs::write(&path, bytes)?;
405
406 assert!(matches!(
407 StorageManifest::load_or_initialize(temporary.path()),
408 Err(ManifestError::Invalid {
409 reason: "CRC32C mismatch",
410 ..
411 })
412 ));
413 Ok(())
414 }
415
416 #[test]
417 fn removes_a_temporary_manifest_after_a_pre_rename_failure() -> Result<(), Box<dyn Error>> {
418 let temporary = TestDirectory::new("manifest-temporary-cleanup")?;
419 initialize_layout(temporary.path())?;
420 let manifest = StorageManifest::initial();
421
422 assert!(matches!(
423 manifest.write_new_with_injected_temporary_failure(temporary.path()),
424 Err(ManifestError::Io(_))
425 ));
426 assert_eq!(fs::read_dir(temporary.path().join("tmp"))?.count(), 0);
427 assert_eq!(fs::read_dir(temporary.path().join("manifest"))?.count(), 0);
428 Ok(())
429 }
430
431 #[test]
432 fn reserves_capacity_before_initializing_a_manifest() -> Result<(), Box<dyn Error>> {
433 let temporary = TestDirectory::new("manifest-initial-capacity")?;
434 initialize_layout(temporary.path())?;
435 fs::write(temporary.path().join("manifest/occupied"), b"occupied")?;
436 let deadline = crate::limits::OperationDeadline::new(std::time::Duration::from_secs(1));
437
438 assert!(matches!(
439 StorageManifest::load_or_initialize_with_limits(temporary.path(), 1, &deadline),
440 Err(ManifestError::Io(source))
441 if matches!(
442 storage_limit_from_io(&source),
443 Some(StorageLimitError::DirectoryEntriesExceeded { maximum: 1 })
444 )
445 ));
446 assert!(
447 !temporary
448 .path()
449 .join("manifest/00000000000000000001.hymanifest")
450 .exists()
451 );
452 Ok(())
453 }
454}