1use super::{
4 AsyncRebaseProbe, BlobReadError, DecodeLimits, Dependency, DependencyError, DependencyRegion,
5 DependencyState, DirectoryReadError, ExtentKind, ExtentReadError, ExtentSeekRequest,
6 ExtentSeekTarget, FilePayload, FileRecord, FileRecordReadError, GenerationRoot, ProbeReceipt,
7 RebaseProbe, TreeReadError, decode_generation_root, encode_tree_page, list_tree_entries,
8 list_tree_entries_async, lookup_file_record, lookup_file_record_async, lookup_tree_entry,
9 lookup_tree_entry_async, plan_extent_range, plan_extent_range_async, read_blob_range,
10 read_blob_range_async, seek_extent, seek_extent_async,
11};
12use crate::async_storage::AsyncObjectStore;
13use crate::cancellation::CancellationToken;
14use crate::foundation::{Digest, FileId, GenerationId};
15use crate::performance::{OperationFailure, OperationReceipt, WorkBudget, WorkCounters, WorkError};
16use crate::storage::{ByteRange, ObjectId, ObjectKind, ObjectStoreError};
17use std::collections::HashMap;
18use std::hash::Hash;
19use std::mem::size_of;
20use std::sync::Mutex;
21use thiserror::Error;
22
23const ZERO_HASH_BLOCK: [u8; 8 * 1024] = [0; 8 * 1024];
24const CONTENT_RANGE_DOMAIN: &[u8] = b"acyclic-fs-dependency-content-range-v1\0";
25
26pub fn capture_content_range_bytes(
37 bytes: &[u8],
38 maximum_bytes: u64,
39 budget: WorkBudget,
40) -> Result<ProbeReceipt, ProbeFailure> {
41 if maximum_bytes == 0 {
42 return Err(failed(
43 AuthenticatedProbeError::InvalidLimits,
44 WorkCounters::default(),
45 ));
46 }
47 if bytes.is_empty() {
48 return Err(failed(
49 AuthenticatedProbeError::InvalidDependency(DependencyError::EmptyContentRange),
50 WorkCounters::default(),
51 ));
52 }
53 if u64::try_from(bytes.len()).unwrap_or(u64::MAX) > maximum_bytes {
54 return Err(failed(
55 AuthenticatedProbeError::ContentRangeTooLarge,
56 WorkCounters::default(),
57 ));
58 }
59 hash_content_range(bytes, budget, WorkCounters::default())
60}
61
62pub struct AuthenticatedGenerationProbe<'a, S> {
67 store: &'a S,
68 limits: ProbeLimits,
69 generations: Mutex<HashMap<GenerationId, GenerationRoot>>,
70 records: Mutex<HashMap<(GenerationId, FileId), Option<FileRecord>>>,
71}
72
73#[derive(Clone, Copy, Debug, Eq, PartialEq)]
75pub struct ProbeLimits {
76 pub decode: DecodeLimits,
78 pub maximum_cached_generations: u32,
80 pub maximum_cached_records: u32,
82 pub maximum_extent_spans: u32,
84 pub maximum_content_payload_bytes: u64,
86 pub maximum_directory_entries: u32,
88}
89
90impl Default for ProbeLimits {
91 fn default() -> Self {
92 Self {
93 decode: DecodeLimits::default(),
94 maximum_cached_generations: 64,
95 maximum_cached_records: 4_096,
96 maximum_extent_spans: 4_096,
97 maximum_content_payload_bytes: 16 * 1024 * 1024,
98 maximum_directory_entries: 1_024,
99 }
100 }
101}
102
103impl<'a, S> AuthenticatedGenerationProbe<'a, S> {
104 pub fn new(store: &'a S, limits: ProbeLimits) -> Result<Self, AuthenticatedProbeError> {
110 if limits.maximum_cached_generations == 0
111 || limits.maximum_cached_records == 0
112 || limits.maximum_extent_spans == 0
113 || limits.maximum_content_payload_bytes == 0
114 || limits.maximum_directory_entries == 0
115 || !limits.decode.page_limits_valid(1)
116 {
117 return Err(AuthenticatedProbeError::InvalidLimits);
118 }
119 Ok(Self {
120 store,
121 limits,
122 generations: Mutex::new(HashMap::new()),
123 records: Mutex::new(HashMap::new()),
124 })
125 }
126
127 fn validate_region(&self, region: &DependencyRegion) -> Result<FileId, ProbeFailure> {
128 region.validate().map_err(|error| {
129 failed(
130 AuthenticatedProbeError::InvalidDependency(error),
131 WorkCounters::default(),
132 )
133 })?;
134 match region {
135 DependencyRegion::ContentRange { length, .. }
136 if *length > self.limits.maximum_content_payload_bytes =>
137 {
138 return Err(failed(
139 AuthenticatedProbeError::ContentRangeTooLarge,
140 WorkCounters::default(),
141 ));
142 }
143 DependencyRegion::DirectoryRange {
144 maximum_entries, ..
145 } if *maximum_entries > self.limits.maximum_directory_entries => {
146 return Err(failed(
147 AuthenticatedProbeError::DirectoryPageTooLarge,
148 WorkCounters::default(),
149 ));
150 }
151 _ => {}
152 }
153 Ok(match region {
154 DependencyRegion::FileRecord(file_id)
155 | DependencyRegion::Metadata(file_id)
156 | DependencyRegion::FileLength(file_id)
157 | DependencyRegion::ContentRange { file_id, .. }
158 | DependencyRegion::SparseSeek { file_id, .. } => *file_id,
159 DependencyRegion::DirectoryName { directory_id, .. }
160 | DependencyRegion::DirectoryRange { directory_id, .. } => *directory_id,
161 })
162 }
163}
164
165impl<S: crate::ImmediateObjectStore> AuthenticatedGenerationProbe<'_, S> {
166 fn generation(
167 &self,
168 generation: GenerationId,
169 budget: WorkBudget,
170 ) -> Result<(GenerationRoot, WorkCounters), ProbeFailure> {
171 if let Some(root) = self
172 .generations
173 .lock()
174 .map_err(|_| {
175 failed(
176 AuthenticatedProbeError::CachePoisoned,
177 WorkCounters::default(),
178 )
179 })?
180 .get(&generation)
181 .cloned()
182 {
183 return Ok((root, WorkCounters::default()));
184 }
185 let object = ObjectId {
186 kind: ObjectKind::GenerationRoot,
187 digest: generation.digest(),
188 };
189 let receipt = self
190 .store
191 .read(object, self.limits.decode.maximum_object_bytes, budget)
192 .map_err(|failure| failed(failure.error.into(), *failure.work))?;
193 let root = decode_generation_root(&receipt.value, self.limits.decode)
194 .map_err(|error| failed(error.into(), receipt.work))?;
195 let mut generations = self
196 .generations
197 .lock()
198 .map_err(|_| failed(AuthenticatedProbeError::CachePoisoned, receipt.work))?;
199 clear_at_capacity(&mut generations, self.limits.maximum_cached_generations);
200 generations.insert(generation, root.clone());
201 Ok((root, receipt.work))
202 }
203
204 fn record(
205 &self,
206 generation: GenerationId,
207 file_id: FileId,
208 budget: WorkBudget,
209 ) -> Result<(Option<FileRecord>, WorkCounters), ProbeFailure> {
210 if let Some(record) = self
211 .records
212 .lock()
213 .map_err(|_| {
214 failed(
215 AuthenticatedProbeError::CachePoisoned,
216 WorkCounters::default(),
217 )
218 })?
219 .get(&(generation, file_id))
220 .copied()
221 {
222 return Ok((record, WorkCounters::default()));
223 }
224 let (root, mut work) = self.generation(generation, budget)?;
225 let remaining = work
226 .remaining(budget)
227 .map_err(|error| failed(error.into(), work))?;
228 let lookup = lookup_file_record(
229 self.store,
230 root.file_table,
231 file_id,
232 self.limits.decode,
233 remaining,
234 )
235 .map_err(|failure| failure.map_with_prior_work(work, Into::into))?;
236 work = work
237 .checked_add(lookup.work)
238 .map_err(|error| failed(error.into(), work))?;
239 let mut records = self
240 .records
241 .lock()
242 .map_err(|_| failed(AuthenticatedProbeError::CachePoisoned, work))?;
243 clear_at_capacity(&mut records, self.limits.maximum_cached_records);
244 records.insert((generation, file_id), lookup.record);
245 Ok((lookup.record, work))
246 }
247}
248
249pub(crate) fn capture_file_record_state(
250 record: FileRecord,
251 budget: WorkBudget,
252 mut work: WorkCounters,
253) -> Result<ProbeReceipt, ProbeFailure> {
254 let encoded =
255 super::file_table::encode_file_table_leaf_records(std::slice::from_ref(&record), 1)
256 .map_err(|error| failed(error.into(), work))?;
257 let domain = b"acyclic-fs-dependency-file-record-v1\0";
258 let value = DependencyState::Present(semantic_digest(domain, &encoded));
259 work = work
260 .checked_add(WorkCounters {
261 bytes_encoded: u64::try_from(encoded.len()).unwrap_or(u64::MAX),
262 bytes_hashed: u64::try_from(encoded.len())
263 .unwrap_or(u64::MAX)
264 .saturating_add(u64::try_from(domain.len()).unwrap_or(u64::MAX))
265 .saturating_add(8),
266 allocation_operations: 1,
267 peak_allocation_bytes: u64::try_from(encoded.capacity()).unwrap_or(u64::MAX),
268 ..WorkCounters::default()
269 })
270 .map_err(|error| failed(error.into(), work))?;
271 work.verify(budget)
272 .map_err(|error| failed(error.into(), work))?;
273 Ok(ProbeReceipt { value, work })
274}
275
276pub(crate) fn capture_sparse_seek_state(
277 target: ExtentSeekTarget,
278 result: Option<u64>,
279 budget: WorkBudget,
280 mut work: WorkCounters,
281) -> Result<ProbeReceipt, ProbeFailure> {
282 let mut encoded = [0_u8; 10];
283 encoded[0] = match target {
284 ExtentSeekTarget::Data => 0,
285 ExtentSeekTarget::Hole => 1,
286 };
287 if let Some(offset) = result {
288 encoded[1] = 1;
289 encoded[2..].copy_from_slice(&offset.to_le_bytes());
290 }
291 let domain = b"acyclic-fs-dependency-sparse-seek-v1\0";
292 let value = DependencyState::Present(semantic_digest(domain, &encoded));
293 work = work
294 .checked_add(WorkCounters {
295 bytes_encoded: u64::try_from(encoded.len()).unwrap_or(u64::MAX),
296 bytes_hashed: u64::try_from(encoded.len())
297 .unwrap_or(u64::MAX)
298 .saturating_add(u64::try_from(domain.len()).unwrap_or(u64::MAX))
299 .saturating_add(8),
300 ..WorkCounters::default()
301 })
302 .map_err(|error| failed(error.into(), work))?;
303 work.verify(budget)
304 .map_err(|error| failed(error.into(), work))?;
305 Ok(ProbeReceipt { value, work })
306}
307
308fn capture_file_length_state(
309 record: FileRecord,
310 budget: WorkBudget,
311 mut work: WorkCounters,
312) -> Result<ProbeReceipt, ProbeFailure> {
313 let logical_bytes = match record.payload {
314 FilePayload::InlineRegular(data) => {
315 u64::try_from(data.as_bytes().len()).unwrap_or(u64::MAX)
316 }
317 FilePayload::Regular { logical_bytes, .. } => logical_bytes,
318 FilePayload::Directory { .. }
319 | FilePayload::SymbolicLink { .. }
320 | FilePayload::Device { .. }
321 | FilePayload::Empty
322 | FilePayload::ReparsePoint { .. } => {
323 return capture_file_record_state(record, budget, work);
324 }
325 };
326 let domain = b"acyclic-fs-dependency-file-length-v1\0";
327 let encoded = logical_bytes.to_le_bytes();
328 let value = DependencyState::Present(semantic_digest(domain, &encoded));
329 work = work
330 .checked_add(WorkCounters {
331 bytes_encoded: u64::try_from(encoded.len()).unwrap_or(u64::MAX),
332 bytes_hashed: u64::try_from(encoded.len())
333 .unwrap_or(u64::MAX)
334 .saturating_add(u64::try_from(domain.len()).unwrap_or(u64::MAX))
335 .saturating_add(8),
336 ..WorkCounters::default()
337 })
338 .map_err(|error| failed(error.into(), work))?;
339 work.verify(budget)
340 .map_err(|error| failed(error.into(), work))?;
341 Ok(ProbeReceipt { value, work })
342}
343
344pub(crate) fn capture_directory_name_state(
345 entry: &super::TreeEntry,
346 budget: WorkBudget,
347 mut work: WorkCounters,
348) -> Result<ProbeReceipt, ProbeFailure> {
349 let encoded = super::tree::encode_tree_leaf_entries(std::slice::from_ref(entry), 1)
350 .map_err(|error| failed(error.into(), work))?;
351 let domain = b"acyclic-fs-dependency-directory-name-v1\0";
352 let value = DependencyState::Present(semantic_digest(domain, &encoded));
353 work = work
354 .checked_add(WorkCounters {
355 bytes_encoded: u64::try_from(encoded.len()).unwrap_or(u64::MAX),
356 bytes_hashed: u64::try_from(encoded.len())
357 .unwrap_or(u64::MAX)
358 .saturating_add(u64::try_from(domain.len()).unwrap_or(u64::MAX))
359 .saturating_add(8),
360 allocation_operations: 1,
361 peak_allocation_bytes: u64::try_from(encoded.capacity()).unwrap_or(u64::MAX),
362 ..WorkCounters::default()
363 })
364 .map_err(|error| failed(error.into(), work))?;
365 work.verify(budget)
366 .map_err(|error| failed(error.into(), work))?;
367 Ok(ProbeReceipt { value, work })
368}
369
370impl<S: crate::ImmediateObjectStore> AuthenticatedGenerationProbe<'_, S> {
371 fn content_state(
372 &self,
373 record: FileRecord,
374 offset: u64,
375 length: u64,
376 budget: WorkBudget,
377 mut work: WorkCounters,
378 ) -> Result<ProbeReceipt, ProbeFailure> {
379 if let FilePayload::InlineRegular(data) = record.payload {
380 return Self::inline_content_state(data, offset, length, budget, work);
381 }
382 let FilePayload::Regular {
383 logical_bytes,
384 extents,
385 } = record.payload
386 else {
387 return capture_file_record_state(record, budget, work);
388 };
389 let range = ByteRange { offset, length };
390 let hash_work = WorkCounters {
391 bytes_hashed: u64::try_from(CONTENT_RANGE_DOMAIN.len())
392 .unwrap_or(u64::MAX)
393 .saturating_add(8)
394 .saturating_add(length),
395 ..WorkCounters::default()
396 };
397 let reserved = work
398 .checked_add(hash_work)
399 .map_err(|error| failed(error.into(), work))?;
400 reserved
401 .verify(budget)
402 .map_err(|error| failed(error.into(), work))?;
403 let remaining = reserved
404 .remaining(budget)
405 .map_err(|error| failed(error.into(), work))?;
406 let plan = plan_extent_range(
407 self.store,
408 super::ExtentRangeRequest {
409 root: extents,
410 file_size: logical_bytes,
411 range,
412 maximum_spans: self.limits.maximum_extent_spans,
413 limits: self.limits.decode,
414 budget: remaining,
415 },
416 )
417 .map_err(|failure| failure.map_with_prior_work(work, Into::into))?;
418 work = work
419 .checked_add(plan.work)
420 .map_err(|error| failed(error.into(), work))?;
421 self.hash_content_spans(plan.spans, length, hash_work, budget, work)
422 }
423
424 fn sparse_seek_state(
425 &self,
426 record: FileRecord,
427 offset: u64,
428 target: ExtentSeekTarget,
429 budget: WorkBudget,
430 mut work: WorkCounters,
431 ) -> Result<ProbeReceipt, ProbeFailure> {
432 let result = match record.payload {
433 FilePayload::InlineRegular(data) => inline_seek_result(data, offset, target),
434 FilePayload::Regular {
435 logical_bytes,
436 extents,
437 } if offset <= logical_bytes => {
438 let receipt = seek_extent(
439 self.store,
440 ExtentSeekRequest {
441 root: extents,
442 file_size: logical_bytes,
443 offset,
444 target,
445 limits: self.limits.decode,
446 budget: work
447 .remaining(budget)
448 .map_err(|error| failed(error.into(), work))?,
449 },
450 )
451 .map_err(|failure| failure.map_with_prior_work(work, Into::into))?;
452 work = work
453 .checked_add(receipt.work)
454 .map_err(|error| failed(error.into(), work))?;
455 receipt.value
456 }
457 FilePayload::Regular { .. } => None,
458 FilePayload::Directory { .. }
459 | FilePayload::SymbolicLink { .. }
460 | FilePayload::Empty
461 | FilePayload::Device { .. }
462 | FilePayload::ReparsePoint { .. } => {
463 return capture_file_record_state(record, budget, work);
464 }
465 };
466 capture_sparse_seek_state(target, result, budget, work)
467 }
468}
469
470impl<S> AuthenticatedGenerationProbe<'_, S> {
471 fn inline_content_state(
472 data: super::InlineFileData,
473 offset: u64,
474 length: u64,
475 budget: WorkBudget,
476 work: WorkCounters,
477 ) -> Result<ProbeReceipt, ProbeFailure> {
478 let end = offset.checked_add(length).ok_or_else(|| {
479 failed(
480 AuthenticatedProbeError::Extent(ExtentReadError::InvalidRange),
481 work,
482 )
483 })?;
484 let bytes = data.as_bytes();
485 if end > u64::try_from(bytes.len()).unwrap_or(u64::MAX) {
486 return Err(failed(
487 AuthenticatedProbeError::Extent(ExtentReadError::InvalidRange),
488 work,
489 ));
490 }
491 let start = usize::try_from(offset).map_err(|_| {
492 failed(
493 AuthenticatedProbeError::Extent(ExtentReadError::InvalidRange),
494 work,
495 )
496 })?;
497 let end = usize::try_from(end).map_err(|_| {
498 failed(
499 AuthenticatedProbeError::Extent(ExtentReadError::InvalidRange),
500 work,
501 )
502 })?;
503 #[allow(
507 clippy::indexing_slicing,
508 reason = "end <= bytes.len() checked above; start <= end since end = offset + length"
509 )]
510 hash_content_range(&bytes[start..end], budget, work)
511 }
512}
513
514impl<S: crate::ImmediateObjectStore> AuthenticatedGenerationProbe<'_, S> {
515 fn hash_content_spans(
516 &self,
517 spans: Vec<super::ExtentSlice>,
518 length: u64,
519 hash_work: WorkCounters,
520 budget: WorkBudget,
521 mut work: WorkCounters,
522 ) -> Result<ProbeReceipt, ProbeFailure> {
523 let mut hasher = blake3::Hasher::new();
524 hasher.update(CONTENT_RANGE_DOMAIN);
525 hasher.update(&length.to_le_bytes());
526 for span in spans {
527 match span.kind {
528 ExtentKind::Hole | ExtentKind::AllocatedZero => {
529 let mut remaining_zeroes = span.length;
530 while remaining_zeroes != 0 {
531 let count = usize::try_from(
532 remaining_zeroes
533 .min(u64::try_from(ZERO_HASH_BLOCK.len()).unwrap_or(u64::MAX)),
534 )
535 .unwrap_or(ZERO_HASH_BLOCK.len());
536 #[allow(
541 clippy::indexing_slicing,
542 reason = "count <= ZERO_HASH_BLOCK.len() by construction above (min() and its unwrap_or branch are both bounded by ZERO_HASH_BLOCK.len())"
543 )]
544 hasher.update(&ZERO_HASH_BLOCK[..count]);
545 remaining_zeroes -= u64::try_from(count).unwrap_or(0);
546 }
547 }
548 ExtentKind::Content {
549 object,
550 object_offset,
551 } => {
552 let reserved = work
553 .checked_add(hash_work)
554 .map_err(|error| failed(error.into(), work))?;
555 let remaining = reserved
556 .remaining(budget)
557 .map_err(|error| failed(error.into(), work))?;
558 let read = read_blob_range(
559 self.store,
560 object,
561 ByteRange {
562 offset: object_offset,
563 length: span.length,
564 },
565 self.limits.decode,
566 remaining,
567 )
568 .map_err(|failure| failure.map_with_prior_work(work, Into::into))?;
569 work = work
570 .checked_add(read.work)
571 .map_err(|error| failed(error.into(), work))?;
572 hasher.update(&read.bytes);
573 }
574 }
575 }
576 work = work
577 .checked_add(hash_work)
578 .map_err(|error| failed(error.into(), work))?;
579 work.verify(budget)
580 .map_err(|error| failed(error.into(), work))?;
581 Ok(ProbeReceipt {
582 value: DependencyState::Present(Digest::from_bytes(*hasher.finalize().as_bytes())),
583 work,
584 })
585 }
586
587 fn directory_page_state(
588 &self,
589 record: FileRecord,
590 after: Option<&super::LogicalName>,
591 maximum_entries: u32,
592 budget: WorkBudget,
593 mut work: WorkCounters,
594 ) -> Result<ProbeReceipt, ProbeFailure> {
595 let FilePayload::Directory { entries } = record.payload else {
596 return capture_file_record_state(record, budget, work);
597 };
598 let remaining = work
599 .remaining(budget)
600 .map_err(|error| failed(error.into(), work))?;
601 let page = list_tree_entries(
602 self.store,
603 entries,
604 after,
605 maximum_entries,
606 self.limits.decode,
607 remaining,
608 )
609 .map_err(|failure| failure.map_with_prior_work(work, Into::into))?;
610 work = work
611 .checked_add(page.work)
612 .map_err(|error| failed(error.into(), work))?;
613 let mut encoded = encode_tree_page(&super::TreePage::Leaf(page.entries), maximum_entries)
614 .map_err(|error| failed(error.into(), work))?;
615 encoded.push(u8::from(page.has_more));
616 let domain = b"acyclic-fs-dependency-directory-page-v1\0";
617 let digest = semantic_digest(domain, &encoded);
618 work = work
619 .checked_add(WorkCounters {
620 bytes_encoded: u64::try_from(encoded.len()).unwrap_or(u64::MAX),
621 bytes_hashed: u64::try_from(encoded.len())
622 .unwrap_or(u64::MAX)
623 .saturating_add(u64::try_from(domain.len()).unwrap_or(u64::MAX))
624 .saturating_add(8),
625 allocation_operations: 1,
626 peak_allocation_bytes: u64::try_from(encoded.capacity()).unwrap_or(u64::MAX),
627 ..WorkCounters::default()
628 })
629 .map_err(|error| failed(error.into(), work))?;
630 work.verify(budget)
631 .map_err(|error| failed(error.into(), work))?;
632 Ok(ProbeReceipt {
633 value: DependencyState::Present(digest),
634 work,
635 })
636 }
637}
638
639impl<S: AsyncObjectStore> AuthenticatedGenerationProbe<'_, S> {
640 pub async fn capture_many_async(
652 &self,
653 generation: GenerationId,
654 regions: Vec<DependencyRegion>,
655 maximum_regions: u32,
656 budget: WorkBudget,
657 cancellation: &CancellationToken,
658 ) -> Result<OperationReceipt<Vec<Dependency>>, ProbeFailure> {
659 cancellation
660 .check()
661 .map_err(|_| failed(AuthenticatedProbeError::Cancelled, WorkCounters::default()))?;
662 validate_capture_batch(regions.len(), maximum_regions)?;
663 for region in ®ions {
664 self.validate_region(region)?;
665 }
666 let (mut captured, mut work) = allocate_capture_output(®ions, budget)?;
667
668 for region in regions {
669 let receipt = self
670 .probe_async(
671 generation,
672 ®ion,
673 work.remaining(budget)
674 .map_err(|error| failed(error.into(), work))?,
675 cancellation,
676 )
677 .await
678 .map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
679 work = work
680 .checked_add(receipt.work)
681 .map_err(|error| failed(error.into(), work))?;
682 captured.push(Dependency {
683 region,
684 expected: receipt.value,
685 });
686 }
687 Ok(OperationReceipt {
688 value: captured,
689 work,
690 })
691 }
692
693 pub async fn capture_records_many_async(
706 &self,
707 regions: Vec<(Option<FileRecord>, DependencyRegion)>,
708 maximum_regions: u32,
709 budget: WorkBudget,
710 cancellation: &CancellationToken,
711 ) -> Result<OperationReceipt<Vec<Dependency>>, ProbeFailure> {
712 cancellation
713 .check()
714 .map_err(|_| failed(AuthenticatedProbeError::Cancelled, WorkCounters::default()))?;
715 validate_capture_batch(regions.len(), maximum_regions)?;
716 for (record, region) in ®ions {
717 let expected_file_id = self.validate_region(region)?;
718 if record.is_some_and(|record| record.file_id != expected_file_id) {
719 return Err(failed(
720 AuthenticatedProbeError::RecordIdentityMismatch,
721 WorkCounters::default(),
722 ));
723 }
724 }
725
726 let (mut captured, mut work) = allocate_capture_output(®ions, budget)?;
727
728 for (record, region) in regions {
729 let receipt = self
730 .capture_validated_record_async(
731 record,
732 ®ion,
733 work.remaining(budget)
734 .map_err(|error| failed(error.into(), work))?,
735 WorkCounters::default(),
736 cancellation,
737 )
738 .await
739 .map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
740 work = work
741 .checked_add(receipt.work)
742 .map_err(|error| failed(error.into(), work))?;
743 captured.push(Dependency {
744 region,
745 expected: receipt.value,
746 });
747 }
748 Ok(OperationReceipt {
749 value: captured,
750 work,
751 })
752 }
753
754 async fn generation_async(
755 &self,
756 generation: GenerationId,
757 budget: WorkBudget,
758 cancellation: &CancellationToken,
759 ) -> Result<(GenerationRoot, WorkCounters), ProbeFailure> {
760 cancellation
761 .check()
762 .map_err(|_| failed(AuthenticatedProbeError::Cancelled, WorkCounters::default()))?;
763 if let Some(root) = self
764 .generations
765 .lock()
766 .map_err(|_| {
767 failed(
768 AuthenticatedProbeError::CachePoisoned,
769 WorkCounters::default(),
770 )
771 })?
772 .get(&generation)
773 .cloned()
774 {
775 return Ok((root, WorkCounters::default()));
776 }
777 let object = ObjectId {
778 kind: ObjectKind::GenerationRoot,
779 digest: generation.digest(),
780 };
781 let receipt = AsyncObjectStore::read(
782 self.store,
783 object,
784 self.limits.decode.maximum_object_bytes,
785 budget,
786 cancellation,
787 )
788 .await
789 .map_err(|failure| failure.map_with_prior_work(WorkCounters::default(), Into::into))?;
790 let root = decode_generation_root(&receipt.value, self.limits.decode)
791 .map_err(|error| failed(error.into(), receipt.work))?;
792 let mut generations = self
793 .generations
794 .lock()
795 .map_err(|_| failed(AuthenticatedProbeError::CachePoisoned, receipt.work))?;
796 clear_at_capacity(&mut generations, self.limits.maximum_cached_generations);
797 generations.insert(generation, root.clone());
798 Ok((root, receipt.work))
799 }
800
801 async fn record_async(
802 &self,
803 generation: GenerationId,
804 file_id: FileId,
805 budget: WorkBudget,
806 cancellation: &CancellationToken,
807 ) -> Result<(Option<FileRecord>, WorkCounters), ProbeFailure> {
808 if let Some(record) = self
809 .records
810 .lock()
811 .map_err(|_| {
812 failed(
813 AuthenticatedProbeError::CachePoisoned,
814 WorkCounters::default(),
815 )
816 })?
817 .get(&(generation, file_id))
818 .copied()
819 {
820 return Ok((record, WorkCounters::default()));
821 }
822 let (root, mut work) = self
823 .generation_async(generation, budget, cancellation)
824 .await?;
825 let remaining = work
826 .remaining(budget)
827 .map_err(|error| failed(error.into(), work))?;
828 let lookup = lookup_file_record_async(
829 self.store,
830 root.file_table,
831 file_id,
832 self.limits.decode,
833 remaining,
834 cancellation,
835 )
836 .await
837 .map_err(|failure| failure.map_with_prior_work(work, Into::into))?;
838 work = work
839 .checked_add(lookup.work)
840 .map_err(|error| failed(error.into(), work))?;
841 let mut records = self
842 .records
843 .lock()
844 .map_err(|_| failed(AuthenticatedProbeError::CachePoisoned, work))?;
845 clear_at_capacity(&mut records, self.limits.maximum_cached_records);
846 records.insert((generation, file_id), lookup.record);
847 Ok((lookup.record, work))
848 }
849
850 async fn content_state_async(
851 &self,
852 record: FileRecord,
853 offset: u64,
854 length: u64,
855 budget: WorkBudget,
856 mut work: WorkCounters,
857 cancellation: &CancellationToken,
858 ) -> Result<ProbeReceipt, ProbeFailure> {
859 if let FilePayload::InlineRegular(data) = record.payload {
860 return Self::inline_content_state(data, offset, length, budget, work);
861 }
862 let FilePayload::Regular {
863 logical_bytes,
864 extents,
865 } = record.payload
866 else {
867 return capture_file_record_state(record, budget, work);
868 };
869 let range = ByteRange { offset, length };
870 let hash_work = WorkCounters {
871 bytes_hashed: u64::try_from(CONTENT_RANGE_DOMAIN.len())
872 .unwrap_or(u64::MAX)
873 .saturating_add(8)
874 .saturating_add(length),
875 ..WorkCounters::default()
876 };
877 let reserved = work
878 .checked_add(hash_work)
879 .map_err(|error| failed(error.into(), work))?;
880 reserved
881 .verify(budget)
882 .map_err(|error| failed(error.into(), work))?;
883 let remaining = reserved
884 .remaining(budget)
885 .map_err(|error| failed(error.into(), work))?;
886 let plan = plan_extent_range_async(
887 self.store,
888 super::ExtentRangeRequest {
889 root: extents,
890 file_size: logical_bytes,
891 range,
892 maximum_spans: self.limits.maximum_extent_spans,
893 limits: self.limits.decode,
894 budget: remaining,
895 },
896 cancellation,
897 )
898 .await
899 .map_err(|failure| failure.map_with_prior_work(work, Into::into))?;
900 work = work
901 .checked_add(plan.work)
902 .map_err(|error| failed(error.into(), work))?;
903 self.hash_content_spans_async(plan.spans, length, hash_work, budget, work, cancellation)
904 .await
905 }
906
907 async fn sparse_seek_state_async(
908 &self,
909 record: FileRecord,
910 offset: u64,
911 target: ExtentSeekTarget,
912 budget: WorkBudget,
913 mut work: WorkCounters,
914 cancellation: &CancellationToken,
915 ) -> Result<ProbeReceipt, ProbeFailure> {
916 let result = match record.payload {
917 FilePayload::InlineRegular(data) => inline_seek_result(data, offset, target),
918 FilePayload::Regular {
919 logical_bytes,
920 extents,
921 } if offset <= logical_bytes => {
922 let receipt = seek_extent_async(
923 self.store,
924 ExtentSeekRequest {
925 root: extents,
926 file_size: logical_bytes,
927 offset,
928 target,
929 limits: self.limits.decode,
930 budget: work
931 .remaining(budget)
932 .map_err(|error| failed(error.into(), work))?,
933 },
934 cancellation,
935 )
936 .await
937 .map_err(|failure| failure.map_with_prior_work(work, Into::into))?;
938 work = work
939 .checked_add(receipt.work)
940 .map_err(|error| failed(error.into(), work))?;
941 receipt.value
942 }
943 FilePayload::Regular { .. } => None,
944 FilePayload::Directory { .. }
945 | FilePayload::SymbolicLink { .. }
946 | FilePayload::Empty
947 | FilePayload::Device { .. }
948 | FilePayload::ReparsePoint { .. } => {
949 return capture_file_record_state(record, budget, work);
950 }
951 };
952 capture_sparse_seek_state(target, result, budget, work)
953 }
954
955 async fn hash_content_spans_async(
956 &self,
957 spans: Vec<super::ExtentSlice>,
958 length: u64,
959 hash_work: WorkCounters,
960 budget: WorkBudget,
961 mut work: WorkCounters,
962 cancellation: &CancellationToken,
963 ) -> Result<ProbeReceipt, ProbeFailure> {
964 let mut hasher = blake3::Hasher::new();
965 hasher.update(CONTENT_RANGE_DOMAIN);
966 hasher.update(&length.to_le_bytes());
967 for span in spans {
968 cancellation
969 .check()
970 .map_err(|_| failed(AuthenticatedProbeError::Cancelled, work))?;
971 match span.kind {
972 ExtentKind::Hole | ExtentKind::AllocatedZero => {
973 let mut remaining_zeroes = span.length;
974 while remaining_zeroes != 0 {
975 cancellation
976 .check()
977 .map_err(|_| failed(AuthenticatedProbeError::Cancelled, work))?;
978 let count = usize::try_from(
979 remaining_zeroes
980 .min(u64::try_from(ZERO_HASH_BLOCK.len()).unwrap_or(u64::MAX)),
981 )
982 .unwrap_or(ZERO_HASH_BLOCK.len());
983 #[allow(
988 clippy::indexing_slicing,
989 reason = "count <= ZERO_HASH_BLOCK.len() by construction above (min() and its unwrap_or branch are both bounded by ZERO_HASH_BLOCK.len())"
990 )]
991 hasher.update(&ZERO_HASH_BLOCK[..count]);
992 remaining_zeroes -= u64::try_from(count).unwrap_or(0);
993 }
994 }
995 ExtentKind::Content {
996 object,
997 object_offset,
998 } => {
999 let reserved = work
1000 .checked_add(hash_work)
1001 .map_err(|error| failed(error.into(), work))?;
1002 let remaining = reserved
1003 .remaining(budget)
1004 .map_err(|error| failed(error.into(), work))?;
1005 let read = read_blob_range_async(
1006 self.store,
1007 object,
1008 ByteRange {
1009 offset: object_offset,
1010 length: span.length,
1011 },
1012 self.limits.decode,
1013 remaining,
1014 cancellation,
1015 )
1016 .await
1017 .map_err(|failure| failure.map_with_prior_work(work, Into::into))?;
1018 work = work
1019 .checked_add(read.work)
1020 .map_err(|error| failed(error.into(), work))?;
1021 hasher.update(&read.bytes);
1022 }
1023 }
1024 }
1025 work = work
1026 .checked_add(hash_work)
1027 .map_err(|error| failed(error.into(), work))?;
1028 work.verify(budget)
1029 .map_err(|error| failed(error.into(), work))?;
1030 Ok(ProbeReceipt {
1031 value: DependencyState::Present(Digest::from_bytes(*hasher.finalize().as_bytes())),
1032 work,
1033 })
1034 }
1035
1036 async fn directory_page_state_async(
1037 &self,
1038 record: FileRecord,
1039 after: Option<&super::LogicalName>,
1040 maximum_entries: u32,
1041 budget: WorkBudget,
1042 mut work: WorkCounters,
1043 cancellation: &CancellationToken,
1044 ) -> Result<ProbeReceipt, ProbeFailure> {
1045 let FilePayload::Directory { entries } = record.payload else {
1046 return capture_file_record_state(record, budget, work);
1047 };
1048 let remaining = work
1049 .remaining(budget)
1050 .map_err(|error| failed(error.into(), work))?;
1051 let page = list_tree_entries_async(
1052 self.store,
1053 entries,
1054 after,
1055 maximum_entries,
1056 self.limits.decode,
1057 remaining,
1058 cancellation,
1059 )
1060 .await
1061 .map_err(|failure| failure.map_with_prior_work(work, Into::into))?;
1062 work = work
1063 .checked_add(page.work)
1064 .map_err(|error| failed(error.into(), work))?;
1065 let mut encoded = encode_tree_page(&super::TreePage::Leaf(page.entries), maximum_entries)
1066 .map_err(|error| failed(error.into(), work))?;
1067 encoded.push(u8::from(page.has_more));
1068 let domain = b"acyclic-fs-dependency-directory-page-v1\0";
1069 let digest = semantic_digest(domain, &encoded);
1070 work = work
1071 .checked_add(WorkCounters {
1072 bytes_encoded: u64::try_from(encoded.len()).unwrap_or(u64::MAX),
1073 bytes_hashed: u64::try_from(encoded.len())
1074 .unwrap_or(u64::MAX)
1075 .saturating_add(u64::try_from(domain.len()).unwrap_or(u64::MAX))
1076 .saturating_add(8),
1077 allocation_operations: 1,
1078 peak_allocation_bytes: u64::try_from(encoded.capacity()).unwrap_or(u64::MAX),
1079 ..WorkCounters::default()
1080 })
1081 .map_err(|error| failed(error.into(), work))?;
1082 work.verify(budget)
1083 .map_err(|error| failed(error.into(), work))?;
1084 Ok(ProbeReceipt {
1085 value: DependencyState::Present(digest),
1086 work,
1087 })
1088 }
1089
1090 pub async fn probe_async(
1099 &self,
1100 generation: GenerationId,
1101 region: &DependencyRegion,
1102 budget: WorkBudget,
1103 cancellation: &CancellationToken,
1104 ) -> Result<ProbeReceipt, ProbeFailure> {
1105 cancellation
1106 .check()
1107 .map_err(|_| failed(AuthenticatedProbeError::Cancelled, WorkCounters::default()))?;
1108 let file_id = self.validate_region(region)?;
1109 let (record, work) = self
1110 .record_async(generation, file_id, budget, cancellation)
1111 .await?;
1112 self.capture_validated_record_async(record, region, budget, work, cancellation)
1113 .await
1114 }
1115
1116 pub async fn capture_record_async(
1128 &self,
1129 record: Option<FileRecord>,
1130 region: &DependencyRegion,
1131 budget: WorkBudget,
1132 cancellation: &CancellationToken,
1133 ) -> Result<ProbeReceipt, ProbeFailure> {
1134 cancellation
1135 .check()
1136 .map_err(|_| failed(AuthenticatedProbeError::Cancelled, WorkCounters::default()))?;
1137 let expected_file_id = self.validate_region(region)?;
1138 if record.is_some_and(|record| record.file_id != expected_file_id) {
1139 return Err(failed(
1140 AuthenticatedProbeError::RecordIdentityMismatch,
1141 WorkCounters::default(),
1142 ));
1143 }
1144 self.capture_validated_record_async(
1145 record,
1146 region,
1147 budget,
1148 WorkCounters::default(),
1149 cancellation,
1150 )
1151 .await
1152 }
1153
1154 async fn capture_validated_record_async(
1155 &self,
1156 record: Option<FileRecord>,
1157 region: &DependencyRegion,
1158 budget: WorkBudget,
1159 mut work: WorkCounters,
1160 cancellation: &CancellationToken,
1161 ) -> Result<ProbeReceipt, ProbeFailure> {
1162 let Some(record) = record else {
1163 return Ok(ProbeReceipt {
1164 value: DependencyState::Absent,
1165 work,
1166 });
1167 };
1168 let value = match region {
1169 DependencyRegion::FileRecord(_) => {
1170 return capture_file_record_state(record, budget, work);
1171 }
1172 DependencyRegion::Metadata(_) => DependencyState::Present(record.metadata.digest),
1173 DependencyRegion::FileLength(_) => {
1174 return capture_file_length_state(record, budget, work);
1175 }
1176 DependencyRegion::DirectoryName { name, .. } => {
1177 let FilePayload::Directory { entries } = record.payload else {
1178 return capture_file_record_state(record, budget, work);
1179 };
1180 let remaining = work
1181 .remaining(budget)
1182 .map_err(|error| failed(error.into(), work))?;
1183 let lookup = lookup_tree_entry_async(
1184 self.store,
1185 entries,
1186 name,
1187 self.limits.decode,
1188 remaining,
1189 cancellation,
1190 )
1191 .await
1192 .map_err(|failure| failure.map_with_prior_work(work, Into::into))?;
1193 work = work
1194 .checked_add(lookup.work)
1195 .map_err(|error| failed(error.into(), work))?;
1196 match lookup.entry {
1197 None => DependencyState::Absent,
1198 Some(entry) => {
1199 return capture_directory_name_state(&entry, budget, work);
1200 }
1201 }
1202 }
1203 DependencyRegion::ContentRange { offset, length, .. } => {
1204 return self
1205 .content_state_async(record, *offset, *length, budget, work, cancellation)
1206 .await;
1207 }
1208 DependencyRegion::SparseSeek { offset, target, .. } => {
1209 return self
1210 .sparse_seek_state_async(record, *offset, *target, budget, work, cancellation)
1211 .await;
1212 }
1213 DependencyRegion::DirectoryRange {
1214 after,
1215 maximum_entries,
1216 ..
1217 } => {
1218 return self
1219 .directory_page_state_async(
1220 record,
1221 after.as_ref(),
1222 *maximum_entries,
1223 budget,
1224 work,
1225 cancellation,
1226 )
1227 .await;
1228 }
1229 };
1230 Ok(ProbeReceipt { value, work })
1231 }
1232}
1233
1234impl<S: AsyncObjectStore> AsyncRebaseProbe for AuthenticatedGenerationProbe<'_, S> {
1235 type Error = AuthenticatedProbeError;
1236
1237 async fn probe_async(
1238 &self,
1239 generation: GenerationId,
1240 region: &DependencyRegion,
1241 budget: WorkBudget,
1242 cancellation: &CancellationToken,
1243 ) -> Result<ProbeReceipt, OperationFailure<Self::Error>> {
1244 AuthenticatedGenerationProbe::probe_async(self, generation, region, budget, cancellation)
1245 .await
1246 }
1247}
1248
1249impl<S: crate::ImmediateObjectStore> RebaseProbe for AuthenticatedGenerationProbe<'_, S> {
1250 type Error = AuthenticatedProbeError;
1251
1252 fn probe(
1253 &self,
1254 generation: GenerationId,
1255 region: &DependencyRegion,
1256 budget: WorkBudget,
1257 ) -> Result<ProbeReceipt, OperationFailure<Self::Error>> {
1258 let file_id = self.validate_region(region)?;
1259 let (record, mut work) = self.record(generation, file_id, budget)?;
1260 let Some(record) = record else {
1261 return Ok(ProbeReceipt {
1262 value: DependencyState::Absent,
1263 work,
1264 });
1265 };
1266 let value = match region {
1267 DependencyRegion::FileRecord(_) => {
1268 return capture_file_record_state(record, budget, work);
1269 }
1270 DependencyRegion::Metadata(_) => DependencyState::Present(record.metadata.digest),
1271 DependencyRegion::FileLength(_) => {
1272 return capture_file_length_state(record, budget, work);
1273 }
1274 DependencyRegion::DirectoryName { name, .. } => {
1275 let FilePayload::Directory { entries } = record.payload else {
1276 return capture_file_record_state(record, budget, work);
1277 };
1278 let remaining = work
1279 .remaining(budget)
1280 .map_err(|error| failed(error.into(), work))?;
1281 let lookup =
1282 lookup_tree_entry(self.store, entries, name, self.limits.decode, remaining)
1283 .map_err(|failure| failure.map_with_prior_work(work, Into::into))?;
1284 work = work
1285 .checked_add(lookup.work)
1286 .map_err(|error| failed(error.into(), work))?;
1287 match lookup.entry {
1288 None => DependencyState::Absent,
1289 Some(entry) => {
1290 return capture_directory_name_state(&entry, budget, work);
1291 }
1292 }
1293 }
1294 DependencyRegion::ContentRange { offset, length, .. } => {
1295 return self.content_state(record, *offset, *length, budget, work);
1296 }
1297 DependencyRegion::SparseSeek { offset, target, .. } => {
1298 return self.sparse_seek_state(record, *offset, *target, budget, work);
1299 }
1300 DependencyRegion::DirectoryRange {
1301 after,
1302 maximum_entries,
1303 ..
1304 } => {
1305 return self.directory_page_state(
1306 record,
1307 after.as_ref(),
1308 *maximum_entries,
1309 budget,
1310 work,
1311 );
1312 }
1313 };
1314 Ok(ProbeReceipt { value, work })
1315 }
1316}
1317
1318fn clear_at_capacity<K: Eq + Hash, V>(map: &mut HashMap<K, V>, maximum: u32) {
1319 if u32::try_from(map.len()).unwrap_or(u32::MAX) >= maximum {
1320 map.clear();
1321 }
1322}
1323
1324fn inline_seek_result(
1325 data: super::InlineFileData,
1326 offset: u64,
1327 target: ExtentSeekTarget,
1328) -> Option<u64> {
1329 let logical_bytes = u64::try_from(data.as_bytes().len()).unwrap_or(u64::MAX);
1330 if offset > logical_bytes {
1331 return None;
1332 }
1333 match target {
1334 ExtentSeekTarget::Data => (offset < logical_bytes).then_some(offset),
1335 ExtentSeekTarget::Hole => Some(logical_bytes),
1336 }
1337}
1338
1339fn hash_content_range(
1340 bytes: &[u8],
1341 budget: WorkBudget,
1342 work: WorkCounters,
1343) -> Result<ProbeReceipt, ProbeFailure> {
1344 let hash_work = WorkCounters {
1345 bytes_hashed: u64::try_from(CONTENT_RANGE_DOMAIN.len())
1346 .unwrap_or(u64::MAX)
1347 .saturating_add(8)
1348 .saturating_add(u64::try_from(bytes.len()).unwrap_or(u64::MAX)),
1349 ..WorkCounters::default()
1350 };
1351 let next = work
1352 .checked_add(hash_work)
1353 .map_err(|error| failed(error.into(), work))?;
1354 next.verify(budget)
1355 .map_err(|error| failed(error.into(), work))?;
1356 Ok(ProbeReceipt {
1357 value: DependencyState::Present(semantic_digest(CONTENT_RANGE_DOMAIN, bytes)),
1358 work: next,
1359 })
1360}
1361
1362fn semantic_digest(domain: &[u8], bytes: &[u8]) -> Digest {
1363 let mut hasher = blake3::Hasher::new();
1364 hasher.update(domain);
1365 hasher.update(&u64::try_from(bytes.len()).unwrap_or(u64::MAX).to_le_bytes());
1366 hasher.update(bytes);
1367 Digest::from_bytes(*hasher.finalize().as_bytes())
1368}
1369
1370type ProbeFailure = OperationFailure<AuthenticatedProbeError>;
1371
1372fn failed(error: AuthenticatedProbeError, work: WorkCounters) -> ProbeFailure {
1373 OperationFailure::new(error, work)
1374}
1375
1376fn validate_capture_batch(count: usize, maximum: u32) -> Result<(), ProbeFailure> {
1377 if maximum == 0 {
1378 return Err(failed(
1379 AuthenticatedProbeError::ZeroBatchLimit,
1380 WorkCounters::default(),
1381 ));
1382 }
1383 if u32::try_from(count).unwrap_or(u32::MAX) > maximum {
1384 return Err(failed(
1385 AuthenticatedProbeError::TooManyRegions { maximum },
1386 WorkCounters::default(),
1387 ));
1388 }
1389 Ok(())
1390}
1391
1392fn allocate_capture_output<T>(
1393 input: &Vec<T>,
1394 budget: WorkBudget,
1395) -> Result<(Vec<Dependency>, WorkCounters), ProbeFailure> {
1396 let input_bytes = input
1397 .capacity()
1398 .checked_mul(size_of::<T>())
1399 .map(crate::foundation::usize_to_u64)
1400 .ok_or_else(|| {
1401 failed(
1402 AuthenticatedProbeError::AllocationFailed,
1403 WorkCounters::default(),
1404 )
1405 })?;
1406 let mut captured = Vec::new();
1407 captured.try_reserve_exact(input.len()).map_err(|_| {
1408 failed(
1409 AuthenticatedProbeError::AllocationFailed,
1410 WorkCounters::default(),
1411 )
1412 })?;
1413 let output_bytes = captured
1414 .capacity()
1415 .checked_mul(size_of::<Dependency>())
1416 .map(crate::foundation::usize_to_u64)
1417 .ok_or_else(|| {
1418 failed(
1419 AuthenticatedProbeError::AllocationFailed,
1420 WorkCounters::default(),
1421 )
1422 })?;
1423 let work = WorkCounters {
1424 allocation_operations: u64::from(!captured.is_empty()),
1425 peak_allocation_bytes: input_bytes.checked_add(output_bytes).ok_or_else(|| {
1426 failed(
1427 AuthenticatedProbeError::AllocationFailed,
1428 WorkCounters::default(),
1429 )
1430 })?,
1431 ..WorkCounters::default()
1432 };
1433 work.verify(budget)
1434 .map_err(|error| failed(error.into(), WorkCounters::default()))?;
1435 Ok((captured, work))
1436}
1437
1438#[derive(Debug, Error)]
1440pub enum AuthenticatedProbeError {
1441 #[error("authenticated dependency probing was cancelled")]
1443 Cancelled,
1444 #[error("authenticated probe limits are invalid")]
1446 InvalidLimits,
1447 #[error("authenticated dependency batch limit must be positive")]
1449 ZeroBatchLimit,
1450 #[error("authenticated dependency batch exceeds maximum {maximum}")]
1452 TooManyRegions {
1453 maximum: u32,
1455 },
1456 #[error("authenticated dependency batch allocation failed")]
1458 AllocationFailed,
1459 #[error("authenticated record identity does not match dependency region")]
1461 RecordIdentityMismatch,
1462 #[error("authenticated content dependency exceeds its byte bound")]
1464 ContentRangeTooLarge,
1465 #[error("authenticated directory dependency exceeds its entry bound")]
1467 DirectoryPageTooLarge,
1468 #[error(transparent)]
1470 InvalidDependency(#[from] DependencyError),
1471 #[error("authenticated probe cache is poisoned")]
1473 CachePoisoned,
1474 #[error(transparent)]
1476 Storage(#[from] ObjectStoreError),
1477 #[error(transparent)]
1479 Decode(#[from] super::CanonicalDecodeError),
1480 #[error(transparent)]
1482 FileRecord(#[from] FileRecordReadError),
1483 #[error(transparent)]
1485 Tree(#[from] TreeReadError),
1486 #[error(transparent)]
1488 Directory(#[from] DirectoryReadError),
1489 #[error(transparent)]
1491 Extent(#[from] ExtentReadError),
1492 #[error(transparent)]
1494 Blob(#[from] BlobReadError),
1495 #[error(transparent)]
1497 Work(#[from] WorkError),
1498}
1499
1500#[cfg(all(test, feature = "memory"))]
1501#[path = "tests/probe.rs"]
1502mod tests;