Skip to main content

acyclic_fs/kernel/
extent_mutation.rs

1//! Sparse persistent extent mutation without file-body materialization.
2
3use super::allocation::{AllocationError, AllocationLedger, VisitedObjectSet};
4use super::codec::DecodedPageKind;
5use super::extent::{extent_page_decode_shape, extent_page_encoded_length};
6use super::{
7    CanonicalDecodeError, DecodeLimits, Extent, ExtentChild, ExtentKind, ExtentPage,
8    decode_extent_page, encode_extent_page,
9};
10use crate::cancellation::CancellationToken;
11use crate::performance::{OperationFailure, WorkBudget, WorkCounters, WorkError};
12use crate::storage::{
13    OBJECT_DIGEST_ENVELOPE_BYTES, ObjectId, ObjectKind, ObjectReadRetention, ObjectStoreError,
14    object_digest,
15};
16use bytes::Bytes;
17use std::mem::size_of;
18use thiserror::Error;
19
20/// One ordered logical-file extent mutation.
21#[derive(Clone, Debug, Eq, PartialEq)]
22pub enum ExtentMutation {
23    /// Replaces one byte range with content, a hole, or allocated zeros.
24    Replace {
25        /// Inclusive logical destination offset.
26        offset: u64,
27        /// Positive byte count.
28        length: u64,
29        /// Replacement representation. Content offset corresponds to `offset`.
30        kind: ExtentKind,
31        /// Whether this operation may extend logical file length.
32        extend: bool,
33    },
34    /// Truncates or extends; growth is always represented by holes.
35    Resize {
36        /// New exact logical byte length.
37        logical_bytes: u64,
38    },
39}
40
41#[derive(Clone, Debug)]
42struct Patch {
43    offset: u64,
44    end: u64,
45    kind: ExtentKind,
46}
47
48struct CompiledPatchPlan {
49    patches: Vec<Patch>,
50    final_size: u64,
51    work: WorkCounters,
52    live_allocation_bytes: u64,
53}
54
55#[derive(Clone, Debug)]
56struct Summary {
57    first: u64,
58    end: u64,
59    page: ObjectId,
60}
61
62struct NodeRequest {
63    page: ObjectId,
64    original_first: u64,
65    original_end: u64,
66    output_end: u64,
67    height: u16,
68}
69
70struct InternalFrame {
71    original: ObjectId,
72    children: Vec<ExtentChild>,
73    original_first: u64,
74    original_end: u64,
75    output_end: u64,
76    height: u16,
77    next_child: usize,
78    rewritten: Vec<Summary>,
79    logical_bytes: u64,
80}
81
82enum EnteredNode {
83    Complete(Vec<Summary>),
84    Internal(InternalFrame),
85}
86
87struct OwnedExtentPage {
88    page: ExtentPage,
89    logical_bytes: u64,
90}
91
92/// New extent root, logical size, and exact path-copy evidence.
93#[derive(Clone, Debug, Eq, PartialEq)]
94pub struct ExtentMutationReceipt {
95    /// Candidate immutable extent-tree root.
96    pub root: ObjectId,
97    /// Final exact logical file length.
98    pub logical_bytes: u64,
99    /// Exact page, encoding, hashing, and backend work.
100    pub work: WorkCounters,
101}
102
103/// Admission bounds shared by asynchronous extent mutation operations.
104#[derive(Clone, Copy, Debug, Eq, PartialEq)]
105pub struct ExtentMutationOptions {
106    /// Maximum accepted mutations in this ordered batch.
107    pub maximum_mutations: u32,
108    /// Canonical decode and authenticated-tree bounds.
109    pub limits: DecodeLimits,
110    /// Exact physical work ceiling.
111    pub budget: WorkBudget,
112}
113
114/// Extent mutation failure retaining spent work and safe orphan writes.
115pub type ExtentMutationFailure = OperationFailure<ExtentMutationError>;
116
117struct Context<'a, S> {
118    store: &'a S,
119    limits: DecodeLimits,
120    budget: WorkBudget,
121    work: WorkCounters,
122    allocations: AllocationLedger,
123    visited: VisitedObjectSet,
124    patches: Vec<Patch>,
125    maximum_seen_height: u16,
126    cancellation: &'a CancellationToken,
127}
128
129/// Applies ordered extent mutations by rewriting only intersecting frontiers.
130///
131/// Shrink operations discard whole later subtrees from parent bounds without
132/// reading them. Growth follows only the final frontier and appends holes.
133/// Content blobs are referenced, never read or materialized. Equivalent
134/// adjacent spans are coalesced after ordered patch replay.
135///
136/// # Errors
137///
138/// Rejects malformed/excessive ranges and unsupported extension before backend
139/// access, then fails closed on routing corruption, cycles, storage failure, or
140/// work outside the admitted budget.
141pub fn apply_extent_mutations<S: crate::ImmediateObjectStore>(
142    store: &S,
143    root: ObjectId,
144    old_logical_bytes: u64,
145    mutations: &[ExtentMutation],
146    maximum_mutations: u32,
147    limits: DecodeLimits,
148    budget: WorkBudget,
149) -> Result<ExtentMutationReceipt, ExtentMutationFailure> {
150    let cancellation = CancellationToken::new();
151    crate::async_storage::poll_immediate(apply_extent_mutations_async(
152        store,
153        root,
154        old_logical_bytes,
155        mutations,
156        ExtentMutationOptions {
157            maximum_mutations,
158            limits,
159            budget,
160        },
161        &cancellation,
162    ))
163}
164
165/// Asynchronously applies ordered extent mutations without blocking an executor.
166///
167/// The sync and async APIs execute this same iterative path-copy state machine.
168/// Cancellation is checked by every backend operation and cannot publish a
169/// mutable object because all writes are immutable and content addressed.
170///
171/// # Errors
172///
173/// Returns the same bounded, receipt-bearing failures as
174/// [`apply_extent_mutations`], plus backend cancellation.
175pub async fn apply_extent_mutations_async<S: crate::AsyncObjectStore>(
176    store: &S,
177    root: ObjectId,
178    old_logical_bytes: u64,
179    mutations: &[ExtentMutation],
180    options: ExtentMutationOptions,
181    cancellation: &CancellationToken,
182) -> Result<ExtentMutationReceipt, ExtentMutationFailure> {
183    cancellation.check().map_err(|_| {
184        OperationFailure::before_work(ExtentMutationError::Storage(ObjectStoreError::Cancelled))
185    })?;
186    let ExtentMutationOptions {
187        maximum_mutations,
188        limits,
189        budget,
190    } = options;
191    validate_request(root, mutations, maximum_mutations, limits)?;
192    let plan = compile_patch_plan(old_logical_bytes, mutations, budget)?;
193    let final_size = plan.final_size;
194    let mut allocations = AllocationLedger::default();
195    let mut work = plan.work;
196    allocations
197        .claim_bytes(plan.live_allocation_bytes, 0, &mut work, budget)
198        .map_err(|error| OperationFailure::new(error.into(), work))?;
199    let maximum_visited = usize::try_from(limits.maximum_visited_pages)
200        .map_err(|_| OperationFailure::new(ExtentMutationError::InvalidLimits, work))?;
201    let visited = VisitedObjectSet::new(maximum_visited, &mut allocations, &mut work, budget)
202        .map_err(|error| OperationFailure::new(error.into(), work))?;
203    let mut context = Context {
204        store,
205        limits,
206        budget,
207        work,
208        allocations,
209        visited,
210        patches: plan.patches,
211        maximum_seen_height: 0,
212        cancellation,
213    };
214    let summaries = context
215        .rewrite(root, 0, old_logical_bytes, final_size)
216        .await
217        .map_err(|error| OperationFailure::new(error, context.work))?;
218    let new_root = context
219        .finish_root(summaries)
220        .await
221        .map_err(|error| OperationFailure::new(error, context.work))?;
222    context
223        .visited
224        .release(&mut context.allocations)
225        .map_err(|error| OperationFailure::new(error.into(), context.work))?;
226    context
227        .allocations
228        .release(plan.live_allocation_bytes)
229        .map_err(|error| OperationFailure::new(error.into(), context.work))?;
230    Ok(ExtentMutationReceipt {
231        root: new_root,
232        logical_bytes: final_size,
233        work: context.work,
234    })
235}
236
237fn validate_request(
238    root: ObjectId,
239    mutations: &[ExtentMutation],
240    maximum_mutations: u32,
241    limits: DecodeLimits,
242) -> Result<(), ExtentMutationFailure> {
243    let error = if root.kind != ObjectKind::ExtentPage {
244        Some(ExtentMutationError::WrongRootKind)
245    } else if mutations.is_empty() {
246        Some(ExtentMutationError::Empty)
247    } else if maximum_mutations == 0
248        || usize::try_from(maximum_mutations).map_or(true, |maximum| mutations.len() > maximum)
249    {
250        Some(ExtentMutationError::TooManyMutations)
251    } else if !limits.page_limits_valid(2) {
252        Some(ExtentMutationError::InvalidLimits)
253    } else {
254        None
255    };
256    match error {
257        Some(error) => Err(OperationFailure::before_work(error)),
258        None => Ok(()),
259    }
260}
261
262fn compile_patch_plan(
263    old_size: u64,
264    mutations: &[ExtentMutation],
265    budget: WorkBudget,
266) -> Result<CompiledPatchPlan, ExtentMutationFailure> {
267    let (final_size, raw_count, mut work) = admit_patch_plan(old_size, mutations, budget)?;
268    if raw_count == 0 {
269        return Ok(CompiledPatchPlan {
270            patches: Vec::new(),
271            final_size,
272            work,
273            live_allocation_bytes: 0,
274        });
275    }
276    reserve_compile_scratch(raw_count, work, budget)?;
277    let mut live_bytes = 0_u64;
278    let mut raw = allocate_vec::<Patch>(raw_count, &mut work, budget, &mut live_bytes)?;
279    let mut size = old_size;
280    for mutation in mutations {
281        charge_items(&mut work, budget, 1)?;
282        match mutation {
283            ExtentMutation::Replace {
284                offset,
285                length,
286                kind,
287                extend: _,
288            } => {
289                let end = offset.checked_add(*length).ok_or_else(|| {
290                    OperationFailure::new(ExtentMutationError::RangeOverflow, work)
291                })?;
292                if *offset > size {
293                    raw.push(Patch {
294                        offset: size,
295                        end: *offset,
296                        kind: ExtentKind::Hole,
297                    });
298                }
299                raw.push(Patch {
300                    offset: *offset,
301                    end,
302                    kind: kind.clone(),
303                });
304                size = size.max(end);
305            }
306            ExtentMutation::Resize { logical_bytes } => {
307                if *logical_bytes != size {
308                    let start = size.min(*logical_bytes);
309                    let end = size.max(*logical_bytes);
310                    raw.push(Patch {
311                        offset: start,
312                        end,
313                        kind: ExtentKind::Hole,
314                    });
315                    size = *logical_bytes;
316                }
317            }
318        }
319    }
320    let patches = normalize_raw_patches(&raw, final_size, &mut work, budget, &mut live_bytes)?;
321    let live_allocation_bytes = bytes_for::<Patch>(patches.capacity(), work)?;
322    Ok(CompiledPatchPlan {
323        patches,
324        final_size,
325        work,
326        live_allocation_bytes,
327    })
328}
329
330fn admit_patch_plan(
331    old_size: u64,
332    mutations: &[ExtentMutation],
333    budget: WorkBudget,
334) -> Result<(u64, usize, WorkCounters), ExtentMutationFailure> {
335    let mut size = old_size;
336    let mut raw_count = 0_usize;
337    let mut work = WorkCounters::default();
338    for mutation in mutations {
339        charge_items(&mut work, budget, 1)?;
340        match mutation {
341            ExtentMutation::Replace {
342                offset,
343                length,
344                kind,
345                extend,
346            } => {
347                if *length == 0 {
348                    return Err(OperationFailure::new(ExtentMutationError::EmptyRange, work));
349                }
350                validate_kind(kind, *length).map_err(|error| OperationFailure::new(error, work))?;
351                let end = offset.checked_add(*length).ok_or_else(|| {
352                    OperationFailure::new(ExtentMutationError::RangeOverflow, work)
353                })?;
354                if end > size && !extend {
355                    return Err(OperationFailure::new(
356                        ExtentMutationError::OutsideFile,
357                        work,
358                    ));
359                }
360                raw_count = raw_count
361                    .checked_add(usize::from(*offset > size) + 1)
362                    .ok_or_else(|| {
363                        OperationFailure::new(ExtentMutationError::RangeOverflow, work)
364                    })?;
365                size = size.max(end);
366            }
367            ExtentMutation::Resize { logical_bytes } => {
368                raw_count = raw_count
369                    .checked_add(usize::from(*logical_bytes != size))
370                    .ok_or_else(|| {
371                        OperationFailure::new(ExtentMutationError::RangeOverflow, work)
372                    })?;
373                size = *logical_bytes;
374            }
375        }
376    }
377    Ok((size, raw_count, work))
378}
379
380fn reserve_compile_scratch(
381    raw_count: usize,
382    work: WorkCounters,
383    budget: WorkBudget,
384) -> Result<(), ExtentMutationFailure> {
385    let coordinates = raw_count
386        .checked_mul(2)
387        .ok_or_else(|| OperationFailure::new(ExtentMutationError::RangeOverflow, work))?;
388    let intervals = coordinates.saturating_sub(1);
389    let capacities = [
390        bytes_for::<Patch>(raw_count, work)?,
391        bytes_for::<u64>(coordinates, work)?,
392        bytes_for::<u64>(coordinates, work)?,
393        bytes_for::<Option<usize>>(intervals, work)?,
394        bytes_for::<usize>(intervals + 1, work)?,
395        bytes_for::<Patch>(intervals, work)?,
396    ];
397    let bytes = capacities.into_iter().try_fold(0_u64, |total, bytes| {
398        total
399            .checked_add(bytes)
400            .ok_or_else(|| OperationFailure::new(ExtentMutationError::RangeOverflow, work))
401    })?;
402    let mut reserved = work
403        .checked_add(WorkCounters {
404            allocation_operations: 6,
405            ..WorkCounters::default()
406        })
407        .map_err(|error| OperationFailure::new(error.into(), work))?;
408    reserved.peak_allocation_bytes = reserved.peak_allocation_bytes.max(bytes);
409    reserved
410        .verify(budget)
411        .map_err(|error| OperationFailure::new(error.into(), work))
412}
413
414fn bytes_for<T>(count: usize, work: WorkCounters) -> Result<u64, ExtentMutationFailure> {
415    count
416        .checked_mul(size_of::<T>())
417        .map(crate::foundation::usize_to_u64)
418        .ok_or_else(|| OperationFailure::new(ExtentMutationError::RangeOverflow, work))
419}
420
421fn allocate_vec<T>(
422    capacity: usize,
423    work: &mut WorkCounters,
424    budget: WorkBudget,
425    live_bytes: &mut u64,
426) -> Result<Vec<T>, ExtentMutationFailure> {
427    let requested = bytes_for::<T>(capacity, *work)?;
428    let next_live = live_bytes
429        .checked_add(requested)
430        .ok_or_else(|| OperationFailure::new(ExtentMutationError::RangeOverflow, *work))?;
431    let mut prospective = work
432        .checked_add(WorkCounters {
433            allocation_operations: u64::from(capacity != 0),
434            ..WorkCounters::default()
435        })
436        .map_err(|error| OperationFailure::new(error.into(), *work))?;
437    prospective.peak_allocation_bytes = prospective.peak_allocation_bytes.max(next_live);
438    prospective
439        .verify(budget)
440        .map_err(|error| OperationFailure::new(error.into(), *work))?;
441    let mut values = Vec::new();
442    values
443        .try_reserve_exact(capacity)
444        .map_err(|_| OperationFailure::new(ExtentMutationError::AllocationFailed, prospective))?;
445    let actual = bytes_for::<T>(values.capacity(), prospective)?;
446    *live_bytes = live_bytes
447        .checked_add(actual)
448        .ok_or_else(|| OperationFailure::new(ExtentMutationError::RangeOverflow, prospective))?;
449    prospective.peak_allocation_bytes = prospective.peak_allocation_bytes.max(*live_bytes);
450    prospective
451        .verify(budget)
452        .map_err(|error| OperationFailure::new(error.into(), prospective))?;
453    *work = prospective;
454    Ok(values)
455}
456
457fn charge_items(
458    work: &mut WorkCounters,
459    budget: WorkBudget,
460    count: u64,
461) -> Result<(), ExtentMutationFailure> {
462    let prospective = work
463        .checked_add(WorkCounters {
464            items_examined: count,
465            ..WorkCounters::default()
466        })
467        .map_err(|error| OperationFailure::new(error.into(), *work))?;
468    prospective
469        .verify(budget)
470        .map_err(|error| OperationFailure::new(error.into(), *work))?;
471    *work = prospective;
472    Ok(())
473}
474
475fn charge_copied_bytes(
476    work: &mut WorkCounters,
477    budget: WorkBudget,
478    bytes: u64,
479) -> Result<(), ExtentMutationFailure> {
480    let prospective = work
481        .checked_add(WorkCounters {
482            bytes_copied: bytes,
483            ..WorkCounters::default()
484        })
485        .map_err(|error| OperationFailure::new(error.into(), *work))?;
486    prospective
487        .verify(budget)
488        .map_err(|error| OperationFailure::new(error.into(), *work))?;
489    *work = prospective;
490    Ok(())
491}
492
493fn normalize_raw_patches(
494    raw: &[Patch],
495    final_size: u64,
496    work: &mut WorkCounters,
497    budget: WorkBudget,
498    live_bytes: &mut u64,
499) -> Result<Vec<Patch>, ExtentMutationFailure> {
500    let coordinate_capacity = raw
501        .len()
502        .checked_mul(2)
503        .ok_or_else(|| OperationFailure::new(ExtentMutationError::RangeOverflow, *work))?;
504    let mut coordinates = allocate_vec::<u64>(coordinate_capacity, work, budget, live_bytes)?;
505    for patch in raw {
506        charge_items(work, budget, 1)?;
507        let start = patch.offset.min(final_size);
508        let end = patch.end.min(final_size);
509        if start < end {
510            coordinates.push(start);
511            coordinates.push(end);
512        }
513    }
514    if coordinates.is_empty() {
515        return Ok(Vec::new());
516    }
517    let mut scratch = allocate_vec::<u64>(coordinates.len(), work, budget, live_bytes)?;
518    radix_sort(&mut coordinates, &mut scratch, work, budget)?;
519    deduplicate_coordinates(&mut coordinates, work, budget)?;
520    let interval_count = coordinates.len().saturating_sub(1);
521    let mut assignments = allocate_vec::<Option<usize>>(interval_count, work, budget, live_bytes)?;
522    charge_items(
523        work,
524        budget,
525        u64::try_from(interval_count).unwrap_or(u64::MAX),
526    )?;
527    assignments.resize(interval_count, None);
528    let mut parents = allocate_vec::<usize>(interval_count + 1, work, budget, live_bytes)?;
529    charge_items(
530        work,
531        budget,
532        u64::try_from(interval_count + 1).unwrap_or(u64::MAX),
533    )?;
534    parents.extend(0..=interval_count);
535    for (patch_index, patch) in raw.iter().enumerate().rev() {
536        charge_items(work, budget, 1)?;
537        let start = patch.offset.min(final_size);
538        let end = patch.end.min(final_size);
539        if start >= end {
540            continue;
541        }
542        let left = coordinate_index(&coordinates, start, work, budget)?;
543        let right = coordinate_index(&coordinates, end, work, budget)?;
544        let mut interval = find_next(&mut parents, left, work, budget)?;
545        #[allow(
546            clippy::indexing_slicing,
547            reason = "coordinate_index's binary search only ever returns an index < \
548                      coordinates.len(), and coordinates.len() == interval_count + 1, so `right` \
549                      is at most interval_count; the loop guard `interval < right` therefore \
550                      keeps `interval < interval_count`, which is in bounds for \
551                      `assignments` (len interval_count) and `parents` (len interval_count + 1)"
552        )]
553        while interval < right {
554            charge_items(work, budget, 1)?;
555            assignments[interval] = Some(patch_index);
556            let successor = find_next(&mut parents, interval + 1, work, budget)?;
557            parents[interval] = successor;
558            interval = successor;
559        }
560    }
561    let mut result = allocate_vec::<Patch>(interval_count, work, budget, live_bytes)?;
562    for (interval, assignment) in assignments.into_iter().enumerate() {
563        charge_items(work, budget, 1)?;
564        let Some(patch_index) = assignment else {
565            continue;
566        };
567        #[allow(
568            clippy::indexing_slicing,
569            reason = "patch_index came from raw.iter().enumerate() above, so it is always a \
570                      valid index into raw"
571        )]
572        let source = &raw[patch_index];
573        #[allow(
574            clippy::indexing_slicing,
575            reason = "interval is a valid index into assignments (len interval_count == \
576                      coordinates.len() - 1) via .enumerate() on assignments, so interval and \
577                      interval + 1 both stay within coordinates bounds"
578        )]
579        let candidate = Patch {
580            offset: coordinates[interval],
581            end: coordinates[interval + 1],
582            kind: offset_kind(&source.kind, coordinates[interval] - source.offset)
583                .map_err(|error| OperationFailure::new(error, *work))?,
584        };
585        if let Some(previous) = result.last_mut()
586            && patch_continues(previous, &candidate)
587                .map_err(|error| OperationFailure::new(error, *work))?
588        {
589            previous.end = candidate.end;
590        } else {
591            result.push(candidate);
592        }
593    }
594    Ok(result)
595}
596
597fn radix_sort(
598    values: &mut Vec<u64>,
599    scratch: &mut Vec<u64>,
600    work: &mut WorkCounters,
601    budget: WorkBudget,
602) -> Result<(), ExtentMutationFailure> {
603    scratch.resize(values.len(), 0);
604    for byte in 0..8_u32 {
605        charge_items(
606            work,
607            budget,
608            u64::try_from(values.len())
609                .unwrap_or(u64::MAX)
610                .saturating_mul(2),
611        )?;
612        charge_copied_bytes(work, budget, bytes_for::<u64>(values.len(), *work)?)?;
613        let shift = byte * 8;
614        let mut counts = [0_usize; 256];
615        #[allow(
616            clippy::indexing_slicing,
617            reason = "the index is `usize::from(<u8>)`, always in 0..=255, matching the fixed \
618                      256-element `counts` array exactly"
619        )]
620        for value in values.iter() {
621            counts[usize::from(((value >> shift) & 0xff) as u8)] += 1;
622        }
623        let mut offset = 0_usize;
624        for count in &mut counts {
625            let current = *count;
626            *count = offset;
627            offset += current;
628        }
629        #[allow(
630            clippy::indexing_slicing,
631            reason = "`bucket` is `usize::from(<u8>)`, always in 0..=255, so `counts[bucket]` is \
632                      in bounds of the fixed 256-element array; `counts[bucket]` itself is a \
633                      running offset seeded by the exclusive prefix sum computed just above from \
634                      a histogram over exactly `values.len()` elements, incremented once per \
635                      element placed, so it never reaches `values.len()` and stays in bounds of \
636                      `scratch` (resized to `values.len()` at the top of this function)"
637        )]
638        for value in values.iter() {
639            let bucket = usize::from(((value >> shift) & 0xff) as u8);
640            scratch[counts[bucket]] = *value;
641            counts[bucket] += 1;
642        }
643        std::mem::swap(values, scratch);
644    }
645    Ok(())
646}
647
648fn deduplicate_coordinates(
649    values: &mut Vec<u64>,
650    work: &mut WorkCounters,
651    budget: WorkBudget,
652) -> Result<(), ExtentMutationFailure> {
653    charge_items(
654        work,
655        budget,
656        u64::try_from(values.len()).unwrap_or(u64::MAX),
657    )?;
658    let mut write = 1_usize;
659    #[allow(
660        clippy::indexing_slicing,
661        reason = "`read` ranges over `1..values.len()` so `values[read]` is always in bounds; \
662                  `write` starts equal to `read` (both 1) and only increments together with \
663                  `read` (once per loop iteration, in this same branch), so `write <= read` is a \
664                  loop invariant, keeping `write - 1` and `write` in bounds too"
665    )]
666    for read in 1..values.len() {
667        if values[read] != values[write - 1] {
668            values[write] = values[read];
669            write += 1;
670        }
671    }
672    values.truncate(write);
673    Ok(())
674}
675
676fn coordinate_index(
677    values: &[u64],
678    target: u64,
679    work: &mut WorkCounters,
680    budget: WorkBudget,
681) -> Result<usize, ExtentMutationFailure> {
682    let mut left = 0_usize;
683    let mut right = values.len();
684    while left < right {
685        charge_items(work, budget, 1)?;
686        let middle = left + (right - left) / 2;
687        #[allow(
688            clippy::indexing_slicing,
689            reason = "binary-search invariant: the loop guard `left < right` with `right` \
690                      initialized to values.len() and only ever narrowed to `middle` keeps \
691                      `middle = left + (right - left) / 2` strictly within `[left, right)`, so \
692                      `middle < values.len()` always holds here"
693        )]
694        let ordering = values[middle].cmp(&target);
695        match ordering {
696            std::cmp::Ordering::Less => left = middle + 1,
697            std::cmp::Ordering::Greater => right = middle,
698            std::cmp::Ordering::Equal => return Ok(middle),
699        }
700    }
701    Err(OperationFailure::new(
702        ExtentMutationError::PatchInvariant,
703        *work,
704    ))
705}
706
707#[allow(
708    clippy::indexing_slicing,
709    reason = "union-find domain invariant: `parents` is initialized in normalize_raw_patches as \
710              the identity permutation over `0..=interval_count` (`parents.extend(0..=interval_count)`), \
711              and both call sites there pass a `start` already shown to be `<= interval_count` \
712              (from coordinate_index's binary-search postcondition, or from `interval < right <= \
713              interval_count`). Every write to a `parents` slot (here, and in \
714              normalize_raw_patches) only ever stores a value returned by `find_next`, which by \
715              induction is always an existing valid index reached by following the chain from a \
716              valid `start`. So `root`, `current`, and every `parents[..]` entry stay within \
717              `0..parents.len()` for the lifetime of the vector"
718)]
719fn find_next(
720    parents: &mut [usize],
721    start: usize,
722    work: &mut WorkCounters,
723    budget: WorkBudget,
724) -> Result<usize, ExtentMutationFailure> {
725    let mut root = start;
726    while parents[root] != root {
727        charge_items(work, budget, 1)?;
728        root = parents[root];
729    }
730    let mut current = start;
731    while parents[current] != current {
732        charge_items(work, budget, 1)?;
733        let next = parents[current];
734        parents[current] = root;
735        current = next;
736    }
737    Ok(root)
738}
739
740fn patch_continues(left: &Patch, right: &Patch) -> Result<bool, ExtentMutationError> {
741    if left.end != right.offset {
742        return Ok(false);
743    }
744    let left_extent = Extent {
745        offset: left.offset,
746        length: left.end - left.offset,
747        kind: left.kind.clone(),
748    };
749    let right_extent = Extent {
750        offset: right.offset,
751        length: right.end - right.offset,
752        kind: right.kind.clone(),
753    };
754    equivalent_continuation(&left_extent, &right_extent)
755}
756
757fn validate_kind(kind: &ExtentKind, length: u64) -> Result<(), ExtentMutationError> {
758    if let ExtentKind::Content {
759        object,
760        object_offset,
761    } = kind
762    {
763        if object.kind != ObjectKind::Blob {
764            return Err(ExtentMutationError::WrongContentKind);
765        }
766        object_offset
767            .checked_add(length)
768            .ok_or(ExtentMutationError::RangeOverflow)?;
769    }
770    Ok(())
771}
772
773fn extent_leaf_chunk_end(
774    extents: &[Extent],
775    start: usize,
776    limits: DecodeLimits,
777) -> Result<(usize, u64), ExtentMutationError> {
778    extent_chunk_end(extents, start, limits, |extent| match extent.kind {
779        ExtentKind::Hole | ExtentKind::AllocatedZero => 8 + 8 + 1,
780        ExtentKind::Content { .. } => 8 + 8 + 1 + 32 + 8,
781    })
782}
783
784fn extent_internal_chunk_end(
785    children: &[Summary],
786    start: usize,
787    limits: DecodeLimits,
788) -> Result<(usize, u64), ExtentMutationError> {
789    extent_chunk_end(children, start, limits, |_| 8 + 8 + 32)
790}
791
792fn extent_chunk_end<T>(
793    items: &[T],
794    start: usize,
795    limits: DecodeLimits,
796    encoded_length: impl Fn(&T) -> usize,
797) -> Result<(usize, u64), ExtentMutationError> {
798    const PAGE_HEADER_BYTES: usize = 8 + 2 + 1 + 4;
799    let maximum_items = usize::try_from(limits.maximum_page_items)
800        .map_err(|_| ExtentMutationError::InvalidLimits)?;
801    let maximum_bytes = usize::try_from(limits.maximum_page_bytes)
802        .map_err(|_| ExtentMutationError::InvalidLimits)?;
803    let mut end = start;
804    let mut bytes = PAGE_HEADER_BYTES;
805    let mut examined = 0_u64;
806    while end < items.len() && end - start < maximum_items {
807        examined = examined.saturating_add(1);
808        #[allow(
809            clippy::indexing_slicing,
810            reason = "the loop guard `end < items.len()` directly establishes `end` is in bounds \
811                      at this point in the loop body"
812        )]
813        let next = bytes
814            .checked_add(encoded_length(&items[end]))
815            .ok_or(ExtentMutationError::PageItemTooLarge)?;
816        if next > maximum_bytes {
817            if end == start {
818                return Err(ExtentMutationError::PageItemTooLarge);
819            }
820            break;
821        }
822        bytes = next;
823        end += 1;
824    }
825    if end == start {
826        return Err(ExtentMutationError::PageItemTooLarge);
827    }
828    Ok((end, examined))
829}
830
831impl<S: crate::AsyncObjectStore> Context<'_, S> {
832    async fn rewrite(
833        &mut self,
834        page_id: ObjectId,
835        original_first: u64,
836        original_end: u64,
837        output_end: u64,
838    ) -> Result<Vec<Summary>, ExtentMutationError> {
839        let mut stack = Vec::new();
840        let mut request = NodeRequest {
841            page: page_id,
842            original_first,
843            original_end,
844            output_end,
845            height: 1,
846        };
847        'traverse: loop {
848            let entered = self.enter_node(request).await?;
849            let mut result = match entered {
850                EnteredNode::Complete(result) => result,
851                EnteredNode::Internal(mut frame) => {
852                    if let Some(next) = self.advance_frame(&mut frame)? {
853                        stack.push(frame);
854                        request = next;
855                        continue;
856                    }
857                    self.finish_frame(frame).await?
858                }
859            };
860            loop {
861                let Some(mut frame) = stack.pop() else {
862                    return Ok(result);
863                };
864                frame.rewritten.extend(result);
865                if let Some(next) = self.advance_frame(&mut frame)? {
866                    stack.push(frame);
867                    request = next;
868                    continue 'traverse;
869                }
870                result = self.finish_frame(frame).await?;
871            }
872        }
873    }
874
875    async fn enter_node(
876        &mut self,
877        request: NodeRequest,
878    ) -> Result<EnteredNode, ExtentMutationError> {
879        if request.height > self.limits.maximum_page_height {
880            return Err(ExtentMutationError::HeightExceeded);
881        }
882        if request.output_end <= request.original_first {
883            return Ok(EnteredNode::Complete(Vec::new()));
884        }
885        self.maximum_seen_height = self.maximum_seen_height.max(request.height);
886        let visited = self.visited.insert(
887            request.page,
888            &mut self.allocations,
889            &mut self.work,
890            self.budget,
891        )?;
892        if !visited.inserted {
893            return Err(ExtentMutationError::CycleOrAlias);
894        }
895        let decoded = self.read_page(request.page).await?;
896        match decoded.page {
897            ExtentPage::Leaf(extents) => {
898                let result = self
899                    .rewrite_leaf(
900                        request.page,
901                        extents,
902                        request.original_first,
903                        request.original_end,
904                        request.output_end,
905                    )
906                    .await;
907                self.allocations.release(decoded.logical_bytes)?;
908                Ok(EnteredNode::Complete(result?))
909            }
910            ExtentPage::Internal(children) => {
911                validate_children(&children, request.original_first, request.original_end)?;
912                Ok(EnteredNode::Internal(InternalFrame {
913                    original: request.page,
914                    children,
915                    original_first: request.original_first,
916                    original_end: request.original_end,
917                    output_end: request.output_end,
918                    height: request.height,
919                    next_child: 0,
920                    rewritten: Vec::new(),
921                    logical_bytes: decoded.logical_bytes,
922                }))
923            }
924        }
925    }
926
927    fn advance_frame(
928        &mut self,
929        frame: &mut InternalFrame,
930    ) -> Result<Option<NodeRequest>, ExtentMutationError> {
931        while frame.next_child < frame.children.len() {
932            let index = frame.next_child;
933            frame.next_child += 1;
934            #[allow(
935                clippy::indexing_slicing,
936                reason = "`index` is captured from `frame.next_child` right after the loop guard \
937                          `frame.next_child < frame.children.len()` confirmed it in bounds, \
938                          before it is incremented"
939            )]
940            let child = &frame.children[index];
941            if child.first_offset >= frame.output_end {
942                self.charge_items(1)?;
943                frame.next_child = frame.children.len();
944                break;
945            }
946            let mut child_output_end = child.end_offset.min(frame.output_end);
947            if index + 1 == frame.children.len() && frame.output_end > frame.original_end {
948                child_output_end = frame.output_end;
949            }
950            let (patch_touched, comparisons) =
951                has_overlapping_patch(&self.patches, child.first_offset, child_output_end);
952            self.charge_items(comparisons.saturating_add(1))?;
953            let touched = child_output_end != child.end_offset || patch_touched;
954            if touched {
955                return Ok(Some(NodeRequest {
956                    page: child.page,
957                    original_first: child.first_offset,
958                    original_end: child.end_offset,
959                    output_end: child_output_end,
960                    height: frame
961                        .height
962                        .checked_add(1)
963                        .ok_or(ExtentMutationError::HeightExceeded)?,
964                }));
965            }
966            frame.rewritten.push(Summary {
967                first: child.first_offset,
968                end: child.end_offset,
969                page: child.page,
970            });
971        }
972        Ok(None)
973    }
974
975    async fn finish_frame(
976        &mut self,
977        frame: InternalFrame,
978    ) -> Result<Vec<Summary>, ExtentMutationError> {
979        let result = if frame.rewritten.is_empty() {
980            Ok(Vec::new())
981        } else if unchanged(&frame.rewritten, &frame.children) {
982            Ok(vec![Summary {
983                first: frame.original_first,
984                end: frame.original_end,
985                page: frame.original,
986            }])
987        } else if frame.height == 1 && frame.rewritten.len() == 1 {
988            Ok(frame.rewritten)
989        } else {
990            self.write_internal_chunks(&frame.rewritten).await
991        };
992        self.allocations.release(frame.logical_bytes)?;
993        result
994    }
995
996    async fn rewrite_leaf(
997        &mut self,
998        original: ObjectId,
999        extents: Vec<Extent>,
1000        original_first: u64,
1001        original_end: u64,
1002        output_end: u64,
1003    ) -> Result<Vec<Summary>, ExtentMutationError> {
1004        validate_leaf(&extents, original_first, original_end)?;
1005        if output_end <= original_first {
1006            return Ok(Vec::new());
1007        }
1008        let original_extents = extents.clone();
1009        let mut rewritten = clip_and_grow(extents, output_end, original_end)?;
1010        let (first_patch, after_last_patch, comparisons) =
1011            overlapping_patch_range(&self.patches, original_first, output_end);
1012        self.work = self.work.checked_add(WorkCounters {
1013            items_examined: comparisons,
1014            ..WorkCounters::default()
1015        })?;
1016        self.work.verify(self.budget)?;
1017        #[allow(
1018            clippy::indexing_slicing,
1019            reason = "overlapping_patch_range returns (first, after_last.max(first), _), where \
1020                      `first` and `after_last` both come from lower_bound's postcondition of \
1021                      `0 <= left <= patches.len()`; the explicit `.max(first)` clamp guarantees \
1022                      `first_patch <= after_last_patch <= self.patches.len()`, so this slice is \
1023                      always in bounds"
1024        )]
1025        let (next, examined) =
1026            apply_patches_linear(rewritten, &self.patches[first_patch..after_last_patch])?;
1027        rewritten = next;
1028        self.work = self.work.checked_add(WorkCounters {
1029            items_examined: examined,
1030            ..WorkCounters::default()
1031        })?;
1032        self.work.verify(self.budget)?;
1033        coalesce(&mut rewritten)?;
1034        if rewritten == original_extents {
1035            return Ok(vec![Summary {
1036                first: original_first,
1037                end: original_end,
1038                page: original,
1039            }]);
1040        }
1041        self.write_leaf_chunks(&rewritten).await
1042    }
1043
1044    async fn write_leaf_chunks(
1045        &mut self,
1046        extents: &[Extent],
1047    ) -> Result<Vec<Summary>, ExtentMutationError> {
1048        let mut result = Vec::new();
1049        let mut start = 0_usize;
1050        #[allow(
1051            clippy::indexing_slicing,
1052            reason = "extent_leaf_chunk_end delegates to extent_chunk_end, which guarantees \
1053                      `start <= end_index <= extents.len()` (the loop there only advances `end` \
1054                      while `end < items.len()`) and returns `Err` rather than success when `end \
1055                      == start` (see the `if end == start` check before its `Ok` return), so \
1056                      `chunk` is always non-empty here and `chunk[0]` / `chunk[chunk.len() - 1]` \
1057                      are in bounds"
1058        )]
1059        while start < extents.len() {
1060            let (end_index, examined) = extent_leaf_chunk_end(extents, start, self.limits)?;
1061            self.charge_items(examined)?;
1062            let chunk = &extents[start..end_index];
1063            let first = chunk[0].offset;
1064            let end = extent_end(&chunk[chunk.len() - 1])?;
1065            result.push(Summary {
1066                first,
1067                end,
1068                page: self.write_page(&ExtentPage::Leaf(chunk.to_vec())).await?,
1069            });
1070            start = end_index;
1071        }
1072        Ok(result)
1073    }
1074
1075    async fn write_internal_chunks(
1076        &mut self,
1077        children: &[Summary],
1078    ) -> Result<Vec<Summary>, ExtentMutationError> {
1079        let mut cursor = 0_usize;
1080        let mut chunks = 0_usize;
1081        while cursor < children.len() {
1082            let (end, examined) = extent_internal_chunk_end(children, cursor, self.limits)?;
1083            self.charge_items(examined)?;
1084            chunks += 1;
1085            cursor = end;
1086        }
1087        if children.len() > 1 && chunks >= children.len() {
1088            return Err(ExtentMutationError::PageItemTooLarge);
1089        }
1090        let mut result = Vec::new();
1091        let mut start = 0_usize;
1092        #[allow(
1093            clippy::indexing_slicing,
1094            reason = "extent_internal_chunk_end delegates to extent_chunk_end, which guarantees \
1095                      `start <= end_index <= children.len()` (the loop there only advances `end` \
1096                      while `end < items.len()`) and returns `Err` rather than success when `end \
1097                      == start` (see the `if end == start` check before its `Ok` return), so \
1098                      `chunk` is always non-empty here and `chunk[0]` / `chunk[chunk.len() - 1]` \
1099                      are in bounds"
1100        )]
1101        while start < children.len() {
1102            let (end_index, examined) = extent_internal_chunk_end(children, start, self.limits)?;
1103            self.charge_items(examined)?;
1104            let chunk = &children[start..end_index];
1105            let first = chunk[0].first;
1106            let end = chunk[chunk.len() - 1].end;
1107            let page = ExtentPage::Internal(
1108                chunk
1109                    .iter()
1110                    .map(|child| ExtentChild {
1111                        first_offset: child.first,
1112                        end_offset: child.end,
1113                        page: child.page,
1114                    })
1115                    .collect(),
1116            );
1117            result.push(Summary {
1118                first,
1119                end,
1120                page: self.write_page(&page).await?,
1121            });
1122            start = end_index;
1123        }
1124        Ok(result)
1125    }
1126
1127    async fn finish_root(
1128        &mut self,
1129        mut summaries: Vec<Summary>,
1130    ) -> Result<ObjectId, ExtentMutationError> {
1131        if summaries.is_empty() {
1132            return self.write_page(&ExtentPage::Leaf(Vec::new())).await;
1133        }
1134        let mut height = self.maximum_seen_height;
1135        while summaries.len() > 1 {
1136            height = height
1137                .checked_add(1)
1138                .ok_or(ExtentMutationError::HeightExceeded)?;
1139            if height > self.limits.maximum_page_height {
1140                return Err(ExtentMutationError::HeightExceeded);
1141            }
1142            summaries = self.write_internal_chunks(&summaries).await?;
1143        }
1144        #[allow(
1145            clippy::indexing_slicing,
1146            reason = "the early `if summaries.is_empty()` return above guarantees `summaries` \
1147                      starts non-empty; write_internal_chunks preserves non-emptiness because \
1148                      its output has one Summary per input chunk and every chunk it forms is \
1149                      non-empty (see the `if end == start` guard in extent_chunk_end, which \
1150                      write_internal_chunks relies on via extent_internal_chunk_end), so a \
1151                      non-empty `summaries` can only stay non-empty across the `while \
1152                      summaries.len() > 1` loop; combined with that loop's exit condition, \
1153                      `summaries.len() == 1` here"
1154        )]
1155        Ok(summaries[0].page)
1156    }
1157
1158    async fn read_page(&mut self, page: ObjectId) -> Result<OwnedExtentPage, ExtentMutationError> {
1159        let prospective = self.work.checked_add(WorkCounters {
1160            page_reads: 1,
1161            ..WorkCounters::default()
1162        })?;
1163        let mut remaining = prospective.remaining(self.budget)?;
1164        remaining.peak_allocation_bytes = self
1165            .budget
1166            .peak_allocation_bytes
1167            .checked_sub(self.allocations.live_bytes())
1168            .ok_or(WorkError::Overflow)?;
1169        let receipt = match crate::AsyncObjectStore::read(
1170            self.store,
1171            page,
1172            self.limits.maximum_page_object_bytes(),
1173            remaining,
1174            self.cancellation,
1175        )
1176        .await
1177        {
1178            Ok(receipt) => receipt,
1179            Err(failure) => {
1180                self.work =
1181                    merge_backend_work(prospective, *failure.work, self.allocations.live_bytes())?;
1182                return Err(ExtentMutationError::Storage(failure.error));
1183            }
1184        };
1185        self.work = merge_backend_work(prospective, receipt.work, self.allocations.live_bytes())?;
1186        self.work.verify(self.budget)?;
1187        let retained_bytes = match receipt.value.retention {
1188            ObjectReadRetention::Shared => 0,
1189            ObjectReadRetention::Owned { logical_bytes } => logical_bytes,
1190        };
1191        self.allocations
1192            .claim_bytes(retained_bytes, 0, &mut self.work, self.budget)?;
1193        let shape = extent_page_decode_shape(&receipt.value, self.limits)?;
1194        self.charge_items(u64::try_from(shape.items).unwrap_or(u64::MAX))?;
1195        let item_bytes = match shape.kind {
1196            DecodedPageKind::Leaf => size_of::<Extent>(),
1197            DecodedPageKind::Internal => size_of::<ExtentChild>(),
1198        };
1199        let logical_bytes = shape
1200            .items
1201            .checked_mul(item_bytes)
1202            .map(crate::foundation::usize_to_u64)
1203            .ok_or(ExtentMutationError::AllocationFailed)?;
1204        self.allocations.claim_bytes(
1205            logical_bytes,
1206            u64::from(logical_bytes != 0),
1207            &mut self.work,
1208            self.budget,
1209        )?;
1210        let decoded = match decode_extent_page(&receipt.value, self.limits) {
1211            Ok(decoded) => decoded,
1212            Err(error) => {
1213                self.allocations.release(logical_bytes)?;
1214                return Err(ExtentMutationError::Decode(error));
1215            }
1216        };
1217        self.allocations.release(retained_bytes)?;
1218        Ok(OwnedExtentPage {
1219            page: decoded,
1220            logical_bytes,
1221        })
1222    }
1223
1224    fn charge_items(&mut self, count: u64) -> Result<(), ExtentMutationError> {
1225        let prospective = self.work.checked_add(WorkCounters {
1226            items_examined: count,
1227            ..WorkCounters::default()
1228        })?;
1229        prospective.verify(self.budget)?;
1230        self.work = prospective;
1231        Ok(())
1232    }
1233
1234    async fn write_page(&mut self, page: &ExtentPage) -> Result<ObjectId, ExtentMutationError> {
1235        let encoded_length = extent_page_encoded_length(page, self.limits.maximum_page_items)?;
1236        let encoded_bytes = crate::foundation::usize_to_u64(encoded_length);
1237        let maximum_page_bytes = self.limits.maximum_page_object_bytes();
1238        if encoded_bytes > maximum_page_bytes {
1239            return Err(ExtentMutationError::Decode(
1240                CanonicalDecodeError::ObjectTooLarge {
1241                    observed: encoded_bytes,
1242                    maximum: maximum_page_bytes,
1243                },
1244            ));
1245        }
1246        self.work
1247            .checked_add(WorkCounters {
1248                bytes_encoded: encoded_bytes,
1249                ..WorkCounters::default()
1250            })?
1251            .verify(self.budget)?;
1252        self.allocations.claim_bytes(
1253            encoded_bytes,
1254            u64::from(encoded_bytes != 0),
1255            &mut self.work,
1256            self.budget,
1257        )?;
1258        let encoded = encode_extent_page(page, self.limits.maximum_page_items)?;
1259        if u64::try_from(encoded.capacity()).unwrap_or(u64::MAX) != encoded_bytes {
1260            return Err(ExtentMutationError::AllocationFailed);
1261        }
1262        self.work = self.work.checked_add(WorkCounters {
1263            bytes_encoded: encoded_bytes,
1264            ..WorkCounters::default()
1265        })?;
1266        self.work.verify(self.budget)?;
1267        let hashed_work = self.work.checked_add(WorkCounters {
1268            bytes_hashed: encoded_bytes
1269                .checked_add(OBJECT_DIGEST_ENVELOPE_BYTES)
1270                .ok_or(ExtentMutationError::RangeOverflow)?,
1271            ..WorkCounters::default()
1272        })?;
1273        hashed_work.verify(self.budget)?;
1274        let object = ObjectId {
1275            kind: ObjectKind::ExtentPage,
1276            digest: object_digest(ObjectKind::ExtentPage, &encoded),
1277        };
1278        self.work = hashed_work;
1279        let prospective = self.work.checked_add(WorkCounters {
1280            page_writes: 1,
1281            ..WorkCounters::default()
1282        })?;
1283        let remaining = prospective.remaining(self.budget)?;
1284        let receipt = match crate::AsyncObjectStore::put(
1285            self.store,
1286            object,
1287            Bytes::from(encoded),
1288            remaining,
1289            self.cancellation,
1290        )
1291        .await
1292        {
1293            Ok(receipt) => receipt,
1294            Err(failure) => {
1295                self.allocations.release(encoded_bytes)?;
1296                self.work =
1297                    merge_backend_work(prospective, *failure.work, self.allocations.live_bytes())?;
1298                return Err(ExtentMutationError::Storage(failure.error));
1299            }
1300        };
1301        self.work = prospective.checked_add(receipt.work)?;
1302        self.work.verify(self.budget)?;
1303        self.allocations.release(encoded_bytes)?;
1304        Ok(object)
1305    }
1306}
1307
1308fn merge_backend_work(
1309    prior: WorkCounters,
1310    mut backend: WorkCounters,
1311    live_bytes: u64,
1312) -> Result<WorkCounters, WorkError> {
1313    let simultaneous_peak = live_bytes
1314        .checked_add(backend.peak_allocation_bytes)
1315        .ok_or(WorkError::Overflow)?;
1316    backend.peak_allocation_bytes = 0;
1317    let mut merged = prior.checked_add(backend)?;
1318    merged.peak_allocation_bytes = merged.peak_allocation_bytes.max(simultaneous_peak);
1319    Ok(merged)
1320}
1321
1322fn clip_and_grow(
1323    extents: Vec<Extent>,
1324    output_end: u64,
1325    original_end: u64,
1326) -> Result<Vec<Extent>, ExtentMutationError> {
1327    let mut result = Vec::new();
1328    for mut extent in extents {
1329        if extent.offset >= output_end {
1330            break;
1331        }
1332        let end = extent_end(&extent)?.min(output_end);
1333        extent.length = end - extent.offset;
1334        result.push(extent);
1335    }
1336    if output_end > original_end {
1337        result.push(Extent {
1338            offset: original_end,
1339            length: output_end - original_end,
1340            kind: ExtentKind::Hole,
1341        });
1342    }
1343    Ok(result)
1344}
1345
1346fn apply_patches_linear(
1347    extents: Vec<Extent>,
1348    patches: &[Patch],
1349) -> Result<(Vec<Extent>, u64), ExtentMutationError> {
1350    let capacity = extents
1351        .len()
1352        .checked_add(patches.len().saturating_mul(2))
1353        .ok_or(ExtentMutationError::RangeOverflow)?;
1354    let mut result = Vec::with_capacity(capacity);
1355    let mut patch_index = 0;
1356    let mut examined = 0_u64;
1357    for extent in extents {
1358        examined = examined.saturating_add(1);
1359        let extent_end = extent_end(&extent)?;
1360        let mut cursor = extent.offset;
1361        while patches
1362            .get(patch_index)
1363            .is_some_and(|patch| patch.end <= cursor)
1364        {
1365            patch_index += 1;
1366            examined = examined.saturating_add(1);
1367        }
1368        while let Some(patch) = patches.get(patch_index) {
1369            examined = examined.saturating_add(1);
1370            if patch.offset >= extent_end {
1371                break;
1372            }
1373            if cursor < patch.offset {
1374                let unchanged_end = patch.offset.min(extent_end);
1375                result.push(Extent {
1376                    offset: cursor,
1377                    length: unchanged_end - cursor,
1378                    kind: offset_kind(&extent.kind, cursor - extent.offset)?,
1379                });
1380                cursor = unchanged_end;
1381            }
1382            let replacement_start = cursor.max(patch.offset);
1383            let replacement_end = extent_end.min(patch.end);
1384            if replacement_start < replacement_end {
1385                result.push(Extent {
1386                    offset: replacement_start,
1387                    length: replacement_end - replacement_start,
1388                    kind: offset_kind(&patch.kind, replacement_start - patch.offset)?,
1389                });
1390                cursor = replacement_end;
1391            }
1392            if patch.end <= cursor {
1393                patch_index += 1;
1394            } else {
1395                break;
1396            }
1397        }
1398        if cursor < extent_end {
1399            result.push(Extent {
1400                offset: cursor,
1401                length: extent_end - cursor,
1402                kind: offset_kind(&extent.kind, cursor - extent.offset)?,
1403            });
1404        }
1405    }
1406    Ok((result, examined))
1407}
1408
1409fn coalesce(extents: &mut Vec<Extent>) -> Result<(), ExtentMutationError> {
1410    let mut result: Vec<Extent> = Vec::with_capacity(extents.len());
1411    for extent in extents.drain(..) {
1412        if let Some(previous) = result.last_mut()
1413            && equivalent_continuation(previous, &extent)?
1414        {
1415            previous.length = previous
1416                .length
1417                .checked_add(extent.length)
1418                .ok_or(ExtentMutationError::RangeOverflow)?;
1419            continue;
1420        }
1421        result.push(extent);
1422    }
1423    *extents = result;
1424    Ok(())
1425}
1426
1427fn equivalent_continuation(left: &Extent, right: &Extent) -> Result<bool, ExtentMutationError> {
1428    if extent_end(left)? != right.offset {
1429        return Ok(false);
1430    }
1431    Ok(match (&left.kind, &right.kind) {
1432        (ExtentKind::Hole, ExtentKind::Hole)
1433        | (ExtentKind::AllocatedZero, ExtentKind::AllocatedZero) => true,
1434        (
1435            ExtentKind::Content {
1436                object: left_object,
1437                object_offset: left_offset,
1438            },
1439            ExtentKind::Content {
1440                object: right_object,
1441                object_offset: right_offset,
1442            },
1443        ) => {
1444            left_object == right_object
1445                && left_offset.checked_add(left.length) == Some(*right_offset)
1446        }
1447        _ => false,
1448    })
1449}
1450
1451fn offset_kind(kind: &ExtentKind, delta: u64) -> Result<ExtentKind, ExtentMutationError> {
1452    match kind {
1453        ExtentKind::Content {
1454            object,
1455            object_offset,
1456        } => Ok(ExtentKind::Content {
1457            object: *object,
1458            object_offset: object_offset
1459                .checked_add(delta)
1460                .ok_or(ExtentMutationError::RangeOverflow)?,
1461        }),
1462        value => Ok(value.clone()),
1463    }
1464}
1465
1466fn validate_leaf(
1467    extents: &[Extent],
1468    expected_first: u64,
1469    expected_end: u64,
1470) -> Result<(), ExtentMutationError> {
1471    if expected_first == expected_end && extents.is_empty() {
1472        return Ok(());
1473    }
1474    if extents.first().map(|extent| extent.offset) != Some(expected_first)
1475        || extents.last().map(extent_end).transpose()? != Some(expected_end)
1476    {
1477        return Err(ExtentMutationError::ChildBoundsMismatch);
1478    }
1479    Ok(())
1480}
1481
1482fn validate_children(
1483    children: &[ExtentChild],
1484    expected_first: u64,
1485    expected_end: u64,
1486) -> Result<(), ExtentMutationError> {
1487    if children.first().map(|child| child.first_offset) != Some(expected_first)
1488        || children.last().map(|child| child.end_offset) != Some(expected_end)
1489    {
1490        return Err(ExtentMutationError::ChildBoundsMismatch);
1491    }
1492    Ok(())
1493}
1494
1495fn unchanged(summaries: &[Summary], children: &[ExtentChild]) -> bool {
1496    summaries.len() == children.len()
1497        && summaries.iter().zip(children).all(|(left, right)| {
1498            left.first == right.first_offset
1499                && left.end == right.end_offset
1500                && left.page == right.page
1501        })
1502}
1503
1504fn overlapping_patch_range(patches: &[Patch], start: u64, end: u64) -> (usize, usize, u64) {
1505    let (first, first_comparisons) = lower_bound(patches, |patch| patch.end <= start);
1506    let (after_last, last_comparisons) = lower_bound(patches, |patch| patch.offset < end);
1507    (
1508        first,
1509        after_last.max(first),
1510        first_comparisons.saturating_add(last_comparisons),
1511    )
1512}
1513
1514fn has_overlapping_patch(patches: &[Patch], start: u64, end: u64) -> (bool, u64) {
1515    let (first, after_last, comparisons) = overlapping_patch_range(patches, start, end);
1516    (first < after_last, comparisons)
1517}
1518
1519fn lower_bound(patches: &[Patch], before: impl Fn(&Patch) -> bool) -> (usize, u64) {
1520    let mut left = 0;
1521    let mut right = patches.len();
1522    let mut comparisons = 0_u64;
1523    while left < right {
1524        comparisons = comparisons.saturating_add(1);
1525        let middle = left + (right - left) / 2;
1526        #[allow(
1527            clippy::indexing_slicing,
1528            reason = "binary-search invariant: the loop guard `left < right` with `right` \
1529                      initialized to patches.len() and only ever narrowed to `middle` keeps \
1530                      `middle = left + (right - left) / 2` strictly within `[left, right)`, so \
1531                      `middle < patches.len()` always holds here"
1532        )]
1533        let below = before(&patches[middle]);
1534        if below {
1535            left = middle + 1;
1536        } else {
1537            right = middle;
1538        }
1539    }
1540    (left, comparisons)
1541}
1542
1543fn extent_end(extent: &Extent) -> Result<u64, ExtentMutationError> {
1544    extent
1545        .offset
1546        .checked_add(extent.length)
1547        .ok_or(ExtentMutationError::RangeOverflow)
1548}
1549
1550/// Persistent sparse extent mutation failures.
1551#[derive(Debug, Error)]
1552pub enum ExtentMutationError {
1553    /// Batch contains no mutations.
1554    #[error("extent mutation batch is empty")]
1555    Empty,
1556    /// Batch exceeds its admitted bound.
1557    #[error("extent mutation count exceeds its bound")]
1558    TooManyMutations,
1559    /// Root has the wrong object kind.
1560    #[error("extent mutation root has the wrong kind")]
1561    WrongRootKind,
1562    /// Page width/height cannot support mutation.
1563    #[error("extent mutation limits are invalid")]
1564    InvalidLimits,
1565    /// One extent or required pair of child routes cannot fit in a page.
1566    #[error("extent mutation item exceeds its page byte bound")]
1567    PageItemTooLarge,
1568    /// Replacement range must be non-empty.
1569    #[error("extent mutation range is empty")]
1570    EmptyRange,
1571    /// Range arithmetic overflowed.
1572    #[error("extent mutation range overflowed")]
1573    RangeOverflow,
1574    /// Non-extending replacement exceeds current logical file length.
1575    #[error("extent mutation exceeds logical file length")]
1576    OutsideFile,
1577    /// Content replacement references another object class.
1578    #[error("extent mutation content is not a blob")]
1579    WrongContentKind,
1580    /// Internal normalized-patch map was inconsistent.
1581    #[error("extent mutation normalized patch map is inconsistent")]
1582    PatchInvariant,
1583    /// Fallible fixed-capacity scratch allocation was unavailable.
1584    #[error("extent mutation scratch allocation failed")]
1585    AllocationFailed,
1586    /// Traversal or growth exceeds the page-height bound.
1587    #[error("extent mutation exceeds its height bound")]
1588    HeightExceeded,
1589    /// Authenticated page graph cycles or aliases.
1590    #[error("extent mutation encountered a cycle or alias")]
1591    CycleOrAlias,
1592    /// Parent range bounds differ from child content.
1593    #[error("extent mutation child bounds mismatch")]
1594    ChildBoundsMismatch,
1595    /// Immutable object storage failed.
1596    #[error(transparent)]
1597    Storage(#[from] ObjectStoreError),
1598    /// Canonical page encoding/decoding failed.
1599    #[error(transparent)]
1600    Decode(#[from] CanonicalDecodeError),
1601    /// Exact work exceeded or overflowed its budget.
1602    #[error(transparent)]
1603    Work(#[from] WorkError),
1604}
1605
1606impl From<AllocationError> for ExtentMutationError {
1607    fn from(error: AllocationError) -> Self {
1608        match error {
1609            AllocationError::Work(error) => Self::Work(error),
1610            AllocationError::Overflow | AllocationError::ReleaseInvariant => {
1611                Self::Work(WorkError::Overflow)
1612            }
1613            AllocationError::InvalidCapacity
1614            | AllocationError::CapacityExceeded
1615            | AllocationError::AllocationFailed => Self::AllocationFailed,
1616        }
1617    }
1618}
1619
1620#[cfg(all(test, feature = "memory"))]
1621#[path = "tests/extent_mutation.rs"]
1622mod tests;