Skip to main content

acyclic_fs/kernel/
blob.rs

1//! Authenticated fixed-size blob chunks and sparse range reads.
2
3use super::allocation::{AllocationError, AllocationLedger, LogicalVecCapacity, VisitedObjectSet};
4use super::codec::{
5    CanonicalDecodeError, DecodeLimits, DecodedPageKind, DecodedPageShape, Decoder, Encoder,
6};
7use super::frontier;
8use super::frontier::Machine as _;
9use crate::async_storage::{
10    AsyncObjectStore, DecodedCacheAdmission, DecodedCacheKey, DecodedCacheValue,
11};
12use crate::cancellation::CancellationToken;
13use crate::performance::{OperationFailure, WorkBudget, WorkCounters, WorkError};
14use crate::speculation::{ResidencyHint, ResidencyReason};
15use crate::storage::{
16    ByteRange, ObjectId, ObjectKind, ObjectRead, ObjectReadRequest, ObjectReadRetention,
17    ObjectReceipt, ObjectStore, ObjectStoreError, object_digest,
18};
19use bytes::Bytes;
20use std::future::Future;
21use std::io::Read;
22use std::mem::size_of;
23use std::sync::Arc;
24use thiserror::Error;
25
26const DOMAIN: &[u8; 8] = b"ACYFSBLB";
27const VERSION: u16 = 1;
28const BLOB_PAGE_HEADER_BYTES: usize = 8 + 2 + 8 + 8 + 1 + 4;
29const BLOB_PAGE_ITEM_BYTES: usize = 8 + 8 + 32;
30
31/// One authenticated fixed-size content chunk reference.
32#[derive(Clone, Copy, Debug, Eq, PartialEq)]
33pub struct BlobChunkRef {
34    /// Inclusive logical offset.
35    pub first_offset: u64,
36    /// Exclusive logical end.
37    pub end_offset: u64,
38    /// Complete immutable chunk object.
39    pub chunk: ObjectId,
40}
41
42/// One authenticated blob-index child reference.
43#[derive(Clone, Copy, Debug, Eq, PartialEq)]
44pub struct BlobChild {
45    /// Inclusive logical offset.
46    pub first_offset: u64,
47    /// Exclusive logical end.
48    pub end_offset: u64,
49    /// Child blob-index page.
50    pub page: ObjectId,
51}
52
53/// Contents of one blob-index page.
54#[derive(Clone, Debug, Eq, PartialEq)]
55pub enum BlobNode {
56    /// Ordered chunk references.
57    Leaf(Vec<BlobChunkRef>),
58    /// Ordered index-page references.
59    Internal(Vec<BlobChild>),
60}
61
62/// One self-bounded authenticated blob-index page.
63#[derive(Clone, Debug, Eq, PartialEq)]
64pub struct BlobPage {
65    /// Inclusive logical offset represented by this page.
66    pub first_offset: u64,
67    /// Exclusive logical end represented by this page.
68    pub end_offset: u64,
69    /// Leaf chunks or internal children.
70    pub node: BlobNode,
71}
72
73impl BlobPage {
74    fn validate(&self, maximum_items: u32) -> Result<(), CanonicalDecodeError> {
75        let (count, ranges_valid, kinds_valid) = match &self.node {
76            BlobNode::Leaf(items) => (
77                items.len(),
78                contiguous(
79                    self.first_offset,
80                    self.end_offset,
81                    items
82                        .iter()
83                        .map(|item| (item.first_offset, item.end_offset)),
84                ),
85                items
86                    .iter()
87                    .all(|item| item.chunk.kind == ObjectKind::BlobChunk),
88            ),
89            BlobNode::Internal(items) => (
90                items.len(),
91                contiguous(
92                    self.first_offset,
93                    self.end_offset,
94                    items
95                        .iter()
96                        .map(|item| (item.first_offset, item.end_offset)),
97                ),
98                items.iter().all(|item| item.page.kind == ObjectKind::Blob),
99            ),
100        };
101        if u32::try_from(count).unwrap_or(u32::MAX) > maximum_items {
102            return Err(CanonicalDecodeError::FieldTooLarge {
103                observed: u32::try_from(count).unwrap_or(u32::MAX),
104                maximum: maximum_items,
105            });
106        }
107        if count == 0 && (self.first_offset != 0 || self.end_offset != 0) {
108            return Err(invariant("non-empty blob bounds require entries"));
109        }
110        if count != 0 && (!ranges_valid || self.first_offset >= self.end_offset) {
111            return Err(invariant("blob ranges are not positive and contiguous"));
112        }
113        if !kinds_valid {
114            return Err(invariant("blob reference has the wrong object kind"));
115        }
116        Ok(())
117    }
118}
119
120fn contiguous(first: u64, end: u64, mut ranges: impl Iterator<Item = (u64, u64)>) -> bool {
121    let Some((item_first, item_end)) = ranges.next() else {
122        return first == 0 && end == 0;
123    };
124    if item_first != first || item_first >= item_end {
125        return false;
126    }
127    let mut prior_end = item_end;
128    for (next_first, next_end) in ranges {
129        if next_first != prior_end || next_first >= next_end {
130            return false;
131        }
132        prior_end = next_end;
133    }
134    prior_end == end
135}
136
137/// Encodes one canonical blob-index page.
138///
139/// # Errors
140///
141/// Rejects unbounded, discontinuous, empty-with-bounds, or wrongly typed pages.
142pub fn encode_blob_page(
143    page: &BlobPage,
144    maximum_items: u32,
145) -> Result<Vec<u8>, CanonicalDecodeError> {
146    let encoded_length = blob_page_encoded_length(page, maximum_items)?;
147    let mut encoder = Encoder::with_exact_capacity(DOMAIN, VERSION, encoded_length)?;
148    encoder.u64(page.first_offset);
149    encoder.u64(page.end_offset);
150    match &page.node {
151        BlobNode::Leaf(items) => {
152            encoder.u8(1);
153            encoder.u32(count(items.len())?);
154            for item in items {
155                encoder.u64(item.first_offset);
156                encoder.u64(item.end_offset);
157                encoder.fixed(item.chunk.digest.as_bytes());
158            }
159        }
160        BlobNode::Internal(items) => {
161            encoder.u8(2);
162            encoder.u32(count(items.len())?);
163            for item in items {
164                encoder.u64(item.first_offset);
165                encoder.u64(item.end_offset);
166                encoder.fixed(item.page.digest.as_bytes());
167            }
168        }
169    }
170    Ok(encoder.finish())
171}
172
173fn blob_page_encoded_length(
174    page: &BlobPage,
175    maximum_items: u32,
176) -> Result<usize, CanonicalDecodeError> {
177    page.validate(maximum_items)?;
178    let items = match &page.node {
179        BlobNode::Leaf(items) => items.len(),
180        BlobNode::Internal(items) => items.len(),
181    };
182    items
183        .checked_mul(BLOB_PAGE_ITEM_BYTES)
184        .and_then(|bytes| BLOB_PAGE_HEADER_BYTES.checked_add(bytes))
185        .ok_or(CanonicalDecodeError::LengthOverflow)
186}
187
188/// Decodes one canonical blob-index page under hard allocation limits.
189///
190/// # Errors
191///
192/// Fails closed on malformed bytes, unknown versions/tags, or bad ranges.
193pub fn decode_blob_page(
194    bytes: &[u8],
195    limits: DecodeLimits,
196) -> Result<BlobPage, CanonicalDecodeError> {
197    let mut decoder = Decoder::new(bytes, DOMAIN, VERSION, limits.maximum_page_object_bytes())?;
198    let first_offset = decoder.u64()?;
199    let end_offset = decoder.u64()?;
200    let tag = decoder.u8()?;
201    let item_count = decoder.u32()?;
202    if item_count > limits.maximum_page_items {
203        return Err(CanonicalDecodeError::FieldTooLarge {
204            observed: item_count,
205            maximum: limits.maximum_page_items,
206        });
207    }
208    let mut leaf = Vec::new();
209    let mut internal = Vec::new();
210    match tag {
211        1 => {
212            leaf.try_reserve_exact(capacity(item_count)?)
213                .map_err(|_| CanonicalDecodeError::AllocationFailed)?;
214            for _ in 0..item_count {
215                leaf.push(BlobChunkRef {
216                    first_offset: decoder.u64()?,
217                    end_offset: decoder.u64()?,
218                    chunk: ObjectId {
219                        kind: ObjectKind::BlobChunk,
220                        digest: crate::foundation::Digest::from_bytes(decoder.fixed()?),
221                    },
222                });
223            }
224        }
225        2 => {
226            internal
227                .try_reserve_exact(capacity(item_count)?)
228                .map_err(|_| CanonicalDecodeError::AllocationFailed)?;
229            for _ in 0..item_count {
230                internal.push(BlobChild {
231                    first_offset: decoder.u64()?,
232                    end_offset: decoder.u64()?,
233                    page: ObjectId {
234                        kind: ObjectKind::Blob,
235                        digest: crate::foundation::Digest::from_bytes(decoder.fixed()?),
236                    },
237                });
238            }
239        }
240        tag => {
241            return Err(CanonicalDecodeError::UnknownTag {
242                field: "blob_page",
243                tag,
244            });
245        }
246    }
247    decoder.finish()?;
248    let page = BlobPage {
249        first_offset,
250        end_offset,
251        node: if tag == 1 {
252            BlobNode::Leaf(leaf)
253        } else {
254            BlobNode::Internal(internal)
255        },
256    };
257    page.validate(limits.maximum_page_items)?;
258    Ok(page)
259}
260
261fn blob_page_decode_shape(
262    bytes: &[u8],
263    limits: DecodeLimits,
264) -> Result<DecodedPageShape, CanonicalDecodeError> {
265    let mut decoder = Decoder::new(bytes, DOMAIN, VERSION, limits.maximum_page_object_bytes())?;
266    decoder.u64()?;
267    decoder.u64()?;
268    let tag = decoder.u8()?;
269    let item_count = decoder.u32()?;
270    if item_count > limits.maximum_page_items {
271        return Err(CanonicalDecodeError::FieldTooLarge {
272            observed: item_count,
273            maximum: limits.maximum_page_items,
274        });
275    }
276    let kind = match tag {
277        1 | 2 => {
278            for _ in 0..item_count {
279                decoder.u64()?;
280                decoder.u64()?;
281                let _: [u8; 32] = decoder.fixed()?;
282            }
283            if tag == 1 {
284                DecodedPageKind::Leaf
285            } else {
286                DecodedPageKind::Internal
287            }
288        }
289        tag => {
290            return Err(CanonicalDecodeError::UnknownTag {
291                field: "blob_page",
292                tag,
293            });
294        }
295    };
296    decoder.finish()?;
297    Ok(DecodedPageShape {
298        kind,
299        items: usize::try_from(item_count).map_err(|_| CanonicalDecodeError::LengthOverflow)?,
300        nested_bytes: 0,
301    })
302}
303
304/// Computes the typed identity of one canonical blob-index page.
305///
306/// # Errors
307///
308/// Returns the same validation errors as [`encode_blob_page`].
309pub fn blob_page_id(page: &BlobPage, maximum_items: u32) -> Result<ObjectId, CanonicalDecodeError> {
310    let bytes = encode_blob_page(page, maximum_items)?;
311    Ok(ObjectId {
312        kind: ObjectKind::Blob,
313        digest: object_digest(ObjectKind::Blob, &bytes),
314    })
315}
316
317/// Hard bounds for streaming immutable-blob construction.
318#[derive(Clone, Copy, Debug, Eq, PartialEq)]
319pub struct BlobBuildOptions {
320    /// Positive maximum bytes per authenticated content chunk.
321    pub chunk_bytes: u32,
322    /// At least two references per blob-index page.
323    pub page_items: u32,
324    /// Maximum canonical bytes in one authenticated blob-index page.
325    pub page_bytes: u32,
326    /// Maximum logical bytes consumed from the source.
327    pub maximum_blob_bytes: u64,
328}
329
330/// Successful streaming blob construction.
331#[derive(Clone, Debug, Eq, PartialEq)]
332pub struct BlobBuild {
333    /// Authenticated blob-index root.
334    pub root: ObjectId,
335    /// Exact logical content length.
336    pub logical_bytes: u64,
337    /// Exact source, encoding, and backend work receipt.
338    pub work: WorkCounters,
339}
340
341/// Nonblocking bounded byte source for browser, network, and native streams.
342pub trait AsyncBlobSource {
343    /// Fills some prefix of `destination`, returning zero only at end of stream.
344    fn read<'a>(
345        &'a mut self,
346        destination: &'a mut [u8],
347        cancellation: &'a CancellationToken,
348    ) -> impl Future<Output = std::io::Result<usize>>;
349
350    /// Optionally returns one owned chunk without copying into a caller buffer.
351    fn read_owned(
352        &mut self,
353        _maximum: usize,
354        _cancellation: &CancellationToken,
355    ) -> impl Future<Output = std::io::Result<Option<Bytes>>> {
356        async { Ok(None) }
357    }
358
359    /// Drains exact work performed by the preceding read, when the source
360    /// already measures its own I/O. Returning `Some` replaces the builder's
361    /// inferred source-byte count for that read; `None` retains that count.
362    fn take_work(&mut self) -> Option<WorkCounters> {
363        None
364    }
365}
366
367impl<T: Read> AsyncBlobSource for T {
368    async fn read<'a>(
369        &'a mut self,
370        destination: &'a mut [u8],
371        cancellation: &'a CancellationToken,
372    ) -> std::io::Result<usize> {
373        if cancellation.is_cancelled() {
374            return Err(std::io::Error::new(
375                std::io::ErrorKind::Interrupted,
376                "blob source read cancelled",
377            ));
378        }
379        Read::read(self, destination)
380    }
381}
382
383/// Streams a source into fixed-size authenticated chunks and a bounded index.
384///
385/// # Errors
386///
387/// Rejects invalid options, oversized input, source/backend failures, arithmetic
388/// overflow, or work beyond the admitted budget. It never materializes the
389/// complete input in one buffer.
390pub fn build_blob<S: crate::ImmediateObjectStore, R: Read>(
391    store: &S,
392    source: &mut R,
393    options: BlobBuildOptions,
394    budget: WorkBudget,
395) -> Result<BlobBuild, BlobBuildFailure> {
396    let cancellation = CancellationToken::new();
397    crate::async_storage::poll_immediate(build_blob_async(
398        store,
399        source,
400        options,
401        budget,
402        &cancellation,
403    ))
404}
405
406/// Asynchronously streams a source into authenticated chunks and a bounded,
407/// incremental blob index.
408///
409/// # Errors
410///
411/// Returns the same receipt-bearing failures as [`build_blob`], including
412/// cooperative cancellation before the next source or object-store operation.
413pub async fn build_blob_async<S: AsyncObjectStore, R: AsyncBlobSource>(
414    store: &S,
415    source: &mut R,
416    options: BlobBuildOptions,
417    budget: WorkBudget,
418    cancellation: &CancellationToken,
419) -> Result<BlobBuild, BlobBuildFailure> {
420    let chunk_capacity = validate_blob_build(options, cancellation)?;
421    let (mut batching, mut work) = BlobBatchStore::new(store, budget)?;
422    let mut index = BlobIndexBuilder::new(options);
423    let mut logical_bytes = 0_u64;
424    loop {
425        let detection_bytes = options
426            .maximum_blob_bytes
427            .saturating_sub(logical_bytes)
428            .saturating_add(1);
429        let allocation = chunk_capacity.min(usize::try_from(detection_bytes).unwrap_or(usize::MAX));
430        let retained = batching.retained_bytes();
431        let owned = source.read_owned(allocation, cancellation).await;
432        let source_work = source.take_work();
433        if let Some(measured) = source_work {
434            work = charge_source_work(
435                work,
436                measured,
437                retained.saturating_add(index.live_allocation_bytes),
438                budget,
439            )?;
440        }
441        let owned = owned.map_err(|error| build_failed(BlobBuildError::Source(error), work))?;
442        if let Some(chunk) = owned {
443            if chunk.is_empty() {
444                break;
445            }
446            let filled = chunk.len();
447            (logical_bytes, work) = accept_owned_blob_chunk(
448                &mut batching,
449                &mut index,
450                chunk,
451                allocation,
452                retained,
453                source_work.is_none(),
454                logical_bytes,
455                options.maximum_blob_bytes,
456                budget,
457                work,
458                cancellation,
459            )
460            .await?;
461            if filled < allocation {
462                break;
463            }
464            continue;
465        }
466        let (bytes, filled, retained_capacity, simultaneous, next_work) = read_blob_chunk(
467            source,
468            allocation,
469            index.live_allocation_bytes,
470            retained,
471            work,
472            budget,
473            cancellation,
474        )
475        .await?;
476        work = next_work;
477        if filled == 0 {
478            break;
479        }
480        let bytes = if filled == bytes.len() {
481            Bytes::from(bytes)
482        } else {
483            Bytes::copy_from_slice(
484                bytes
485                    .get(..filled)
486                    .ok_or_else(|| build_failed(BlobBuildError::TooLarge, work))?,
487            )
488        };
489        (logical_bytes, work) = accept_owned_blob_chunk(
490            &mut batching,
491            &mut index,
492            bytes,
493            allocation,
494            retained,
495            false,
496            logical_bytes,
497            options.maximum_blob_bytes,
498            budget,
499            work,
500            cancellation,
501        )
502        .await?;
503        let _ = (retained_capacity, simultaneous);
504        if filled < allocation {
505            break;
506        }
507    }
508    let root = index
509        .finish(&mut batching, budget, &mut work, cancellation)
510        .await?;
511    work = finish_blob_batch(&mut batching, work, budget, cancellation).await?;
512    Ok(BlobBuild {
513        root,
514        logical_bytes,
515        work,
516    })
517}
518
519#[allow(clippy::too_many_arguments)]
520async fn read_blob_chunk<R: AsyncBlobSource>(
521    source: &mut R,
522    allocation: usize,
523    index_bytes: u64,
524    retained: u64,
525    mut work: WorkCounters,
526    budget: WorkBudget,
527    cancellation: &CancellationToken,
528) -> Result<(Vec<u8>, usize, u64, u64, WorkCounters), BlobBuildFailure> {
529    let (mut bytes, prospective, simultaneous, retained_capacity) =
530        allocate_chunk_buffer(allocation, index_bytes, retained, work, budget)?;
531    work = prospective;
532    let mut filled = 0_usize;
533    while filled < bytes.len() {
534        if cancellation.is_cancelled() {
535            return Err(build_failed(BlobBuildError::Cancelled, work));
536        }
537        #[allow(clippy::indexing_slicing, reason = "bounded by while loop above")]
538        let read = AsyncBlobSource::read(source, &mut bytes[filled..], cancellation).await;
539        let measured = source.take_work();
540        if let Some(measured) = measured {
541            work = charge_source_work(work, measured, simultaneous, budget)?;
542        }
543        match read {
544            Ok(0) => break,
545            Ok(count) => {
546                if count > bytes.len() - filled {
547                    return Err(build_failed(BlobBuildError::TooLarge, work));
548                }
549                filled = filled.saturating_add(count);
550                if measured.is_none() {
551                    work = build_add(
552                        work,
553                        WorkCounters {
554                            source_bytes_read: u64::try_from(count).unwrap_or(u64::MAX),
555                            ..WorkCounters::default()
556                        },
557                    )?;
558                }
559                build_verify(work, budget)?;
560            }
561            Err(error) => return Err(build_failed(BlobBuildError::Source(error), work)),
562        }
563    }
564    Ok((bytes, filled, retained_capacity, simultaneous, work))
565}
566
567fn charge_source_work(
568    work: WorkCounters,
569    mut measured: WorkCounters,
570    live_bytes: u64,
571    budget: WorkBudget,
572) -> Result<WorkCounters, BlobBuildFailure> {
573    let simultaneous = live_bytes
574        .checked_add(measured.peak_allocation_bytes)
575        .ok_or_else(|| build_failed(BlobBuildError::TooLarge, work))?;
576    measured.peak_allocation_bytes = 0;
577    let mut combined = build_add(work, measured)?;
578    combined.peak_allocation_bytes = combined.peak_allocation_bytes.max(simultaneous);
579    build_verify(combined, budget)?;
580    Ok(combined)
581}
582
583#[allow(clippy::too_many_arguments)]
584async fn accept_owned_blob_chunk<S: AsyncObjectStore>(
585    batching: &mut BlobBatchStore<'_, S>,
586    index: &mut BlobIndexBuilder,
587    chunk: Bytes,
588    allocation: usize,
589    retained: u64,
590    owned_source: bool,
591    logical_bytes: u64,
592    maximum_blob_bytes: u64,
593    budget: WorkBudget,
594    work: WorkCounters,
595    cancellation: &CancellationToken,
596) -> Result<(u64, WorkCounters), BlobBuildFailure> {
597    if chunk.len() > allocation {
598        return Err(build_failed(BlobBuildError::TooLarge, work));
599    }
600    let retained_capacity = chunk.len() as u64;
601    let prospective = build_add(
602        work,
603        WorkCounters {
604            source_bytes_read: if owned_source { retained_capacity } else { 0 },
605            peak_allocation_bytes: index
606                .live_allocation_bytes
607                .saturating_add(retained)
608                .saturating_add(retained_capacity),
609            ..WorkCounters::default()
610        },
611    )?;
612    build_verify(prospective, budget)?;
613    let logical_bytes = logical_bytes
614        .checked_add(retained_capacity)
615        .ok_or_else(|| build_failed(BlobBuildError::TooLarge, prospective))?;
616    if logical_bytes > maximum_blob_bytes {
617        return Err(build_failed(BlobBuildError::TooLarge, prospective));
618    }
619    let first_offset = logical_bytes - retained_capacity;
620    let chunk_id = ObjectId {
621        kind: ObjectKind::BlobChunk,
622        digest: object_digest(ObjectKind::BlobChunk, &chunk),
623    };
624    let mut work = build_put(
625        batching,
626        chunk_id,
627        chunk,
628        PutAllocation {
629            retained: retained_capacity,
630            live: index
631                .live_allocation_bytes
632                .saturating_add(retained_capacity),
633        },
634        budget,
635        prospective,
636        cancellation,
637    )
638    .await?;
639    index
640        .push_chunk(
641            batching,
642            BlobChunkRef {
643                first_offset,
644                end_offset: logical_bytes,
645                chunk: chunk_id,
646            },
647            budget,
648            &mut work,
649            cancellation,
650        )
651        .await?;
652    Ok((logical_bytes, work))
653}
654
655fn validate_blob_build(
656    options: BlobBuildOptions,
657    cancellation: &CancellationToken,
658) -> Result<usize, BlobBuildFailure> {
659    if cancellation.is_cancelled() {
660        return Err(OperationFailure::before_work(BlobBuildError::Cancelled));
661    }
662    if options.chunk_bytes == 0
663        || effective_page_width(options).is_none()
664        || options.maximum_blob_bytes == 0
665    {
666        return Err(build_failed(
667            BlobBuildError::InvalidOptions,
668            WorkCounters::default(),
669        ));
670    }
671    usize::try_from(options.chunk_bytes)
672        .map_err(|_| build_failed(BlobBuildError::InvalidOptions, WorkCounters::default()))
673}
674
675async fn finish_blob_batch<S: AsyncObjectStore>(
676    batching: &mut BlobBatchStore<'_, S>,
677    work: WorkCounters,
678    budget: WorkBudget,
679    cancellation: &CancellationToken,
680) -> Result<WorkCounters, BlobBuildFailure> {
681    let retained = batching.retained_bytes();
682    let mut remaining = work
683        .remaining(budget)
684        .map_err(|error| build_failed(BlobBuildError::Work(error), work))?;
685    remaining.peak_allocation_bytes = remaining
686        .peak_allocation_bytes
687        .checked_sub(retained)
688        .ok_or_else(|| build_failed(BlobBuildError::Work(WorkError::Overflow), work))?;
689    let receipt = batching
690        .finish(remaining, cancellation)
691        .await
692        .map_err(|failure| failure.map_with_prior_work(work, BlobBuildError::Storage))?;
693    let backend_peak = receipt.work.peak_allocation_bytes;
694    let mut backend_work = receipt.work;
695    backend_work.peak_allocation_bytes = 0;
696    let mut combined = build_add(work, backend_work)?;
697    let simultaneous = retained
698        .checked_add(backend_peak)
699        .ok_or_else(|| build_failed(BlobBuildError::Work(WorkError::Overflow), work))?;
700    combined.peak_allocation_bytes = combined.peak_allocation_bytes.max(simultaneous);
701    build_verify(combined, budget)?;
702    Ok(combined)
703}
704
705const BLOB_BATCH_OBJECTS: usize = 4;
706const BLOB_BATCH_BYTES: u64 = 4 * 1024 * 1024;
707
708struct BlobBatchState {
709    writes: Vec<crate::storage::ObjectWrite>,
710    bytes: u64,
711}
712
713struct BlobBatchStore<'a, S> {
714    inner: &'a S,
715    state: BlobBatchState,
716    vector_bytes: u64,
717}
718
719impl<'a, S: AsyncObjectStore> BlobBatchStore<'a, S> {
720    fn new(inner: &'a S, budget: WorkBudget) -> Result<(Self, WorkCounters), BlobBuildFailure> {
721        let minimum = WorkCounters {
722            allocation_operations: 1,
723            peak_allocation_bytes: u64::try_from(BLOB_BATCH_OBJECTS)
724                .unwrap_or(u64::MAX)
725                .saturating_mul(
726                    u64::try_from(size_of::<crate::storage::ObjectWrite>()).unwrap_or(u64::MAX),
727                ),
728            ..WorkCounters::default()
729        };
730        minimum
731            .verify(budget)
732            .map_err(|error| build_failed(BlobBuildError::Work(error), WorkCounters::default()))?;
733        let mut writes = Vec::new();
734        writes
735            .try_reserve_exact(BLOB_BATCH_OBJECTS)
736            .map_err(|_| build_failed(BlobBuildError::AllocationFailed, WorkCounters::default()))?;
737        let vector_bytes = u64::try_from(writes.capacity())
738            .unwrap_or(u64::MAX)
739            .saturating_mul(
740                u64::try_from(size_of::<crate::storage::ObjectWrite>()).unwrap_or(u64::MAX),
741            );
742        let admitted = WorkCounters {
743            allocation_operations: 1,
744            peak_allocation_bytes: vector_bytes,
745            ..WorkCounters::default()
746        };
747        admitted
748            .verify(budget)
749            .map_err(|error| build_failed(BlobBuildError::Work(error), admitted))?;
750        Ok((
751            Self {
752                inner,
753                state: BlobBatchState { writes, bytes: 0 },
754                vector_bytes,
755            },
756            admitted,
757        ))
758    }
759
760    fn retained_bytes(&self) -> u64 {
761        self.vector_bytes.saturating_add(self.state.bytes)
762    }
763
764    async fn flush_locked(
765        &mut self,
766        // The caller has already removed every buffer retained by the blob builder and this
767        // batch. Passing the original budget here would admit hidden simultaneous allocation.
768        backend_budget: WorkBudget,
769        cancellation: &CancellationToken,
770    ) -> crate::storage::ObjectResult<()> {
771        if self.state.writes.is_empty() {
772            return Ok(crate::storage::ObjectReceipt {
773                value: (),
774                work: WorkCounters::default(),
775            });
776        }
777        let receipt = self
778            .inner
779            .put_many(&self.state.writes, backend_budget, cancellation)
780            .await?;
781        self.state.writes.clear();
782        self.state.bytes = 0;
783        Ok(crate::storage::ObjectReceipt {
784            value: (),
785            work: receipt.work,
786        })
787    }
788
789    async fn finish(
790        &mut self,
791        budget: WorkBudget,
792        cancellation: &CancellationToken,
793    ) -> crate::storage::ObjectResult<()> {
794        self.flush_locked(budget, cancellation).await
795    }
796
797    async fn put_with_retained(
798        &mut self,
799        object_id: ObjectId,
800        bytes: Bytes,
801        retained_bytes: u64,
802        budget: WorkBudget,
803        cancellation: &CancellationToken,
804    ) -> crate::storage::ObjectResult<()> {
805        cancellation.check().map_err(|_| {
806            crate::storage::ObjectFailure::before_work(crate::storage::ObjectStoreError::Cancelled)
807        })?;
808        self.state.bytes = self
809            .state
810            .bytes
811            .checked_add(retained_bytes)
812            .ok_or_else(|| {
813                crate::storage::ObjectFailure::before_work(
814                    crate::storage::ObjectStoreError::Rejected(
815                        "object write batch length overflowed".to_owned(),
816                    ),
817                )
818            })?;
819        self.state
820            .writes
821            .push(crate::storage::ObjectWrite { object_id, bytes });
822        if self.state.writes.len() >= BLOB_BATCH_OBJECTS || self.state.bytes >= BLOB_BATCH_BYTES {
823            return self.flush_locked(budget, cancellation).await;
824        }
825        Ok(crate::storage::ObjectReceipt {
826            value: (),
827            work: WorkCounters::default(),
828        })
829    }
830}
831
832fn allocate_chunk_buffer(
833    allocation: usize,
834    index_live_bytes: u64,
835    retained_batch_bytes: u64,
836    work: WorkCounters,
837    budget: WorkBudget,
838) -> Result<(Vec<u8>, WorkCounters, u64, u64), BlobBuildFailure> {
839    let allocation_bytes = u64::try_from(allocation).unwrap_or(u64::MAX);
840    let simultaneous = index_live_bytes
841        .checked_add(allocation_bytes)
842        .and_then(|bytes| bytes.checked_add(retained_batch_bytes))
843        .ok_or_else(|| build_failed(BlobBuildError::TooLarge, work))?;
844    let mut prospective = build_add(
845        work,
846        WorkCounters {
847            allocation_operations: 1,
848            ..WorkCounters::default()
849        },
850    )?;
851    prospective.peak_allocation_bytes = prospective.peak_allocation_bytes.max(simultaneous);
852    build_verify(prospective, budget)?;
853    let mut bytes = Vec::new();
854    if bytes.try_reserve_exact(allocation).is_err() {
855        return Err(build_failed(BlobBuildError::AllocationFailed, work));
856    }
857    bytes.resize(allocation, 0);
858    let retained_capacity = u64::try_from(bytes.capacity()).unwrap_or(u64::MAX);
859    let actual_simultaneous = index_live_bytes
860        .checked_add(retained_capacity)
861        .and_then(|value| value.checked_add(retained_batch_bytes))
862        .ok_or_else(|| build_failed(BlobBuildError::TooLarge, prospective))?;
863    prospective.peak_allocation_bytes = prospective.peak_allocation_bytes.max(actual_simultaneous);
864    build_verify(prospective, budget)?;
865    Ok((bytes, prospective, actual_simultaneous, retained_capacity))
866}
867
868struct BlobIndexBuilder {
869    options: BlobBuildOptions,
870    width: usize,
871    leaf: Vec<BlobChunkRef>,
872    levels: Vec<Vec<BlobChild>>,
873    live_allocation_bytes: u64,
874}
875
876impl BlobIndexBuilder {
877    fn new(options: BlobBuildOptions) -> Self {
878        Self {
879            options,
880            width: effective_page_width(options).unwrap_or(0),
881            leaf: Vec::new(),
882            levels: Vec::new(),
883            live_allocation_bytes: 0,
884        }
885    }
886
887    async fn push_chunk<S: AsyncObjectStore>(
888        &mut self,
889        store: &mut BlobBatchStore<'_, S>,
890        chunk: BlobChunkRef,
891        budget: WorkBudget,
892        work: &mut WorkCounters,
893        cancellation: &CancellationToken,
894    ) -> Result<(), BlobBuildFailure> {
895        reserve_page_items(
896            &mut self.leaf,
897            self.width,
898            &mut self.live_allocation_bytes,
899            work,
900            budget,
901        )?;
902        self.leaf.push(chunk);
903        if self.leaf.len() == self.width {
904            self.flush_leaf(store, budget, work, cancellation).await?;
905        }
906        Ok(())
907    }
908
909    async fn flush_leaf<S: AsyncObjectStore>(
910        &mut self,
911        store: &mut BlobBatchStore<'_, S>,
912        budget: WorkBudget,
913        work: &mut WorkCounters,
914        cancellation: &CancellationToken,
915    ) -> Result<(), BlobBuildFailure> {
916        let mut items = std::mem::take(&mut self.leaf);
917        if items.is_empty() {
918            return Ok(());
919        }
920        let (first_offset, end_offset) = match items.as_slice() {
921            [only] => (only.first_offset, only.end_offset),
922            [first, .., last] => (first.first_offset, last.end_offset),
923            [] => unreachable!("checked items.is_empty() above"),
924        };
925        let page = BlobPage {
926            first_offset,
927            end_offset,
928            node: BlobNode::Leaf(items),
929        };
930        let page_id = put_blob_page(
931            store,
932            &page,
933            self.options,
934            self.live_allocation_bytes,
935            budget,
936            work,
937            cancellation,
938        )
939        .await?;
940        let child = BlobChild {
941            first_offset: page.first_offset,
942            end_offset: page.end_offset,
943            page: page_id,
944        };
945        let BlobNode::Leaf(reusable) = page.node else {
946            unreachable!();
947        };
948        items = reusable;
949        items.clear();
950        self.leaf = items;
951        self.push_child(store, 0, child, budget, work, cancellation)
952            .await
953    }
954
955    #[allow(
956        clippy::indexing_slicing,
957        reason = "`self.ensure_level(level, ...)` runs immediately above every use of \
958                  `self.levels[level]` in this loop iteration; it loops `while self.levels.len() \
959                  <= level` pushing new levels, so afterwards `level < self.levels.len()` and \
960                  `self.levels` is not resized again before this iteration's accesses"
961    )]
962    async fn push_child<S: AsyncObjectStore>(
963        &mut self,
964        store: &mut BlobBatchStore<'_, S>,
965        mut level: usize,
966        mut child: BlobChild,
967        budget: WorkBudget,
968        work: &mut WorkCounters,
969        cancellation: &CancellationToken,
970    ) -> Result<(), BlobBuildFailure> {
971        loop {
972            self.ensure_level(level, work, budget)?;
973            reserve_page_items(
974                &mut self.levels[level],
975                self.width,
976                &mut self.live_allocation_bytes,
977                work,
978                budget,
979            )?;
980            self.levels[level].push(child);
981            if self.levels[level].len() < self.width {
982                return Ok(());
983            }
984            child = self
985                .flush_internal(store, level, budget, work, cancellation)
986                .await?;
987            level = level
988                .checked_add(1)
989                .ok_or_else(|| build_failed(BlobBuildError::TooLarge, *work))?;
990        }
991    }
992
993    #[allow(
994        clippy::indexing_slicing,
995        reason = "`flush_internal` is only called from `push_child` (right after \
996                  `self.ensure_level(level, ...)` established `level < self.levels.len()`, with \
997                  no intervening resize) and from `finish` (with `level` taken from \
998                  `self.levels.iter().position(...)`, which is inherently `< self.levels.len()`)"
999    )]
1000    async fn flush_internal<S: AsyncObjectStore>(
1001        &mut self,
1002        store: &mut BlobBatchStore<'_, S>,
1003        level: usize,
1004        budget: WorkBudget,
1005        work: &mut WorkCounters,
1006        cancellation: &CancellationToken,
1007    ) -> Result<BlobChild, BlobBuildFailure> {
1008        let items = std::mem::take(&mut self.levels[level]);
1009        let (first_offset, end_offset) = match items.as_slice() {
1010            [only] => (only.first_offset, only.end_offset),
1011            [first, .., last] => (first.first_offset, last.end_offset),
1012            [] => unreachable!(
1013                "flush_internal is only called once self.levels[level] is non-empty: from \
1014                 push_child at self.width (>= 2) items, or from finish() for a level chosen via \
1015                 self.levels.iter().position(|items| !items.is_empty())"
1016            ),
1017        };
1018        let page = BlobPage {
1019            first_offset,
1020            end_offset,
1021            node: BlobNode::Internal(items),
1022        };
1023        let page_id = put_blob_page(
1024            store,
1025            &page,
1026            self.options,
1027            self.live_allocation_bytes,
1028            budget,
1029            work,
1030            cancellation,
1031        )
1032        .await?;
1033        let child = BlobChild {
1034            first_offset: page.first_offset,
1035            end_offset: page.end_offset,
1036            page: page_id,
1037        };
1038        let BlobNode::Internal(mut reusable) = page.node else {
1039            unreachable!();
1040        };
1041        reusable.clear();
1042        self.levels[level] = reusable;
1043        Ok(child)
1044    }
1045
1046    fn ensure_level(
1047        &mut self,
1048        level: usize,
1049        work: &mut WorkCounters,
1050        budget: WorkBudget,
1051    ) -> Result<(), BlobBuildFailure> {
1052        while self.levels.len() <= level {
1053            let bytes = u64::try_from(size_of::<Vec<BlobChild>>()).unwrap_or(u64::MAX);
1054            let next_live = self
1055                .live_allocation_bytes
1056                .checked_add(bytes)
1057                .ok_or_else(|| build_failed(BlobBuildError::TooLarge, *work))?;
1058            let mut prospective = work
1059                .checked_add(WorkCounters {
1060                    allocation_operations: 1,
1061                    ..WorkCounters::default()
1062                })
1063                .map_err(|error| build_failed(error.into(), *work))?;
1064            prospective.peak_allocation_bytes = prospective.peak_allocation_bytes.max(next_live);
1065            build_verify(prospective, budget)?;
1066            if self.levels.try_reserve_exact(1).is_err() {
1067                return Err(build_failed(BlobBuildError::AllocationFailed, *work));
1068            }
1069            self.levels.push(Vec::new());
1070            self.live_allocation_bytes = next_live;
1071            *work = prospective;
1072        }
1073        Ok(())
1074    }
1075
1076    async fn finish<S: AsyncObjectStore>(
1077        mut self,
1078        store: &mut BlobBatchStore<'_, S>,
1079        budget: WorkBudget,
1080        work: &mut WorkCounters,
1081        cancellation: &CancellationToken,
1082    ) -> Result<ObjectId, BlobBuildFailure> {
1083        if self.leaf.is_empty() && self.levels.is_empty() {
1084            return put_blob_page(
1085                store,
1086                &BlobPage {
1087                    first_offset: 0,
1088                    end_offset: 0,
1089                    node: BlobNode::Leaf(Vec::new()),
1090                },
1091                self.options,
1092                self.live_allocation_bytes,
1093                budget,
1094                work,
1095                cancellation,
1096            )
1097            .await;
1098        }
1099        self.flush_leaf(store, budget, work, cancellation).await?;
1100        loop {
1101            let Some(level) = self.levels.iter().position(|items| !items.is_empty()) else {
1102                return Err(build_failed(BlobBuildError::InvalidIndexState, *work));
1103            };
1104            #[allow(
1105                clippy::indexing_slicing,
1106                reason = "`level` comes from `self.levels.iter().position(...)` above, so `level \
1107                          < self.levels.len()` and `level + 1 <= self.levels.len()`, which is \
1108                          in-bounds for a `RangeFrom` slice"
1109            )]
1110            let higher_is_empty = self.levels[level + 1..].iter().all(Vec::is_empty);
1111            #[allow(
1112                clippy::indexing_slicing,
1113                reason = "`level < self.levels.len()` from `position(...)` above; the \
1114                          `levels[level][0]` access is additionally guarded by the \
1115                          `levels[level].len() == 1` check in this same condition"
1116            )]
1117            if higher_is_empty && self.levels[level].len() == 1 {
1118                return Ok(self.levels[level][0].page);
1119            }
1120            let child = self
1121                .flush_internal(store, level, budget, work, cancellation)
1122                .await?;
1123            self.push_child(store, level + 1, child, budget, work, cancellation)
1124                .await?;
1125        }
1126    }
1127}
1128
1129fn effective_page_width(options: BlobBuildOptions) -> Option<usize> {
1130    let item_bound = usize::try_from(options.page_items).ok()?;
1131    let byte_bound = usize::try_from(options.page_bytes)
1132        .ok()?
1133        .checked_sub(BLOB_PAGE_HEADER_BYTES)?
1134        / BLOB_PAGE_ITEM_BYTES;
1135    let width = item_bound.min(byte_bound);
1136    (width >= 2).then_some(width)
1137}
1138
1139fn reserve_page_items<T>(
1140    items: &mut Vec<T>,
1141    width: usize,
1142    live_allocation_bytes: &mut u64,
1143    work: &mut WorkCounters,
1144    budget: WorkBudget,
1145) -> Result<(), BlobBuildFailure> {
1146    if items.capacity() >= width {
1147        return Ok(());
1148    }
1149    let bytes = width
1150        .checked_mul(size_of::<T>())
1151        .map(crate::foundation::usize_to_u64)
1152        .ok_or_else(|| build_failed(BlobBuildError::TooLarge, *work))?;
1153    let next_live = live_allocation_bytes
1154        .checked_add(bytes)
1155        .ok_or_else(|| build_failed(BlobBuildError::TooLarge, *work))?;
1156    let mut prospective = work
1157        .checked_add(WorkCounters {
1158            allocation_operations: 1,
1159            ..WorkCounters::default()
1160        })
1161        .map_err(|error| build_failed(error.into(), *work))?;
1162    prospective.peak_allocation_bytes = prospective.peak_allocation_bytes.max(next_live);
1163    build_verify(prospective, budget)?;
1164    if items.try_reserve_exact(width - items.capacity()).is_err() {
1165        return Err(build_failed(BlobBuildError::AllocationFailed, *work));
1166    }
1167    *live_allocation_bytes = next_live;
1168    *work = prospective;
1169    Ok(())
1170}
1171
1172async fn put_blob_page<S: AsyncObjectStore>(
1173    store: &mut BlobBatchStore<'_, S>,
1174    page: &BlobPage,
1175    options: BlobBuildOptions,
1176    live_allocation_bytes: u64,
1177    budget: WorkBudget,
1178    work: &mut WorkCounters,
1179    cancellation: &CancellationToken,
1180) -> Result<ObjectId, BlobBuildFailure> {
1181    let retained = store.retained_bytes();
1182    let encoded = encode_blob_page(page, options.page_items)
1183        .map_err(|error| build_failed(BlobBuildError::Encode(error), *work))?;
1184    if encoded.len() > usize::try_from(options.page_bytes).unwrap_or(usize::MAX) {
1185        return Err(build_failed(
1186            BlobBuildError::PageTooLarge {
1187                observed: u64::try_from(encoded.len()).unwrap_or(u64::MAX),
1188                maximum: u64::from(options.page_bytes),
1189            },
1190            *work,
1191        ));
1192    }
1193    let page_id = ObjectId {
1194        kind: ObjectKind::Blob,
1195        digest: object_digest(ObjectKind::Blob, &encoded),
1196    };
1197    let encoded_bytes = u64::try_from(encoded.capacity()).unwrap_or(u64::MAX);
1198    let simultaneous = live_allocation_bytes
1199        .checked_add(encoded_bytes)
1200        .and_then(|bytes| bytes.checked_add(retained))
1201        .ok_or_else(|| build_failed(BlobBuildError::TooLarge, *work))?;
1202    let mut encoded_work = build_add(
1203        *work,
1204        WorkCounters {
1205            page_writes: 1,
1206            bytes_encoded: u64::try_from(encoded.len()).unwrap_or(u64::MAX),
1207            allocation_operations: 1,
1208            ..WorkCounters::default()
1209        },
1210    )?;
1211    encoded_work.peak_allocation_bytes = encoded_work.peak_allocation_bytes.max(simultaneous);
1212    build_verify(encoded_work, budget)?;
1213    *work = build_put(
1214        store,
1215        page_id,
1216        Bytes::from(encoded),
1217        PutAllocation {
1218            retained: encoded_bytes,
1219            live: simultaneous,
1220        },
1221        budget,
1222        encoded_work,
1223        cancellation,
1224    )
1225    .await?;
1226    Ok(page_id)
1227}
1228
1229#[derive(Clone, Copy)]
1230struct PutAllocation {
1231    retained: u64,
1232    live: u64,
1233}
1234
1235async fn build_put<S: AsyncObjectStore>(
1236    store: &mut BlobBatchStore<'_, S>,
1237    object: ObjectId,
1238    bytes: Bytes,
1239    allocation: PutAllocation,
1240    budget: WorkBudget,
1241    work: WorkCounters,
1242    cancellation: &CancellationToken,
1243) -> Result<WorkCounters, BlobBuildFailure> {
1244    let mut remaining = work
1245        .remaining(budget)
1246        .map_err(|error| build_failed(BlobBuildError::Work(error), work))?;
1247    remaining.peak_allocation_bytes = budget
1248        .peak_allocation_bytes
1249        .checked_sub(allocation.live)
1250        .ok_or_else(|| build_failed(BlobBuildError::Work(WorkError::Overflow), work))?;
1251    match store
1252        .put_with_retained(object, bytes, allocation.retained, remaining, cancellation)
1253        .await
1254    {
1255        Ok(receipt) => {
1256            let backend_peak = allocation
1257                .live
1258                .checked_add(receipt.work.peak_allocation_bytes)
1259                .ok_or_else(|| build_failed(BlobBuildError::Work(WorkError::Overflow), work))?;
1260            let mut backend_work = receipt.work;
1261            backend_work.peak_allocation_bytes = 0;
1262            let mut combined = build_add(work, backend_work)?;
1263            combined.peak_allocation_bytes = combined.peak_allocation_bytes.max(backend_peak);
1264            build_verify(combined, budget)?;
1265            Ok(combined)
1266        }
1267        Err(failure) => Err(failure.map_with_prior_work(work, BlobBuildError::Storage)),
1268    }
1269}
1270
1271fn build_add(left: WorkCounters, right: WorkCounters) -> Result<WorkCounters, BlobBuildFailure> {
1272    left.checked_add(right)
1273        .map_err(|error| build_failed(BlobBuildError::Work(error), left))
1274}
1275
1276fn build_verify(work: WorkCounters, budget: WorkBudget) -> Result<(), BlobBuildFailure> {
1277    work.verify(budget)
1278        .map_err(|error| build_failed(BlobBuildError::Work(error), work))
1279}
1280
1281/// Streaming blob-build failure retaining exact completed work.
1282pub type BlobBuildFailure = OperationFailure<BlobBuildError>;
1283
1284/// Stable streaming blob-build errors.
1285#[derive(Debug, Error)]
1286pub enum BlobBuildError {
1287    /// Cooperative cancellation occurred before the next source or store operation.
1288    #[error("blob build was cancelled")]
1289    Cancelled,
1290    /// Chunk, page, or total-size bounds are invalid.
1291    #[error("blob build options are invalid")]
1292    InvalidOptions,
1293    /// Canonical blob-index page exceeds its explicit byte ceiling.
1294    #[error("blob index page has {observed} bytes; maximum is {maximum}")]
1295    PageTooLarge {
1296        /// Canonical encoded bytes.
1297        observed: u64,
1298        /// Configured page-byte ceiling.
1299        maximum: u64,
1300    },
1301    /// Fallible bounded index scratch allocation was unavailable.
1302    #[error("blob build scratch allocation failed")]
1303    AllocationFailed,
1304    /// Incremental index finalization reached an impossible state.
1305    #[error("blob build index state is invalid")]
1306    InvalidIndexState,
1307    /// Input exceeded the admitted logical size.
1308    #[error("blob source exceeds its admitted logical size")]
1309    TooLarge,
1310    /// Source stream failed.
1311    #[error("blob source failed: {0}")]
1312    Source(#[source] std::io::Error),
1313    /// Canonical blob-index encoding failed.
1314    #[error(transparent)]
1315    Encode(#[from] CanonicalDecodeError),
1316    /// Immutable-object backend failed.
1317    #[error(transparent)]
1318    Storage(#[from] ObjectStoreError),
1319    /// Exact work overflowed or exceeded its budget.
1320    #[error(transparent)]
1321    Work(#[from] WorkError),
1322}
1323
1324fn build_failed(error: BlobBuildError, work: WorkCounters) -> BlobBuildFailure {
1325    OperationFailure::new(error, work)
1326}
1327
1328/// Successful authenticated logical blob range and exact work evidence.
1329#[derive(Clone, Debug, Eq, PartialEq)]
1330pub struct BlobRead {
1331    /// Requested bytes.
1332    pub bytes: Bytes,
1333    /// Exact kernel and backend work.
1334    pub work: WorkCounters,
1335    /// Exact authenticated forward page or chunk exposed by this traversal.
1336    pub next_residency: Option<ResidencyHint>,
1337}
1338
1339/// Reads only blob-index pages and chunks intersecting one exact range.
1340///
1341/// # Errors
1342///
1343/// Rejects invalid bounds, missing/corrupt objects, graph cycles, child-bound
1344/// forgery, incomplete coverage, or any work outside the admitted budget.
1345pub fn read_blob_range<S: ObjectStore>(
1346    store: &S,
1347    root: ObjectId,
1348    range: ByteRange,
1349    limits: DecodeLimits,
1350    budget: WorkBudget,
1351) -> Result<BlobRead, BlobReadFailure> {
1352    let mut machine = BlobRangeMachine::new(root, range, limits, budget)?;
1353    frontier::drive_sync(store, &mut machine)
1354}
1355
1356/// Asynchronously reads one authenticated logical blob range through the same
1357/// transition machine as [`read_blob_range`].
1358///
1359/// # Errors
1360///
1361/// Returns the same typed authentication and work failures, plus cooperative
1362/// cancellation before the next immutable-object read.
1363pub async fn read_blob_range_async<S: AsyncObjectStore>(
1364    store: &S,
1365    root: ObjectId,
1366    range: ByteRange,
1367    limits: DecodeLimits,
1368    budget: WorkBudget,
1369    cancellation: &CancellationToken,
1370) -> Result<BlobRead, BlobReadFailure> {
1371    if cancellation.is_cancelled() {
1372        return Err(failed(BlobReadError::Cancelled, WorkCounters::default()));
1373    }
1374    let mut machine = BlobRangeMachine::new(root, range, limits, budget)?;
1375    drive_blob_async(store, &mut machine, cancellation).await
1376}
1377
1378async fn drive_blob_async<S: AsyncObjectStore>(
1379    store: &S,
1380    machine: &mut BlobRangeMachine,
1381    cancellation: &CancellationToken,
1382) -> Result<BlobRead, BlobReadFailure> {
1383    loop {
1384        if cancellation.is_cancelled() {
1385            return Err(machine.cancelled());
1386        }
1387        if let Some(output) = machine.complete()? {
1388            return Ok(output);
1389        }
1390        if let Some(batch) = machine.prepare_chunk_batch()? {
1391            let receipt =
1392                AsyncObjectStore::read_many(store, &batch.requests, batch.remaining, cancellation)
1393                    .await
1394                    .map_err(|failure| machine.batch_storage_failure(batch.prospective, failure))?;
1395            machine.accept_chunk_batch(batch, receipt)?;
1396            continue;
1397        }
1398        let request = machine.prepare_read()?;
1399        let key = (request.page.kind == ObjectKind::Blob)
1400            .then(|| DecodedCacheKey::new::<BlobPage>(request.page, machine.limits));
1401        if let Some(key) = key {
1402            let cached = store
1403                .decoded_cache_get(key)
1404                .map_err(|error| failed(error.into(), machine.work))?;
1405            if let Some(cached) = cached {
1406                machine.accept_cached_page(cached)?;
1407                continue;
1408            }
1409        }
1410        let receipt = AsyncObjectStore::read(
1411            store,
1412            request.page,
1413            request.maximum_bytes,
1414            request.remaining,
1415            cancellation,
1416        )
1417        .await
1418        .map_err(|failure| machine.storage_failure(request.prospective, failure))?;
1419        machine.accept_read_with_cache(store, key, request.prospective, &receipt)?;
1420    }
1421}
1422
1423struct PendingChunkBatch {
1424    chunks: Vec<BlobChunkRef>,
1425    requests: Vec<ObjectReadRequest>,
1426    scratch_bytes: u64,
1427    remaining: WorkBudget,
1428    prospective: WorkCounters,
1429}
1430
1431#[derive(Clone, Copy)]
1432enum PendingBlobRead {
1433    Page {
1434        page: ObjectId,
1435        expected_first: u64,
1436        expected_end: Option<u64>,
1437        height: u16,
1438    },
1439    Chunk(BlobChunkRef),
1440}
1441
1442struct BlobRangeMachine {
1443    range: ByteRange,
1444    range_end: u64,
1445    limits: DecodeLimits,
1446    budget: WorkBudget,
1447    allocation: usize,
1448    maximum_pending: usize,
1449    pending: Vec<PendingBlobRead>,
1450    pending_capacity: LogicalVecCapacity,
1451    awaiting: Option<PendingBlobRead>,
1452    visited: VisitedObjectSet,
1453    output: Vec<u8>,
1454    work: WorkCounters,
1455    allocations: AllocationLedger,
1456    successor: Option<(u64, ResidencyHint)>,
1457}
1458
1459impl BlobRangeMachine {
1460    fn new(
1461        root: ObjectId,
1462        range: ByteRange,
1463        limits: DecodeLimits,
1464        budget: WorkBudget,
1465    ) -> Result<Self, BlobReadFailure> {
1466        if root.kind != ObjectKind::Blob {
1467            return Err(failed(
1468                BlobReadError::WrongRootKind,
1469                WorkCounters::default(),
1470            ));
1471        }
1472        let range_end = range
1473            .offset
1474            .checked_add(range.length)
1475            .ok_or_else(|| failed(BlobReadError::InvalidRange, WorkCounters::default()))?;
1476        let allocation = usize::try_from(range.length)
1477            .map_err(|_| failed(BlobReadError::InvalidRange, WorkCounters::default()))?;
1478        if !limits.page_limits_valid(1) {
1479            return Err(failed(
1480                BlobReadError::InvalidLimits,
1481                WorkCounters::default(),
1482            ));
1483        }
1484        let maximum_visited = usize::try_from(limits.maximum_visited_pages)
1485            .map_err(|_| failed(BlobReadError::InvalidLimits, WorkCounters::default()))?;
1486        let maximum_page_items = usize::try_from(limits.maximum_page_items)
1487            .map_err(|_| failed(BlobReadError::InvalidLimits, WorkCounters::default()))?;
1488        let maximum_pending = maximum_visited
1489            .checked_add(maximum_page_items)
1490            .ok_or_else(|| failed(BlobReadError::InvalidLimits, WorkCounters::default()))?;
1491        let mut allocations = AllocationLedger::default();
1492        let mut work = WorkCounters::default();
1493        allocations
1494            .claim_bytes(range.length, u64::from(allocation != 0), &mut work, budget)
1495            .map_err(|error| failed(error.into(), work))?;
1496        let visited = VisitedObjectSet::new(maximum_visited, &mut allocations, &mut work, budget)
1497            .map_err(|error| failed(error.into(), work))?;
1498        let mut pending = Vec::new();
1499        let mut pending_capacity = LogicalVecCapacity::default();
1500        pending_capacity
1501            .ensure_for_push(
1502                &mut pending,
1503                maximum_pending,
1504                &mut allocations,
1505                &mut work,
1506                budget,
1507            )
1508            .map_err(|error| failed(error.into(), work))?;
1509        pending.push(PendingBlobRead::Page {
1510            page: root,
1511            expected_first: 0,
1512            expected_end: None,
1513            height: 1,
1514        });
1515        let mut output = Vec::new();
1516        if output.try_reserve_exact(allocation).is_err() {
1517            return Err(failed(BlobReadError::AllocationFailed, work));
1518        }
1519        Ok(Self {
1520            range,
1521            range_end,
1522            limits,
1523            budget,
1524            allocation,
1525            maximum_pending,
1526            pending,
1527            pending_capacity,
1528            awaiting: None,
1529            visited,
1530            output,
1531            work,
1532            allocations,
1533            successor: None,
1534        })
1535    }
1536
1537    fn finish(&mut self) -> Result<BlobRead, BlobReadFailure> {
1538        if self.output.len() != self.allocation {
1539            return Err(failed(BlobReadError::IncompleteCoverage, self.work));
1540        }
1541        Ok(BlobRead {
1542            bytes: Bytes::from(std::mem::take(&mut self.output)),
1543            work: self.work,
1544            next_residency: self.successor.map(|(_, hint)| hint),
1545        })
1546    }
1547
1548    fn prepare_read(&mut self) -> Result<frontier::ReadRequest, BlobReadFailure> {
1549        let pending = self
1550            .pending
1551            .pop()
1552            .ok_or_else(|| failed(BlobReadError::TraversalState, self.work))?;
1553        let (object, maximum_bytes) = match pending {
1554            PendingBlobRead::Page { page, height, .. } => {
1555                if height > self.limits.maximum_page_height {
1556                    return Err(failed(BlobReadError::CycleOrHeight, self.work));
1557                }
1558                let visited = self
1559                    .visited
1560                    .insert(page, &mut self.allocations, &mut self.work, self.budget)
1561                    .map_err(|error| failed(error.into(), self.work))?;
1562                if !visited.inserted {
1563                    return Err(failed(BlobReadError::CycleOrHeight, self.work));
1564                }
1565                (page, self.limits.maximum_page_object_bytes())
1566            }
1567            PendingBlobRead::Chunk(chunk) => (chunk.chunk, chunk.end_offset - chunk.first_offset),
1568        };
1569        let mut remaining = self
1570            .work
1571            .remaining(self.budget)
1572            .map_err(|error| failed(error.into(), self.work))?;
1573        remaining.peak_allocation_bytes = self
1574            .budget
1575            .peak_allocation_bytes
1576            .checked_sub(self.allocations.live_bytes())
1577            .ok_or_else(|| failed(BlobReadError::Work(WorkError::Overflow), self.work))?;
1578        self.awaiting = Some(pending);
1579        Ok(frontier::ReadRequest {
1580            page: object,
1581            maximum_bytes,
1582            remaining,
1583            prospective: self.work,
1584        })
1585    }
1586
1587    fn prepare_chunk_batch(&mut self) -> Result<Option<PendingChunkBatch>, BlobReadFailure> {
1588        let chunk_count = self
1589            .pending
1590            .iter()
1591            .rev()
1592            .take_while(|pending| matches!(pending, PendingBlobRead::Chunk(_)))
1593            .count();
1594        if chunk_count < 2 {
1595            return Ok(None);
1596        }
1597        let scratch_bytes = u64::try_from(chunk_count)
1598            .unwrap_or(u64::MAX)
1599            .checked_mul(
1600                u64::try_from(size_of::<BlobChunkRef>() + size_of::<ObjectReadRequest>())
1601                    .unwrap_or(u64::MAX),
1602            )
1603            .ok_or_else(|| failed(BlobReadError::Work(WorkError::Overflow), self.work))?;
1604        let live_bytes = self
1605            .allocations
1606            .live_bytes()
1607            .checked_add(scratch_bytes)
1608            .ok_or_else(|| failed(BlobReadError::Work(WorkError::Overflow), self.work))?;
1609        let mut prospective = self.work;
1610        prospective.allocation_operations = prospective
1611            .allocation_operations
1612            .checked_add(2)
1613            .ok_or_else(|| failed(BlobReadError::Work(WorkError::Overflow), self.work))?;
1614        prospective.peak_allocation_bytes = prospective.peak_allocation_bytes.max(live_bytes);
1615        verify_work(prospective, self.budget)?;
1616        let mut chunks = Vec::new();
1617        let mut requests = Vec::new();
1618        chunks
1619            .try_reserve_exact(chunk_count)
1620            .map_err(|_| failed(BlobReadError::AllocationFailed, self.work))?;
1621        requests
1622            .try_reserve_exact(chunk_count)
1623            .map_err(|_| failed(BlobReadError::AllocationFailed, self.work))?;
1624        for pending in self.pending.iter().rev().take(chunk_count) {
1625            let PendingBlobRead::Chunk(chunk) = pending else {
1626                return Err(failed(BlobReadError::TraversalState, self.work));
1627            };
1628            requests.push(ObjectReadRequest {
1629                object_id: chunk.chunk,
1630                maximum_bytes: chunk.end_offset - chunk.first_offset,
1631            });
1632            chunks.push(*chunk);
1633        }
1634        let mut remaining = prospective
1635            .remaining(self.budget)
1636            .map_err(|error| failed(error.into(), prospective))?;
1637        remaining.peak_allocation_bytes = self
1638            .budget
1639            .peak_allocation_bytes
1640            .checked_sub(live_bytes)
1641            .ok_or_else(|| failed(BlobReadError::Work(WorkError::Overflow), prospective))?;
1642        Ok(Some(PendingChunkBatch {
1643            chunks,
1644            requests,
1645            scratch_bytes,
1646            remaining,
1647            prospective,
1648        }))
1649    }
1650
1651    fn accept_chunk_batch(
1652        &mut self,
1653        batch: PendingChunkBatch,
1654        receipt: ObjectReceipt<Vec<ObjectRead>>,
1655    ) -> Result<(), BlobReadFailure> {
1656        if receipt.value.len() != batch.chunks.len() {
1657            return Err(failed(BlobReadError::TraversalState, batch.prospective));
1658        }
1659        let retained = self
1660            .pending
1661            .len()
1662            .checked_sub(batch.chunks.len())
1663            .ok_or_else(|| failed(BlobReadError::TraversalState, batch.prospective))?;
1664        if self.pending.get(retained..).is_none_or(|pending| {
1665            !pending
1666                .iter()
1667                .rev()
1668                .zip(&batch.chunks)
1669                .all(|(pending, chunk)| matches!(pending, PendingBlobRead::Chunk(found) if found == chunk))
1670        }) {
1671            return Err(failed(BlobReadError::TraversalState, batch.prospective));
1672        }
1673        self.pending.truncate(retained);
1674        self.work = merge_blob_backend_work(
1675            batch.prospective,
1676            receipt.work,
1677            self.allocations.live_bytes(),
1678        )
1679        .map_err(|error| failed(error.into(), batch.prospective))?;
1680        let retained_bytes = receipt.value.iter().try_fold(0_u64, |total, value| {
1681            total.checked_add(match value.retention {
1682                ObjectReadRetention::Shared => 0,
1683                ObjectReadRetention::Owned { logical_bytes } => logical_bytes,
1684            })
1685        });
1686        let retained_bytes = retained_bytes
1687            .ok_or_else(|| failed(BlobReadError::Work(WorkError::Overflow), self.work))?;
1688        let simultaneous = self
1689            .allocations
1690            .live_bytes()
1691            .checked_add(batch.scratch_bytes)
1692            .and_then(|bytes| bytes.checked_add(retained_bytes))
1693            .ok_or_else(|| failed(BlobReadError::Work(WorkError::Overflow), self.work))?;
1694        self.work.peak_allocation_bytes = self.work.peak_allocation_bytes.max(simultaneous);
1695        verify_work(self.work, self.budget)?;
1696        for (chunk, value) in batch.chunks.into_iter().zip(receipt.value) {
1697            self.accept_chunk(
1698                &ObjectReceipt {
1699                    value,
1700                    work: WorkCounters::default(),
1701                },
1702                chunk,
1703            )?;
1704        }
1705        Ok(())
1706    }
1707
1708    fn batch_storage_failure(
1709        &self,
1710        prospective: WorkCounters,
1711        failure: crate::storage::ObjectFailure,
1712    ) -> BlobReadFailure {
1713        match merge_blob_backend_work(prospective, *failure.work, self.allocations.live_bytes()) {
1714            Ok(combined) => failed(failure.error.into(), combined),
1715            Err(error) => failed(error.into(), prospective),
1716        }
1717    }
1718
1719    fn accept_read(
1720        &mut self,
1721        prospective: WorkCounters,
1722        receipt: &ObjectReceipt<ObjectRead>,
1723    ) -> Result<Option<BlobRead>, BlobReadFailure> {
1724        let pending = self
1725            .awaiting
1726            .take()
1727            .ok_or_else(|| failed(BlobReadError::TraversalState, self.work))?;
1728        self.work =
1729            merge_blob_backend_work(prospective, receipt.work, self.allocations.live_bytes())
1730                .map_err(|error| failed(error.into(), prospective))?;
1731        match pending {
1732            PendingBlobRead::Page {
1733                expected_first,
1734                expected_end,
1735                height,
1736                ..
1737            } => self.accept_page(receipt, expected_first, expected_end, height)?,
1738            PendingBlobRead::Chunk(chunk) => self.accept_chunk(receipt, chunk)?,
1739        }
1740        Ok(None)
1741    }
1742
1743    fn accept_read_with_cache<S: AsyncObjectStore>(
1744        &mut self,
1745        store: &S,
1746        key: Option<DecodedCacheKey>,
1747        prospective: WorkCounters,
1748        receipt: &ObjectReceipt<ObjectRead>,
1749    ) -> Result<(), BlobReadFailure> {
1750        let pending = self
1751            .awaiting
1752            .take()
1753            .ok_or_else(|| failed(BlobReadError::TraversalState, self.work))?;
1754        self.work =
1755            merge_blob_backend_work(prospective, receipt.work, self.allocations.live_bytes())
1756                .map_err(|error| failed(error.into(), prospective))?;
1757        match pending {
1758            PendingBlobRead::Page {
1759                expected_first,
1760                expected_end,
1761                height,
1762                ..
1763            } => self.accept_page_with_cache(
1764                store,
1765                key.ok_or_else(|| failed(BlobReadError::TraversalState, self.work))?,
1766                receipt,
1767                expected_first,
1768                expected_end,
1769                height,
1770            ),
1771            PendingBlobRead::Chunk(chunk) => self.accept_chunk(receipt, chunk),
1772        }
1773    }
1774
1775    fn add_items(&mut self, count: u64) -> Result<(), BlobReadFailure> {
1776        self.work = add_work(
1777            self.work,
1778            WorkCounters {
1779                items_examined: count,
1780                ..WorkCounters::default()
1781            },
1782        )?;
1783        verify_work(self.work, self.budget)
1784    }
1785
1786    fn accept_page(
1787        &mut self,
1788        receipt: &ObjectReceipt<ObjectRead>,
1789        expected_first: u64,
1790        expected_end: Option<u64>,
1791        height: u16,
1792    ) -> Result<(), BlobReadFailure> {
1793        let (page, decoded_bytes, retained_bytes) = self.decode_page(receipt)?;
1794        let result = self.accept_decoded_page(&page, expected_first, expected_end, height);
1795        self.allocations
1796            .release(decoded_bytes)
1797            .map_err(|error| failed(error.into(), self.work))?;
1798        self.allocations
1799            .release(retained_bytes)
1800            .map_err(|error| failed(error.into(), self.work))?;
1801        result
1802    }
1803
1804    fn decode_page(
1805        &mut self,
1806        receipt: &ObjectReceipt<ObjectRead>,
1807    ) -> Result<(BlobPage, u64, u64), BlobReadFailure> {
1808        self.work = add_work(
1809            self.work,
1810            WorkCounters {
1811                page_reads: 1,
1812                ..WorkCounters::default()
1813            },
1814        )?;
1815        verify_work(self.work, self.budget)?;
1816        let retained_bytes = match receipt.value.retention {
1817            ObjectReadRetention::Shared => 0,
1818            ObjectReadRetention::Owned { logical_bytes } => logical_bytes,
1819        };
1820        self.allocations
1821            .claim_bytes(retained_bytes, 0, &mut self.work, self.budget)
1822            .map_err(|error| failed(error.into(), self.work))?;
1823        let shape = blob_page_decode_shape(&receipt.value, self.limits)
1824            .map_err(|error| failed(error.into(), self.work))?;
1825        self.add_items(u64::try_from(shape.items).unwrap_or(u64::MAX))?;
1826        let item_bytes = match shape.kind {
1827            DecodedPageKind::Leaf => size_of::<BlobChunkRef>(),
1828            DecodedPageKind::Internal => size_of::<BlobChild>(),
1829        };
1830        let decoded_bytes = shape
1831            .items
1832            .checked_mul(item_bytes)
1833            .map(crate::foundation::usize_to_u64)
1834            .ok_or_else(|| failed(BlobReadError::AllocationFailed, self.work))?;
1835        self.allocations
1836            .claim_bytes(
1837                decoded_bytes,
1838                u64::from(decoded_bytes != 0),
1839                &mut self.work,
1840                self.budget,
1841            )
1842            .map_err(|error| failed(error.into(), self.work))?;
1843        let page = decode_blob_page(&receipt.value, self.limits)
1844            .map_err(|error| failed(BlobReadError::Decode(error), self.work))?;
1845        Ok((page, decoded_bytes, retained_bytes))
1846    }
1847
1848    fn accept_cached_page(&mut self, cached: DecodedCacheValue) -> Result<(), BlobReadFailure> {
1849        let pending = self
1850            .awaiting
1851            .take()
1852            .ok_or_else(|| failed(BlobReadError::TraversalState, self.work))?;
1853        let PendingBlobRead::Page {
1854            expected_first,
1855            expected_end,
1856            height,
1857            ..
1858        } = pending
1859        else {
1860            return Err(failed(BlobReadError::TraversalState, self.work));
1861        };
1862        self.work = add_work(
1863            self.work,
1864            WorkCounters {
1865                page_reads: 1,
1866                ..WorkCounters::default()
1867            },
1868        )?;
1869        verify_work(self.work, self.budget)?;
1870        let page = cached
1871            .value
1872            .downcast::<BlobPage>()
1873            .map_err(|_| failed(ObjectStoreError::Corrupt.into(), self.work))?;
1874        self.accept_decoded_page(&page, expected_first, expected_end, height)
1875    }
1876
1877    fn accept_page_with_cache<S: AsyncObjectStore>(
1878        &mut self,
1879        store: &S,
1880        key: DecodedCacheKey,
1881        receipt: &ObjectReceipt<ObjectRead>,
1882        expected_first: u64,
1883        expected_end: Option<u64>,
1884        height: u16,
1885    ) -> Result<(), BlobReadFailure> {
1886        let (page, decoded_bytes, retained_bytes) = self.decode_page(receipt)?;
1887        let admitted = store.decoded_cache_admit(
1888            key,
1889            DecodedCacheValue {
1890                value: Arc::new(page),
1891                logical_bytes: decoded_bytes,
1892            },
1893        );
1894        let result = match admitted {
1895            Ok(DecodedCacheAdmission::Shared(value)) => {
1896                self.allocations
1897                    .release(decoded_bytes)
1898                    .map_err(|error| failed(error.into(), self.work))?;
1899                let page = value
1900                    .value
1901                    .downcast::<BlobPage>()
1902                    .map_err(|_| failed(ObjectStoreError::Corrupt.into(), self.work))?;
1903                self.accept_decoded_page(&page, expected_first, expected_end, height)
1904            }
1905            Ok(DecodedCacheAdmission::Uncached(value)) => {
1906                let page = value
1907                    .value
1908                    .downcast::<BlobPage>()
1909                    .map_err(|_| failed(ObjectStoreError::Corrupt.into(), self.work))?;
1910                let result = self.accept_decoded_page(&page, expected_first, expected_end, height);
1911                self.allocations
1912                    .release(decoded_bytes)
1913                    .map_err(|error| failed(error.into(), self.work))?;
1914                result
1915            }
1916            Err(error) => {
1917                self.allocations
1918                    .release(decoded_bytes)
1919                    .map_err(|release| failed(release.into(), self.work))?;
1920                Err(failed(error.into(), self.work))
1921            }
1922        };
1923        self.allocations
1924            .release(retained_bytes)
1925            .map_err(|error| failed(error.into(), self.work))?;
1926        result
1927    }
1928
1929    fn accept_decoded_page(
1930        &mut self,
1931        page: &BlobPage,
1932        expected_first: u64,
1933        expected_end: Option<u64>,
1934        height: u16,
1935    ) -> Result<(), BlobReadFailure> {
1936        if page.first_offset != expected_first
1937            || expected_end.is_some_and(|end| page.end_offset != end)
1938        {
1939            return Err(failed(BlobReadError::ChildBoundsMismatch, self.work));
1940        }
1941        if expected_end.is_none() && self.range_end > page.end_offset {
1942            return Err(failed(BlobReadError::InvalidRange, self.work));
1943        }
1944        match &page.node {
1945            BlobNode::Internal(children) => self.push_children(children, height)?,
1946            BlobNode::Leaf(chunks) => self.push_chunks(chunks)?,
1947        }
1948        Ok(())
1949    }
1950
1951    fn push_children(
1952        &mut self,
1953        children: &[BlobChild],
1954        height: u16,
1955    ) -> Result<(), BlobReadFailure> {
1956        let child_height = height
1957            .checked_add(1)
1958            .ok_or_else(|| failed(BlobReadError::CycleOrHeight, self.work))?;
1959        for child in children.iter().rev() {
1960            self.examine_item()?;
1961            if child.first_offset < self.range_end && child.end_offset > self.range.offset {
1962                self.pending_capacity
1963                    .ensure_for_push(
1964                        &mut self.pending,
1965                        self.maximum_pending,
1966                        &mut self.allocations,
1967                        &mut self.work,
1968                        self.budget,
1969                    )
1970                    .map_err(|error| failed(error.into(), self.work))?;
1971                self.pending.push(PendingBlobRead::Page {
1972                    page: child.page,
1973                    expected_first: child.first_offset,
1974                    expected_end: Some(child.end_offset),
1975                    height: child_height,
1976                });
1977            } else if self.range.length != 0 && child.first_offset >= self.range_end {
1978                self.retain_successor(
1979                    child.first_offset,
1980                    ObjectReadRequest {
1981                        object_id: child.page,
1982                        maximum_bytes: self.limits.maximum_page_object_bytes(),
1983                    },
1984                );
1985            }
1986        }
1987        Ok(())
1988    }
1989
1990    fn push_chunks(&mut self, chunks: &[BlobChunkRef]) -> Result<(), BlobReadFailure> {
1991        for chunk in chunks.iter().rev() {
1992            self.examine_item()?;
1993            if chunk.first_offset < self.range_end && chunk.end_offset > self.range.offset {
1994                self.pending_capacity
1995                    .ensure_for_push(
1996                        &mut self.pending,
1997                        self.maximum_pending,
1998                        &mut self.allocations,
1999                        &mut self.work,
2000                        self.budget,
2001                    )
2002                    .map_err(|error| failed(error.into(), self.work))?;
2003                self.pending.push(PendingBlobRead::Chunk(*chunk));
2004            } else if self.range.length != 0 && chunk.first_offset >= self.range_end {
2005                self.retain_successor(
2006                    chunk.first_offset,
2007                    ObjectReadRequest {
2008                        object_id: chunk.chunk,
2009                        maximum_bytes: chunk.end_offset - chunk.first_offset,
2010                    },
2011                );
2012            }
2013        }
2014        Ok(())
2015    }
2016
2017    fn retain_successor(&mut self, offset: u64, request: ObjectReadRequest) {
2018        if self
2019            .successor
2020            .is_none_or(|(current_offset, _)| offset < current_offset)
2021        {
2022            self.successor = Some((
2023                offset,
2024                ResidencyHint {
2025                    request,
2026                    reason: ResidencyReason::SequentialRange,
2027                },
2028            ));
2029        }
2030    }
2031
2032    fn examine_item(&mut self) -> Result<(), BlobReadFailure> {
2033        self.work = add_work(
2034            self.work,
2035            WorkCounters {
2036                items_examined: 1,
2037                ..WorkCounters::default()
2038            },
2039        )?;
2040        verify_work(self.work, self.budget)
2041    }
2042
2043    fn accept_chunk(
2044        &mut self,
2045        receipt: &ObjectReceipt<ObjectRead>,
2046        chunk: BlobChunkRef,
2047    ) -> Result<(), BlobReadFailure> {
2048        let retained_bytes = match receipt.value.retention {
2049            ObjectReadRetention::Shared => 0,
2050            ObjectReadRetention::Owned { logical_bytes } => logical_bytes,
2051        };
2052        self.allocations
2053            .claim_bytes(retained_bytes, 0, &mut self.work, self.budget)
2054            .map_err(|error| failed(error.into(), self.work))?;
2055        let chunk_length = chunk.end_offset - chunk.first_offset;
2056        if u64::try_from(receipt.value.len()).unwrap_or(u64::MAX) != chunk_length {
2057            return Err(failed(BlobReadError::ChunkLengthMismatch, self.work));
2058        }
2059        let start = self.range.offset.max(chunk.first_offset) - chunk.first_offset;
2060        let end = self.range_end.min(chunk.end_offset) - chunk.first_offset;
2061        let start =
2062            usize::try_from(start).map_err(|_| failed(BlobReadError::InvalidRange, self.work))?;
2063        let end =
2064            usize::try_from(end).map_err(|_| failed(BlobReadError::InvalidRange, self.work))?;
2065        #[allow(
2066            clippy::indexing_slicing,
2067            reason = "start <= end <= receipt.value.len() by construction: `BlobPage::validate` \
2068                      (run by `decode_blob_page` before any chunk reaches this machine) requires \
2069                      `contiguous()` to hold, so `chunk.first_offset < chunk.end_offset`; only \
2070                      chunks with `chunk.first_offset < self.range_end && chunk.end_offset > \
2071                      self.range.offset` are ever queued (see the frontier filter above); \
2072                      `self.range_end` is `range.offset + range.length` via `checked_add`, so \
2073                      `range.offset <= self.range_end`; together these bound \
2074                      `max(range.offset, chunk.first_offset) <= min(range_end, \
2075                      chunk.end_offset)`, i.e. start <= end; and the length check just above \
2076                      establishes `end <= chunk_length == receipt.value.len()`"
2077        )]
2078        self.output.extend_from_slice(&receipt.value[start..end]);
2079        self.work = add_work(
2080            self.work,
2081            WorkCounters {
2082                bytes_copied: u64::try_from(end - start).unwrap_or(u64::MAX),
2083                output_bytes: u64::try_from(end - start).unwrap_or(u64::MAX),
2084                items_returned: 1,
2085                ..WorkCounters::default()
2086            },
2087        )?;
2088        verify_work(self.work, self.budget)?;
2089        self.allocations
2090            .release(retained_bytes)
2091            .map_err(|error| failed(error.into(), self.work))
2092    }
2093}
2094
2095impl frontier::Machine for BlobRangeMachine {
2096    type Output = BlobRead;
2097    type Failure = BlobReadFailure;
2098
2099    fn complete(&mut self) -> Result<Option<Self::Output>, Self::Failure> {
2100        if self.pending.is_empty() && self.awaiting.is_none() {
2101            return self.finish().map(Some);
2102        }
2103        Ok(None)
2104    }
2105
2106    fn prepare_read(&mut self) -> Result<frontier::ReadRequest, Self::Failure> {
2107        BlobRangeMachine::prepare_read(self)
2108    }
2109
2110    fn accept(
2111        &mut self,
2112        prospective: WorkCounters,
2113        receipt: &ObjectReceipt<ObjectRead>,
2114    ) -> Result<Option<Self::Output>, Self::Failure> {
2115        self.accept_read(prospective, receipt)
2116    }
2117
2118    fn storage_failure(
2119        &self,
2120        prospective: WorkCounters,
2121        failure: crate::storage::ObjectFailure,
2122    ) -> Self::Failure {
2123        match merge_blob_backend_work(prospective, *failure.work, self.allocations.live_bytes()) {
2124            Ok(combined) => failed(failure.error.into(), combined),
2125            Err(error) => failed(error.into(), prospective),
2126        }
2127    }
2128
2129    fn cancelled(&self) -> Self::Failure {
2130        failed(BlobReadError::Cancelled, self.work)
2131    }
2132}
2133
2134fn add_work(left: WorkCounters, right: WorkCounters) -> Result<WorkCounters, BlobReadFailure> {
2135    left.checked_add(right)
2136        .map_err(|error| failed(BlobReadError::Work(error), left))
2137}
2138
2139fn verify_work(work: WorkCounters, budget: WorkBudget) -> Result<(), BlobReadFailure> {
2140    work.verify(budget)
2141        .map_err(|error| failed(BlobReadError::Work(error), work))
2142}
2143
2144fn merge_blob_backend_work(
2145    prior: WorkCounters,
2146    mut backend: WorkCounters,
2147    live_bytes: u64,
2148) -> Result<WorkCounters, WorkError> {
2149    let simultaneous_peak = live_bytes
2150        .checked_add(backend.peak_allocation_bytes)
2151        .ok_or(WorkError::Overflow)?;
2152    backend.peak_allocation_bytes = 0;
2153    let mut merged = prior.checked_add(backend)?;
2154    merged.peak_allocation_bytes = merged.peak_allocation_bytes.max(simultaneous_peak);
2155    Ok(merged)
2156}
2157
2158/// Authenticated blob-read failure with exact work retained on every path.
2159pub type BlobReadFailure = OperationFailure<BlobReadError>;
2160
2161fn failed(error: BlobReadError, work: WorkCounters) -> BlobReadFailure {
2162    OperationFailure::new(error, work)
2163}
2164
2165/// Stable authenticated blob-read errors.
2166#[derive(Debug, Error)]
2167pub enum BlobReadError {
2168    /// Cooperative cancellation occurred before the next storage boundary.
2169    #[error("blob range reading was cancelled")]
2170    Cancelled,
2171    /// The private resumable reader entered an impossible transition.
2172    #[error("blob range transition state is invalid")]
2173    TraversalState,
2174    /// Root has the wrong typed object class.
2175    #[error("blob root is not a blob-index page")]
2176    WrongRootKind,
2177    /// Requested range overflows or exceeds the blob.
2178    #[error("requested blob range is invalid")]
2179    InvalidRange,
2180    /// Decode and traversal limits are invalid or unrepresentable.
2181    #[error("blob read limits are invalid")]
2182    InvalidLimits,
2183    /// Bounded output, decode, or traversal scratch allocation failed.
2184    #[error("blob read scratch allocation failed")]
2185    AllocationFailed,
2186    /// Graph repeats a page or exceeds its height bound.
2187    #[error("blob index contains a cycle or exceeds its height bound")]
2188    CycleOrHeight,
2189    /// Parent routing bounds do not match the authenticated child.
2190    #[error("blob child bounds do not match its page")]
2191    ChildBoundsMismatch,
2192    /// Intersecting chunks did not cover the exact request.
2193    #[error("blob chunks do not cover the requested range exactly")]
2194    IncompleteCoverage,
2195    /// Authenticated chunk bytes do not equal the indexed logical span.
2196    #[error("blob chunk length does not match its index span")]
2197    ChunkLengthMismatch,
2198    /// Immutable-object backend rejected a read.
2199    #[error(transparent)]
2200    Storage(#[from] ObjectStoreError),
2201    /// Canonical page failed decoding.
2202    #[error(transparent)]
2203    Decode(#[from] CanonicalDecodeError),
2204    /// Exact work exceeded or overflowed its budget.
2205    #[error(transparent)]
2206    Work(#[from] WorkError),
2207}
2208
2209impl From<AllocationError> for BlobReadError {
2210    fn from(error: AllocationError) -> Self {
2211        match error {
2212            AllocationError::Work(error) => Self::Work(error),
2213            AllocationError::Overflow | AllocationError::ReleaseInvariant => {
2214                Self::Work(WorkError::Overflow)
2215            }
2216            AllocationError::InvalidCapacity
2217            | AllocationError::CapacityExceeded
2218            | AllocationError::AllocationFailed => Self::AllocationFailed,
2219        }
2220    }
2221}
2222
2223fn count(value: usize) -> Result<u32, CanonicalDecodeError> {
2224    u32::try_from(value).map_err(|_| CanonicalDecodeError::LengthOverflow)
2225}
2226
2227fn capacity(value: u32) -> Result<usize, CanonicalDecodeError> {
2228    usize::try_from(value).map_err(|_| CanonicalDecodeError::LengthOverflow)
2229}
2230
2231fn invariant(message: &str) -> CanonicalDecodeError {
2232    CanonicalDecodeError::Invariant(message.to_owned())
2233}
2234
2235#[cfg(all(test, feature = "memory"))]
2236#[path = "tests/blob.rs"]
2237mod tests;