1use super::{
4 AttributeName, AttributePage, BlobNode, DecodeLimits, ExtentKind, ExtentPage, FilePayload,
5 FileRecord, FileTablePage, GenerationRoot, LogicalName, MetadataField, TreeEntry, TreePage,
6 decode_attribute_page, decode_blob_page, decode_extent_page, decode_file_metadata,
7 decode_file_table_page, decode_generation_root, decode_tree_page,
8};
9use crate::foundation::{FileId, GenerationId};
10use crate::model::FilesystemProfile;
11use crate::performance::{OperationFailure, WorkBudget, WorkCounters, WorkError};
12use crate::storage::{ObjectId, ObjectKind, ObjectStoreError};
13use crate::{CancellationToken, async_storage};
14use bytes::Bytes;
15use std::collections::{BTreeMap, HashMap, HashSet};
16use std::mem::size_of;
17use thiserror::Error;
18
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
21pub struct ClosureLimits {
22 pub decode: DecodeLimits,
24 pub maximum_objects: u64,
26 pub maximum_files: u64,
28 pub maximum_object_bytes: u64,
30 pub profile: FilesystemProfile,
32 pub symbolic_links: bool,
34 pub hard_links: bool,
36 pub sparse_files: bool,
38}
39
40#[derive(Clone, Debug, Eq, PartialEq)]
42pub struct GenerationProof {
43 pub root: GenerationRoot,
45 pub generation_id: GenerationId,
47 pub object_count: u64,
49 pub file_count: u64,
51 pub objects: Vec<ObjectId>,
53 pub work: WorkCounters,
55}
56
57pub type GenerationProofFailure = OperationFailure<ClosureError>;
59
60pub fn prove_generation_closure<S: crate::ImmediateObjectStore>(
68 store: &S,
69 generation_root: ObjectId,
70 limits: ClosureLimits,
71 budget: WorkBudget,
72) -> Result<GenerationProof, GenerationProofFailure> {
73 let cancellation = CancellationToken::new();
74 let mut context = ProofContext::new(store, limits, budget, &cancellation);
75 async_storage::poll_immediate(context.prove(generation_root))
76 .map_err(|error| OperationFailure::new(error, context.work))
77}
78
79pub async fn prove_generation_closure_async<S: crate::AsyncObjectStore>(
89 store: &S,
90 generation_root: ObjectId,
91 limits: ClosureLimits,
92 budget: WorkBudget,
93 cancellation: &CancellationToken,
94) -> Result<GenerationProof, GenerationProofFailure> {
95 let mut context = ProofContext::new(store, limits, budget, cancellation);
96 context
97 .prove(generation_root)
98 .await
99 .map_err(|error| OperationFailure::new(error, context.work))
100}
101
102#[cfg(feature = "s3-http")]
103pub(crate) async fn prove_blob_closure_async<S: crate::AsyncObjectStore>(
104 store: &S,
105 root: ObjectId,
106 expected_bytes: u64,
107 limits: ClosureLimits,
108 budget: WorkBudget,
109 cancellation: &CancellationToken,
110) -> Result<(Vec<ObjectId>, WorkCounters), GenerationProofFailure> {
111 let mut context = ProofContext::new(store, limits, budget, cancellation);
112 let logical_bytes = context
113 .prove_blob(root)
114 .await
115 .map_err(|error| OperationFailure::new(error, context.work))?;
116 if logical_bytes != expected_bytes {
117 return Err(OperationFailure::new(
118 ClosureError::BlobLengthMismatch,
119 context.work,
120 ));
121 }
122 let mut objects = context.objects.into_iter().collect::<Vec<_>>();
123 objects.sort_unstable();
124 Ok((objects, context.work))
125}
126
127struct ProofContext<'a, S> {
128 store: &'a S,
129 limits: ClosureLimits,
130 budget: WorkBudget,
131 work: WorkCounters,
132 objects: HashSet<ObjectId>,
133 blob_lengths: HashMap<ObjectId, u64>,
134 cancellation: &'a CancellationToken,
135}
136
137impl<'a, S: crate::AsyncObjectStore> ProofContext<'a, S> {
138 fn new(
139 store: &'a S,
140 limits: ClosureLimits,
141 budget: WorkBudget,
142 cancellation: &'a CancellationToken,
143 ) -> Self {
144 Self {
145 store,
146 limits,
147 budget,
148 work: WorkCounters::default(),
149 objects: HashSet::new(),
150 blob_lengths: HashMap::new(),
151 cancellation,
152 }
153 }
154
155 async fn prove(&mut self, generation_root: ObjectId) -> Result<GenerationProof, ClosureError> {
156 Self::require_kind(generation_root, ObjectKind::GenerationRoot)?;
157 let root_bytes = self.read_object(generation_root, false).await?;
158 let root = decode_generation_root(&root_bytes, self.limits.decode)?;
159 let generation_id = GenerationId::new(generation_root.digest);
160 let records = self.prove_file_table(root.file_table).await?;
161 let file_count = u64::try_from(records.len()).unwrap_or(u64::MAX);
162 if file_count > self.limits.maximum_files {
163 return Err(ClosureError::TooManyFiles {
164 observed: file_count,
165 maximum: self.limits.maximum_files,
166 });
167 }
168 self.prove_records_and_namespace(root.root_file_id, &records)
169 .await?;
170 let mut objects: Vec<ObjectId> = self.objects.iter().copied().collect();
171 objects.sort_unstable_by(|left, right| {
172 left.kind
173 .canonical_tag()
174 .cmp(&right.kind.canonical_tag())
175 .then_with(|| left.digest.as_bytes().cmp(right.digest.as_bytes()))
176 });
177 let object_bytes = u64::try_from(objects.capacity())
178 .unwrap_or(u64::MAX)
179 .saturating_mul(u64::try_from(size_of::<ObjectId>()).unwrap_or(u64::MAX));
180 self.work = self
181 .work
182 .checked_add(WorkCounters {
183 items_examined: u64::try_from(objects.len()).unwrap_or(u64::MAX),
184 bytes_copied: u64::try_from(objects.len())
185 .unwrap_or(u64::MAX)
186 .saturating_mul(u64::try_from(size_of::<ObjectId>()).unwrap_or(u64::MAX)),
187 allocation_operations: u64::from(!objects.is_empty()),
188 peak_allocation_bytes: object_bytes,
189 ..WorkCounters::default()
190 })
191 .map_err(ClosureError::Work)?;
192 self.work.verify(self.budget).map_err(ClosureError::Work)?;
193 Ok(GenerationProof {
194 root,
195 generation_id,
196 object_count: u64::try_from(self.objects.len()).unwrap_or(u64::MAX),
197 file_count,
198 objects,
199 work: self.work,
200 })
201 }
202
203 async fn read_object(&mut self, object: ObjectId, page: bool) -> Result<Bytes, ClosureError> {
204 self.objects.insert(object);
205 let count = u64::try_from(self.objects.len()).unwrap_or(u64::MAX);
206 if count > self.limits.maximum_objects {
207 return Err(ClosureError::TooManyObjects {
208 observed: count,
209 maximum: self.limits.maximum_objects,
210 });
211 }
212 let semantic = WorkCounters {
213 page_reads: u64::from(page),
214 ..WorkCounters::default()
215 };
216 let prospective = self.work.checked_add(semantic)?;
217 let remaining = prospective.remaining(self.budget)?;
218 let receipt = crate::AsyncObjectStore::read(
219 self.store,
220 object,
221 self.limits.decode.maximum_object_bytes,
222 remaining,
223 self.cancellation,
224 )
225 .await
226 .map_err(|failure| match prospective.checked_add(*failure.work) {
227 Ok(combined) => {
228 self.work = combined;
229 ClosureError::Storage(failure.error)
230 }
231 Err(error) => {
232 self.work = prospective;
233 ClosureError::Work(error)
234 }
235 })?;
236 self.work = prospective.checked_add(receipt.work)?;
237 self.work.verify(self.budget)?;
238 if self.work.object_bytes_read > self.limits.maximum_object_bytes {
239 return Err(ClosureError::ClosureBytesExceeded {
240 observed: self.work.object_bytes_read,
241 maximum: self.limits.maximum_object_bytes,
242 });
243 }
244 Ok(receipt.value.bytes)
245 }
246
247 async fn prove_file_table(
248 &mut self,
249 root: ObjectId,
250 ) -> Result<BTreeMap<FileId, FileRecord>, ClosureError> {
251 Self::require_kind(root, ObjectKind::FileTablePage)?;
252 let mut records = BTreeMap::new();
253 let mut pending = vec![(root, None, None)];
254 let mut pages = HashSet::new();
255 while let Some((page_id, lower, upper)) = pending.pop() {
256 if !pages.insert(page_id) {
257 return Err(ClosureError::RoutingAlias);
258 }
259 let bytes = self.read_object(page_id, true).await?;
260 match decode_file_table_page(&bytes, self.limits.decode)? {
261 FileTablePage::Leaf(entries) => {
262 validate_file_bounds(&entries, lower, upper)?;
263 for record in entries {
264 if records.insert(record.file_id, record).is_some() {
265 return Err(ClosureError::DuplicateFileRecord(record.file_id));
266 }
267 }
268 }
269 FileTablePage::Internal(children) => {
270 validate_file_child_bounds(&children, lower, upper)?;
271 for (index, child) in children.iter().enumerate().rev() {
272 let child = *child;
273 let child_upper = children
274 .get(index + 1)
275 .map(|next| next.first_file_id)
276 .or(upper);
277 pending.push((child.page, Some(child.first_file_id), child_upper));
278 }
279 }
280 }
281 }
282 Ok(records)
283 }
284
285 async fn prove_records_and_namespace(
286 &mut self,
287 root_file_id: FileId,
288 records: &BTreeMap<FileId, FileRecord>,
289 ) -> Result<(), ClosureError> {
290 let root = records
291 .get(&root_file_id)
292 .ok_or(ClosureError::MissingRootFile)?;
293 if !matches!(root.payload, FilePayload::Directory { .. }) {
294 return Err(ClosureError::RootIsNotDirectory);
295 }
296 let mut directory_entries = BTreeMap::new();
297 for record in records.values() {
298 if !record.kind.is_supported_by_profile(self.limits.profile)
299 || (record.kind == super::FileKind::SymbolicLink && !self.limits.symbolic_links)
300 || (record.link_count > 1 && !self.limits.hard_links)
301 {
302 return Err(ClosureError::UnsupportedVolumeSemantics);
303 }
304 self.prove_metadata(record.metadata).await?;
305 match record.payload {
306 FilePayload::Regular {
307 logical_bytes,
308 extents,
309 } => {
310 self.prove_extent_tree(extents, logical_bytes).await?;
311 }
312 FilePayload::Directory { entries } => {
313 directory_entries.insert(record.file_id, self.prove_tree(entries).await?);
314 }
315 FilePayload::SymbolicLink {
316 target_bytes,
317 target,
318 }
319 | FilePayload::ReparsePoint {
320 payload_bytes: target_bytes,
321 payload: target,
322 } => {
323 if self.prove_blob(target).await? != target_bytes {
324 return Err(ClosureError::BlobLengthMismatch);
325 }
326 }
327 FilePayload::InlineRegular(_) | FilePayload::Empty | FilePayload::Device { .. } => {
328 }
329 }
330 }
331 Self::validate_namespace(root_file_id, records, &directory_entries)
332 }
333
334 async fn prove_metadata(&mut self, metadata_id: ObjectId) -> Result<(), ClosureError> {
335 Self::require_kind(metadata_id, ObjectKind::Metadata)?;
336 if self.objects.contains(&metadata_id) {
341 return Ok(());
342 }
343 let bytes = self.read_object(metadata_id, false).await?;
344 let metadata = decode_file_metadata(&bytes, self.limits.decode)?;
345 if let MetadataField::Value(attributes) = metadata.named_attributes {
346 self.prove_attributes(attributes).await?;
347 }
348 for blob in [metadata.acl, metadata.security_descriptor] {
349 if let MetadataField::Value(blob) = blob {
350 self.prove_blob(blob).await?;
351 }
352 }
353 Ok(())
354 }
355
356 async fn prove_tree(&mut self, root: ObjectId) -> Result<Vec<TreeEntry>, ClosureError> {
357 Self::require_kind(root, ObjectKind::TreePage)?;
358 let mut entries = Vec::new();
359 let mut pending = vec![(root, None, None)];
360 let mut pages = HashSet::new();
361 while let Some((page_id, lower, upper)) = pending.pop() {
362 if !pages.insert(page_id) {
363 return Err(ClosureError::RoutingAlias);
364 }
365 let bytes = self.read_object(page_id, true).await?;
366 match decode_tree_page(&bytes, self.limits.decode)? {
367 TreePage::Leaf(values) => {
368 validate_tree_bounds(&values, lower.as_ref(), upper.as_ref())?;
369 entries.extend(values);
370 }
371 TreePage::Internal(children) => {
372 validate_tree_child_bounds(&children, lower.as_ref(), upper.as_ref())?;
373 for (index, child) in children.iter().enumerate().rev() {
374 let child = child.clone();
375 let child_upper = children
376 .get(index + 1)
377 .map(|next| next.first_name.clone())
378 .or_else(|| upper.clone());
379 pending.push((child.page, Some(child.first_name), child_upper));
380 }
381 }
382 }
383 }
384 Ok(entries)
385 }
386
387 async fn prove_extent_tree(&mut self, root: ObjectId, size: u64) -> Result<(), ClosureError> {
388 Self::require_kind(root, ObjectKind::ExtentPage)?;
389 let mut pending = vec![(root, 0_u64, size)];
390 let mut pages = HashSet::new();
391 while let Some((page_id, first, end)) = pending.pop() {
392 if !pages.insert(page_id) {
393 return Err(ClosureError::RoutingAlias);
394 }
395 let bytes = self.read_object(page_id, true).await?;
396 match decode_extent_page(&bytes, self.limits.decode)? {
397 ExtentPage::Leaf(extents) => {
398 validate_extent_bounds(&extents, first, end)?;
399 for extent in extents {
400 if !self.limits.sparse_files
401 && !matches!(extent.kind, ExtentKind::Content { .. })
402 {
403 return Err(ClosureError::UnsupportedVolumeSemantics);
404 }
405 if let ExtentKind::Content {
406 object,
407 object_offset,
408 } = extent.kind
409 {
410 let content_end = object_offset
411 .checked_add(extent.length)
412 .ok_or(ClosureError::RangeOverflow)?;
413 if content_end > self.prove_blob(object).await? {
414 return Err(ClosureError::BlobSpanOutsideObject);
415 }
416 }
417 }
418 }
419 ExtentPage::Internal(children) => {
420 if children.first().map(|child| child.first_offset) != Some(first)
421 || children.last().map(|child| child.end_offset) != Some(end)
422 {
423 return Err(ClosureError::RoutingBoundsMismatch);
424 }
425 for child in children.into_iter().rev() {
426 pending.push((child.page, child.first_offset, child.end_offset));
427 }
428 }
429 }
430 }
431 Ok(())
432 }
433
434 async fn prove_blob(&mut self, root: ObjectId) -> Result<u64, ClosureError> {
435 Self::require_kind(root, ObjectKind::Blob)?;
436 if let Some(length) = self.blob_lengths.get(&root) {
437 return Ok(*length);
438 }
439 let mut pending = vec![(root, 0_u64, None)];
440 let mut pages = HashSet::new();
441 let mut root_length = None;
442 while let Some((page_id, first, end)) = pending.pop() {
443 if !pages.insert(page_id) {
444 return Err(ClosureError::RoutingAlias);
445 }
446 let bytes = self.read_object(page_id, true).await?;
447 let page = decode_blob_page(&bytes, self.limits.decode)?;
448 if page.first_offset != first || end.is_some_and(|value| page.end_offset != value) {
449 return Err(ClosureError::RoutingBoundsMismatch);
450 }
451 if root_length.is_none() {
452 root_length = Some(page.end_offset);
453 }
454 match page.node {
455 BlobNode::Internal(children) => {
456 for child in children.into_iter().rev() {
457 pending.push((child.page, child.first_offset, Some(child.end_offset)));
458 }
459 }
460 BlobNode::Leaf(chunks) => {
461 for chunk in chunks {
462 let expected = chunk.end_offset - chunk.first_offset;
463 Self::require_kind(chunk.chunk, ObjectKind::BlobChunk)?;
464 let value = self.read_object(chunk.chunk, false).await?;
465 if u64::try_from(value.len()).unwrap_or(u64::MAX) != expected {
466 return Err(ClosureError::BlobLengthMismatch);
467 }
468 }
469 }
470 }
471 }
472 let length = root_length.ok_or(ClosureError::RoutingBoundsMismatch)?;
473 self.blob_lengths.insert(root, length);
474 Ok(length)
475 }
476
477 async fn prove_attributes(&mut self, root: ObjectId) -> Result<(), ClosureError> {
478 Self::require_kind(root, ObjectKind::AttributePage)?;
479 let mut pending = vec![(root, None, None)];
480 let mut pages = HashSet::new();
481 while let Some((page_id, lower, upper)) = pending.pop() {
482 if !pages.insert(page_id) {
483 return Err(ClosureError::RoutingAlias);
484 }
485 let bytes = self.read_object(page_id, true).await?;
486 match decode_attribute_page(&bytes, self.limits.decode)? {
487 AttributePage::Leaf(entries) => {
488 validate_attribute_bounds(&entries, lower.as_ref(), upper.as_ref())?;
489 for entry in entries {
490 if self.prove_blob(entry.value).await? != entry.value_bytes {
491 return Err(ClosureError::BlobLengthMismatch);
492 }
493 }
494 }
495 AttributePage::Internal(children) => {
496 validate_attribute_child_bounds(&children, lower.as_ref(), upper.as_ref())?;
497 for (index, child) in children.iter().enumerate().rev() {
498 let child = child.clone();
499 let child_upper = children
500 .get(index + 1)
501 .map(|next| next.first_name.clone())
502 .or_else(|| upper.clone());
503 pending.push((child.page, Some(child.first_name), child_upper));
504 }
505 }
506 }
507 }
508 Ok(())
509 }
510
511 fn validate_namespace(
512 root: FileId,
513 records: &BTreeMap<FileId, FileRecord>,
514 directories: &BTreeMap<FileId, Vec<TreeEntry>>,
515 ) -> Result<(), ClosureError> {
516 let mut links: BTreeMap<FileId, u64> = BTreeMap::new();
517 links.insert(root, 1);
518 for entries in directories.values() {
519 for entry in entries {
520 let record = records
521 .get(&entry.file_id)
522 .ok_or(ClosureError::MissingFileRecord(entry.file_id))?;
523 if record.kind != entry.kind {
524 return Err(ClosureError::NamespaceKindMismatch(entry.file_id));
525 }
526 let count = links.entry(entry.file_id).or_default();
527 *count = count.checked_add(1).ok_or(ClosureError::RangeOverflow)?;
528 }
529 }
530 for record in records.values() {
531 if links.get(&record.file_id).copied().unwrap_or(0) != record.link_count {
532 return Err(ClosureError::LinkCountMismatch(record.file_id));
533 }
534 if matches!(record.payload, FilePayload::Directory { .. }) && record.link_count != 1 {
535 return Err(ClosureError::DirectoryHardLink(record.file_id));
536 }
537 }
538 let mut reachable = HashSet::new();
539 let mut visited_directories = HashSet::new();
540 let mut pending = vec![root];
541 while let Some(file_id) = pending.pop() {
542 if !visited_directories.insert(file_id) {
543 return Err(ClosureError::DirectoryHardLink(file_id));
544 }
545 reachable.insert(file_id);
546 let entries = directories
547 .get(&file_id)
548 .ok_or(ClosureError::MissingDirectoryTree(file_id))?;
549 for entry in entries {
550 reachable.insert(entry.file_id);
551 if directories.contains_key(&entry.file_id) {
552 pending.push(entry.file_id);
553 }
554 }
555 }
556 if reachable.len() != records.len() {
557 return Err(ClosureError::UnreachableFileRecords);
558 }
559 Ok(())
560 }
561
562 fn require_kind(object: ObjectId, expected: ObjectKind) -> Result<(), ClosureError> {
563 if object.kind == expected {
564 Ok(())
565 } else {
566 Err(ClosureError::WrongObjectKind)
567 }
568 }
569}
570
571fn validate_file_bounds(
572 records: &[FileRecord],
573 lower: Option<FileId>,
574 upper: Option<FileId>,
575) -> Result<(), ClosureError> {
576 if lower.is_some_and(|value| records.first().map(|record| record.file_id) != Some(value))
577 || upper.is_some_and(|value| records.last().is_some_and(|record| record.file_id >= value))
578 {
579 return Err(ClosureError::RoutingBoundsMismatch);
580 }
581 Ok(())
582}
583
584fn validate_file_child_bounds(
585 children: &[super::FileTableChild],
586 lower: Option<FileId>,
587 upper: Option<FileId>,
588) -> Result<(), ClosureError> {
589 if lower.is_some_and(|value| children.first().map(|child| child.first_file_id) != Some(value))
590 || upper.is_some_and(|value| {
591 children
592 .last()
593 .is_some_and(|child| child.first_file_id >= value)
594 })
595 {
596 return Err(ClosureError::RoutingBoundsMismatch);
597 }
598 Ok(())
599}
600
601fn validate_tree_bounds(
602 entries: &[TreeEntry],
603 lower: Option<&LogicalName>,
604 upper: Option<&LogicalName>,
605) -> Result<(), ClosureError> {
606 if lower.is_some_and(|value| entries.first().map(|entry| &entry.name) != Some(value))
607 || upper.is_some_and(|value| entries.last().is_some_and(|entry| entry.name >= *value))
608 {
609 return Err(ClosureError::RoutingBoundsMismatch);
610 }
611 Ok(())
612}
613
614fn validate_tree_child_bounds(
615 children: &[super::TreeChild],
616 lower: Option<&LogicalName>,
617 upper: Option<&LogicalName>,
618) -> Result<(), ClosureError> {
619 if lower.is_some_and(|value| children.first().map(|child| &child.first_name) != Some(value))
620 || upper.is_some_and(|value| {
621 children
622 .last()
623 .is_some_and(|child| child.first_name >= *value)
624 })
625 {
626 return Err(ClosureError::RoutingBoundsMismatch);
627 }
628 Ok(())
629}
630
631fn validate_extent_bounds(
632 extents: &[super::Extent],
633 first: u64,
634 end: u64,
635) -> Result<(), ClosureError> {
636 if first == end && extents.is_empty() {
637 return Ok(());
638 }
639 if extents.first().map(|extent| extent.offset) != Some(first)
640 || extents
641 .last()
642 .and_then(|extent| extent.offset.checked_add(extent.length))
643 != Some(end)
644 {
645 return Err(ClosureError::RoutingBoundsMismatch);
646 }
647 Ok(())
648}
649
650fn validate_attribute_bounds(
651 entries: &[super::AttributeEntry],
652 lower: Option<&AttributeName>,
653 upper: Option<&AttributeName>,
654) -> Result<(), ClosureError> {
655 if lower.is_some_and(|value| entries.first().map(|entry| &entry.name) != Some(value))
656 || upper.is_some_and(|value| entries.last().is_some_and(|entry| entry.name >= *value))
657 {
658 return Err(ClosureError::RoutingBoundsMismatch);
659 }
660 Ok(())
661}
662
663fn validate_attribute_child_bounds(
664 children: &[super::AttributeChild],
665 lower: Option<&AttributeName>,
666 upper: Option<&AttributeName>,
667) -> Result<(), ClosureError> {
668 if lower.is_some_and(|value| children.first().map(|child| &child.first_name) != Some(value))
669 || upper.is_some_and(|value| {
670 children
671 .last()
672 .is_some_and(|child| child.first_name >= *value)
673 })
674 {
675 return Err(ClosureError::RoutingBoundsMismatch);
676 }
677 Ok(())
678}
679
680#[derive(Debug, Error)]
682pub enum ClosureError {
683 #[error("generation closure object has the wrong kind")]
685 WrongObjectKind,
686 #[error("routing graph contains a cycle or alias")]
688 RoutingAlias,
689 #[error("routing bounds do not match authenticated child content")]
691 RoutingBoundsMismatch,
692 #[error("generation closure has {observed} objects; maximum is {maximum}")]
694 TooManyObjects {
695 observed: u64,
697 maximum: u64,
699 },
700 #[error("generation closure has {observed} files; maximum is {maximum}")]
702 TooManyFiles {
703 observed: u64,
705 maximum: u64,
707 },
708 #[error("generation closure read {observed} bytes; maximum is {maximum}")]
710 ClosureBytesExceeded {
711 observed: u64,
713 maximum: u64,
715 },
716 #[error("duplicate file record {0:?}")]
718 DuplicateFileRecord(FileId),
719 #[error("root file record is missing")]
721 MissingRootFile,
722 #[error("root file record is not a directory")]
724 RootIsNotDirectory,
725 #[error("generation closure uses semantics unsupported by its volume configuration")]
727 UnsupportedVolumeSemantics,
728 #[error("namespace references missing file record {0:?}")]
730 MissingFileRecord(FileId),
731 #[error("namespace kind differs from file record {0:?}")]
733 NamespaceKindMismatch(FileId),
734 #[error("file link count is incorrect for {0:?}")]
736 LinkCountMismatch(FileId),
737 #[error("directory has multiple namespace links {0:?}")]
739 DirectoryHardLink(FileId),
740 #[error("directory record has no namespace tree {0:?}")]
742 MissingDirectoryTree(FileId),
743 #[error("file table contains unreachable records")]
745 UnreachableFileRecords,
746 #[error("blob logical length does not match its declaration")]
748 BlobLengthMismatch,
749 #[error("extent content span exceeds its blob")]
751 BlobSpanOutsideObject,
752 #[error("generation closure range arithmetic overflowed")]
754 RangeOverflow,
755 #[error(transparent)]
757 Storage(#[from] ObjectStoreError),
758 #[error(transparent)]
760 Decode(#[from] super::CanonicalDecodeError),
761 #[error(transparent)]
763 Work(#[from] WorkError),
764}
765
766#[cfg(all(test, feature = "memory"))]
767#[path = "tests/closure.rs"]
768mod tests;