Skip to main content

acyclic_fs/kernel/
probe.rs

1//! Object-store-backed authenticated dependency probes.
2
3use 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
26/// Computes canonical layout-independent evidence for one non-empty logical
27/// content range already present in memory.
28///
29/// This function is the normative cross-language vector surface. Sparse and
30/// inline content use the same domain, length prefix, and logical byte stream.
31///
32/// # Errors
33///
34/// Rejects a zero maximum, empty/excessive content, or exact hash work outside
35/// the admitted budget.
36pub 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
62/// Authenticated exact-region resolver with disposable per-instance summary caches.
63///
64/// Caches contain only immutable decoded summaries. Cache loss affects
65/// performance, never correctness or authority.
66pub 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/// Hard bounds for one disposable authenticated dependency probe.
74#[derive(Clone, Copy, Debug, Eq, PartialEq)]
75pub struct ProbeLimits {
76    /// Canonical decoder and page traversal bounds.
77    pub decode: DecodeLimits,
78    /// Maximum retained immutable generation summaries.
79    pub maximum_cached_generations: u32,
80    /// Maximum retained `(generation, file)` summaries.
81    pub maximum_cached_records: u32,
82    /// Maximum sparse spans produced for one content dependency.
83    pub maximum_extent_spans: u32,
84    /// Maximum semantic file bytes in one observed content range.
85    pub maximum_content_payload_bytes: u64,
86    /// Maximum entries in one observed directory page.
87    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    /// Creates a bounded resolver over one immutable-object backend.
105    ///
106    /// # Errors
107    ///
108    /// Rejects zero cache or extent-output limits before backend access.
109    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        // `end <= bytes.len()` was checked above (as u64) before the usize
504        // conversions, and `start <= end` since `end = offset + length` with
505        // `length >= 0`, so this range is always in bounds.
506        #[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                        // `count = min(remaining_zeroes, ZERO_HASH_BLOCK.len())`
537                        // as computed immediately above (the `unwrap_or` branch
538                        // is itself `ZERO_HASH_BLOCK.len()`), so `count <=
539                        // ZERO_HASH_BLOCK.len()` always holds.
540                        #[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    /// Captures a bounded set of exact regions through one shared immutable
641    /// generation and file-record cache.
642    ///
643    /// Regions are admitted as a complete batch before allocation or backend
644    /// access. Input order and duplicates are preserved; repeated regions and
645    /// regions sharing a file identity reuse the same authenticated frontiers.
646    ///
647    /// # Errors
648    ///
649    /// Rejects a zero/exceeded batch bound, malformed region, allocation,
650    /// cancellation, authentication, storage, or exact-work failure.
651    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 &regions {
664            self.validate_region(region)?;
665        }
666        let (mut captured, mut work) = allocate_capture_output(&regions, budget)?;
667
668        for region in regions {
669            let receipt = self
670                .probe_async(
671                    generation,
672                    &region,
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    /// Captures a bounded batch from records already authenticated by the
694    /// caller's path or identity frontier, without rereading the generation or
695    /// file table.
696    ///
697    /// Input order and duplicates are preserved. Every present record must own
698    /// its region; absence is an explicit authenticated caller fact.
699    ///
700    /// # Errors
701    ///
702    /// Rejects the complete batch before semantic backend access when its
703    /// bound, region, record identity, allocation, cancellation, or work is
704    /// invalid.
705    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 &regions {
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(&regions, budget)?;
727
728        for (record, region) in regions {
729            let receipt = self
730                .capture_validated_record_async(
731                    record,
732                    &region,
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                        // `count = min(remaining_zeroes, ZERO_HASH_BLOCK.len())`
984                        // as computed immediately above (the `unwrap_or` branch
985                        // is itself `ZERO_HASH_BLOCK.len()`), so `count <=
986                        // ZERO_HASH_BLOCK.len()` always holds.
987                        #[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    /// Resolves one exact dependency through nonblocking immutable-object I/O.
1091    ///
1092    /// This is the browser/remote equivalent of [`RebaseProbe::probe`] and
1093    /// shares all canonical decoders and frontier machines with it.
1094    ///
1095    /// # Errors
1096    ///
1097    /// Returns exact typed authentication, cancellation, and work failures.
1098    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    /// Captures one exact region from a file record already authenticated by
1117    /// the caller's generation/path frontier.
1118    ///
1119    /// This is the zero-reread mutation/read precondition path. The region is
1120    /// validated and must name the supplied record identity. `None` represents
1121    /// an authenticated absent identity.
1122    ///
1123    /// # Errors
1124    ///
1125    /// Returns a typed identity mismatch, malformed region, cancellation,
1126    /// storage, authentication, allocation, or exact-work failure.
1127    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/// Authenticated generation-probe failures.
1439#[derive(Debug, Error)]
1440pub enum AuthenticatedProbeError {
1441    /// Cooperative cancellation occurred before the next storage or hashing boundary.
1442    #[error("authenticated dependency probing was cancelled")]
1443    Cancelled,
1444    /// Cache or extent-output limits are zero.
1445    #[error("authenticated probe limits are invalid")]
1446    InvalidLimits,
1447    /// A dependency-capture batch requires a positive region bound.
1448    #[error("authenticated dependency batch limit must be positive")]
1449    ZeroBatchLimit,
1450    /// A dependency-capture batch exceeds its admitted region count.
1451    #[error("authenticated dependency batch exceeds maximum {maximum}")]
1452    TooManyRegions {
1453        /// Admitted maximum region count.
1454        maximum: u32,
1455    },
1456    /// Bounded dependency output allocation failed or overflowed.
1457    #[error("authenticated dependency batch allocation failed")]
1458    AllocationFailed,
1459    /// A directly supplied record does not own the requested dependency region.
1460    #[error("authenticated record identity does not match dependency region")]
1461    RecordIdentityMismatch,
1462    /// Content dependency exceeds the configured byte bound.
1463    #[error("authenticated content dependency exceeds its byte bound")]
1464    ContentRangeTooLarge,
1465    /// Directory dependency exceeds the configured entry bound.
1466    #[error("authenticated directory dependency exceeds its entry bound")]
1467    DirectoryPageTooLarge,
1468    /// Dependency shape is empty, overflowing, or otherwise invalid.
1469    #[error(transparent)]
1470    InvalidDependency(#[from] DependencyError),
1471    /// Process-local immutable summary cache was poisoned.
1472    #[error("authenticated probe cache is poisoned")]
1473    CachePoisoned,
1474    /// Immutable object storage failed.
1475    #[error(transparent)]
1476    Storage(#[from] ObjectStoreError),
1477    /// Generation or canonical semantic encoding failed.
1478    #[error(transparent)]
1479    Decode(#[from] super::CanonicalDecodeError),
1480    /// File-table frontier authentication failed.
1481    #[error(transparent)]
1482    FileRecord(#[from] FileRecordReadError),
1483    /// Directory frontier authentication failed.
1484    #[error(transparent)]
1485    Tree(#[from] TreeReadError),
1486    /// Bounded directory frontier authentication failed.
1487    #[error(transparent)]
1488    Directory(#[from] DirectoryReadError),
1489    /// Sparse extent frontier authentication failed.
1490    #[error(transparent)]
1491    Extent(#[from] ExtentReadError),
1492    /// Authenticated content blob reading failed.
1493    #[error(transparent)]
1494    Blob(#[from] BlobReadError),
1495    /// Exact work exceeded or overflowed its budget.
1496    #[error(transparent)]
1497    Work(#[from] WorkError),
1498}
1499
1500#[cfg(all(test, feature = "memory"))]
1501#[path = "tests/probe.rs"]
1502mod tests;