Skip to main content

acyclic_fs/kernel/
generation_mutation.rs

1//! One-volume generation transaction composition over sparse persistent kernels.
2
3use super::allocation::{AllocationError, AllocationLedger};
4use super::regular_mutation::{
5    RegularMutation, RegularMutationError, apply_regular_clone_async, apply_regular_mutation_async,
6};
7use super::{
8    DecodeLimits, DirectoryReadError, FileKind, FileMutation, FilePayload, FileRecord,
9    FileRecordReadError, FileTableMutation, FileTableMutationError, GenerationRoot, LogicalName,
10    MetadataField, Mutation, MutationPlan, MutationPlanError, NamespacePath, PathBatchEntry,
11    PathLookupError, TreeEntry, TreeMutation, TreeMutationError, apply_file_table_mutations_async,
12    apply_tree_mutations_async, list_tree_entries_async, lookup_file_records_async,
13    lookup_path_refs_async,
14};
15use crate::async_storage::{AsyncObjectStore, BoxStorageFuture};
16use crate::cancellation::CancellationToken;
17use crate::foundation::{FileId, GenerationId};
18use crate::model::{VolumeConfig, VolumeConfigError};
19use crate::performance::{OperationFailure, WorkBudget, WorkCounters, WorkError};
20use crate::storage::ObjectId;
21use std::cell::Cell;
22use std::mem::size_of;
23use thiserror::Error;
24
25/// Candidate generation produced by one atomic within-volume transaction.
26#[derive(Clone, Debug, Eq, PartialEq)]
27pub struct GenerationMutationReceipt {
28    /// Unpublished immutable generation root.
29    pub root: GenerationRoot,
30    /// Exact planning, lookup, path-copy, and immutable-write work.
31    pub work: WorkCounters,
32}
33
34/// Generation transaction failure preserving spent work and harmless orphan objects.
35pub type GenerationMutationFailure = OperationFailure<GenerationMutationError>;
36
37/// Stable one-volume generation transaction failures.
38#[derive(Debug, Error)]
39pub enum GenerationMutationError {
40    /// A path mutation planner rejected the batch.
41    #[error(transparent)]
42    Plan(#[from] MutationPlanError),
43    /// Volume limits or semantic configuration are invalid.
44    #[error(transparent)]
45    Config(#[from] VolumeConfigError),
46    /// Mutation endpoints exceed the admitted shared-path batch bound.
47    #[error("generation mutation endpoint count exceeds the volume path limit")]
48    TooManyPaths,
49    /// A newly created record must begin with exactly one namespace binding.
50    #[error("new file record must have link count one")]
51    InvalidInitialLinkCount,
52    /// The volume does not admit symbolic links.
53    #[error("symbolic links are disabled for this volume")]
54    UnsupportedSymbolicLink,
55    /// The volume profile cannot represent the created kind exactly.
56    #[error("file kind is unsupported by the volume profile")]
57    UnsupportedFileKind,
58    /// The requested operation can introduce sparse semantics in a dense volume.
59    #[error("sparse mutation is disabled for this volume")]
60    UnsupportedSparseMutation,
61    /// Shared authenticated path resolution failed.
62    #[error(transparent)]
63    Path(#[from] PathLookupError),
64    /// Created-identity freshness lookup failed.
65    #[error(transparent)]
66    FileRecord(#[from] FileRecordReadError),
67    /// One operation requires a parent directory absent from the candidate view.
68    #[error("namespace mutation parent directory is missing")]
69    MissingParent,
70    /// One operation requires an existing source binding.
71    #[error("namespace mutation source is missing")]
72    MissingSource,
73    /// A create/link/rename destination expected absence.
74    #[error("namespace mutation destination already exists")]
75    AlreadyExists,
76    /// A supplied exact file identity precondition failed.
77    #[error("namespace mutation file identity precondition failed")]
78    FileIdentityConflict,
79    /// A create reused an identity already present in the base or batch.
80    #[error("namespace mutation created file identity is not fresh")]
81    FileIdentityAlreadyExists,
82    /// A hard link targeted a directory or a volume without hard-link support.
83    #[error("namespace mutation hard link is unsupported")]
84    UnsupportedHardLink,
85    /// A directory move would place the directory below itself.
86    #[error("namespace mutation would create a directory cycle")]
87    DirectoryCycle,
88    /// A removed or replaced directory is not empty in the candidate view.
89    #[error("namespace mutation directory is not empty")]
90    DirectoryNotEmpty,
91    /// Authenticated state and sparse planning disagree.
92    #[error("namespace mutation authenticated state is inconsistent")]
93    InconsistentState,
94    /// Directory path-copy failed.
95    #[error(transparent)]
96    Tree(#[from] TreeMutationError),
97    /// File-table path-copy failed.
98    #[error(transparent)]
99    FileTable(#[from] FileTableMutationError),
100    /// Regular-file sparse mutation failed.
101    #[error(transparent)]
102    Regular(#[from] RegularMutationError),
103    /// Bounded empty-directory proof failed.
104    #[error(transparent)]
105    Directory(#[from] DirectoryReadError),
106    /// Bounded scratch storage could not be allocated.
107    #[error("namespace mutation scratch allocation failed")]
108    AllocationFailed,
109    /// Exact work overflowed or exceeded the request budget.
110    #[error(transparent)]
111    Work(#[from] WorkError),
112}
113
114#[derive(Clone, Copy, Debug, Eq, PartialEq)]
115struct Binding {
116    file_id: FileId,
117    kind: FileKind,
118}
119
120#[derive(Clone, Copy)]
121struct PathState {
122    operation: usize,
123    endpoint: usize,
124    binding: Option<Binding>,
125    base_parent: Option<FileId>,
126    base_parent_exact: bool,
127    parent_state: Option<usize>,
128}
129
130#[derive(Clone, Copy)]
131struct RecordSeed {
132    record: FileRecord,
133    created: bool,
134    replacement: bool,
135}
136
137#[derive(Clone, Copy)]
138struct RecordState {
139    base: Option<FileRecord>,
140    working: FileRecord,
141    present: bool,
142}
143
144struct DirectoryEdit {
145    directory_id: FileId,
146    order: u32,
147    retained_name_bytes: u64,
148    mutation: Option<TreeMutation>,
149}
150
151struct FreshnessContext<'a, S> {
152    store: &'a S,
153    generation: &'a GenerationRoot,
154    config: VolumeConfig,
155    budget: WorkBudget,
156    cancellation: &'a CancellationToken,
157}
158
159struct InitialLookups {
160    paths: Vec<PathBatchEntry>,
161    path_bytes: u64,
162    identities: Vec<Option<FileRecord>>,
163    identity_bytes: u64,
164}
165
166/// Applies one ordered namespace, metadata, and content batch to a generation.
167///
168/// All path preconditions are evaluated against a sparse candidate view. The
169/// executor performs one borrowed-path lookup, groups edits by stable parent
170/// directory identity, rewrites every touched directory once, and then rewrites
171/// the global file table once. It never publishes authority state; callers feed
172/// the returned root into checkpoint and publication.
173///
174/// # Errors
175///
176/// Rejects malformed or contradictory candidate state before publication and
177/// retains exact work plus safe orphan-write evidence on storage,
178/// cancellation, allocation, or semantic failure.
179pub async fn apply_generation_mutations_async<S: AsyncObjectStore>(
180    store: &S,
181    generation: &GenerationRoot,
182    operations: Vec<Mutation>,
183    config: VolumeConfig,
184    budget: WorkBudget,
185    cancellation: &CancellationToken,
186) -> Result<GenerationMutationReceipt, GenerationMutationFailure> {
187    let (receipt, _operations) = apply_generation_mutations_retaining_async(
188        store,
189        generation,
190        operations,
191        config,
192        budget,
193        cancellation,
194    )
195    .await?;
196    Ok(receipt)
197}
198
199pub(crate) async fn apply_generation_mutations_retaining_async<S: AsyncObjectStore>(
200    store: &S,
201    generation: &GenerationRoot,
202    operations: Vec<Mutation>,
203    config: VolumeConfig,
204    budget: WorkBudget,
205    cancellation: &CancellationToken,
206) -> Result<(GenerationMutationReceipt, Vec<Mutation>), GenerationMutationFailure> {
207    cancellation
208        .check()
209        .map_err(|_| OperationFailure::before_work(path_cancelled()))?;
210    preflight_generation_mutations(&operations, config)?;
211    let plan = MutationPlan::compile(operations, config.limits, budget)
212        .map_err(|failure| OperationFailure::new(failure.error.into(), *failure.work))?;
213    let mut work = plan.work();
214    let mut allocations = AllocationLedger::default();
215    allocations
216        .claim_bytes(plan.retained_allocation_bytes(), 0, &mut work, budget)
217        .map_err(|error| allocation_failure(error, work))?;
218
219    let (created_ids, created_id_bytes) = verify_created_identities(
220        FreshnessContext {
221            store,
222            generation,
223            config,
224            budget,
225            cancellation,
226        },
227        &plan,
228        &mut allocations,
229        &mut work,
230    )
231    .await?;
232
233    let lookups = load_initial_lookups(
234        FreshnessContext {
235            store,
236            generation,
237            config,
238            budget,
239            cancellation,
240        },
241        &plan,
242        &created_ids,
243        &mut allocations,
244        &mut work,
245    )
246    .await?;
247    drop(created_ids);
248    allocations
249        .release(created_id_bytes)
250        .map_err(|error| allocation_failure(error, work))?;
251
252    let mut state = TransactionState::new(&plan, lookups, config, allocations, work, budget)?;
253    state.simulate(store, &plan, config, cancellation).await?;
254    let plan_bytes = plan.retained_allocation_bytes();
255    let operations = plan.into_operations();
256    state
257        .allocations
258        .release(plan_bytes)
259        .map_err(|error| allocation_failure(error, state.work))?;
260    state
261        .rewrite_directories(store, config, cancellation)
262        .await?;
263    state
264        .verify_removed_directories(store, config, cancellation)
265        .await?;
266    let receipt = state
267        .rewrite_file_table(store, generation, config, cancellation)
268        .await?;
269    Ok((receipt, operations))
270}
271
272/// Synchronous adapter over [`apply_generation_mutations_async`].
273///
274/// # Errors
275///
276/// Returns the same bounded failures, plus a typed rejection if a nominally
277/// synchronous object store unexpectedly suspends.
278pub fn apply_generation_mutations<S: crate::ImmediateObjectStore>(
279    store: &S,
280    generation: &GenerationRoot,
281    operations: Vec<Mutation>,
282    config: VolumeConfig,
283    budget: WorkBudget,
284) -> Result<GenerationMutationReceipt, GenerationMutationFailure> {
285    crate::async_storage::poll_immediate(apply_generation_mutations_async(
286        store,
287        generation,
288        operations,
289        config,
290        budget,
291        &CancellationToken::new(),
292    ))
293}
294
295async fn verify_created_identities<S: AsyncObjectStore>(
296    context: FreshnessContext<'_, S>,
297    plan: &MutationPlan,
298    allocations: &mut AllocationLedger,
299    work: &mut WorkCounters,
300) -> Result<(Vec<FileId>, u64), GenerationMutationFailure> {
301    let created_count = plan
302        .operations()
303        .iter()
304        .filter(|operation| matches!(operation, Mutation::Create { .. }))
305        .count();
306    if created_count == 0 {
307        return Ok((Vec::new(), 0));
308    }
309    let mut created_ids =
310        reserve_exact::<FileId>(created_count, allocations, work, context.budget)?;
311    created_ids.extend(plan.operations().iter().filter_map(|operation| {
312        if let Mutation::Create { record, .. } = operation {
313            Some(record.file_id)
314        } else {
315            None
316        }
317    }));
318    let comparisons = Cell::new(0_u64);
319    created_ids.sort_unstable_by(|left, right| {
320        comparisons.set(comparisons.get().saturating_add(1));
321        left.cmp(right)
322    });
323    charge_items(work, comparisons.get(), context.budget)?;
324    let mut duplicate_examined = 0_u64;
325    for pair in created_ids.windows(2) {
326        duplicate_examined = duplicate_examined
327            .checked_add(1)
328            .ok_or_else(|| failed(WorkError::Overflow.into(), *work))?;
329        if let [left, right] = pair
330            && left == right
331        {
332            charge_items(work, duplicate_examined, context.budget)?;
333            return Err(failed(
334                GenerationMutationError::FileIdentityAlreadyExists,
335                *work,
336            ));
337        }
338    }
339    charge_items(work, duplicate_examined, context.budget)?;
340    let created_bytes = logical_vec_bytes(&created_ids)?;
341    let freshness = lookup_file_records_async(
342        context.store,
343        context.generation.file_table,
344        &created_ids,
345        context.config.limits.maximum_mutations_per_batch,
346        decode_limits(context.config),
347        nested_budget(*work, context.budget, allocations.live_bytes())?,
348        context.cancellation,
349    )
350    .await
351    .map_err(|failure| {
352        nested_failure(
353            *work,
354            *failure.work,
355            allocations.live_bytes(),
356            failure.error.into(),
357        )
358    })?;
359    *work = merge_nested(
360        *work,
361        freshness.work,
362        allocations.live_bytes(),
363        context.budget,
364    )?;
365    if freshness.records.iter().any(Option::is_some) {
366        return Err(failed(
367            GenerationMutationError::FileIdentityAlreadyExists,
368            *work,
369        ));
370    }
371    drop(freshness);
372    Ok((created_ids, created_bytes))
373}
374
375async fn load_initial_lookups<S: AsyncObjectStore>(
376    context: FreshnessContext<'_, S>,
377    plan: &MutationPlan,
378    created_ids: &[FileId],
379    allocations: &mut AllocationLedger,
380    work: &mut WorkCounters,
381) -> Result<InitialLookups, GenerationMutationFailure> {
382    let (paths, path_bytes) = load_path_lookups(&context, plan, allocations, work).await?;
383    let (identities, identity_bytes) =
384        load_identity_lookups(&context, plan, created_ids, allocations, work).await?;
385    Ok(InitialLookups {
386        paths,
387        path_bytes,
388        identities,
389        identity_bytes,
390    })
391}
392
393async fn load_path_lookups<S: AsyncObjectStore>(
394    context: &FreshnessContext<'_, S>,
395    plan: &MutationPlan,
396    allocations: &mut AllocationLedger,
397    work: &mut WorkCounters,
398) -> Result<(Vec<PathBatchEntry>, u64), GenerationMutationFailure> {
399    let path_count = plan.ordered_operation_paths().len();
400    let mut path_refs =
401        reserve_exact::<&NamespacePath>(path_count, allocations, work, context.budget)?;
402    path_refs.extend(plan.ordered_paths());
403    let path_ref_bytes = logical_vec_bytes(&path_refs)?;
404    let (paths, path_bytes) = if path_refs.is_empty() {
405        (Vec::new(), 0)
406    } else {
407        let lookup = lookup_path_refs_async(
408            context.store,
409            context.generation,
410            &path_refs,
411            context.config,
412            nested_budget(*work, context.budget, allocations.live_bytes())?,
413            context.cancellation,
414        )
415        .await
416        .map_err(|failure| {
417            nested_failure(
418                *work,
419                *failure.work,
420                allocations.live_bytes(),
421                failure.error.into(),
422            )
423        })?;
424        *work = merge_nested(*work, lookup.work, allocations.live_bytes(), context.budget)?;
425        (lookup.entries, lookup.retained_allocation_bytes)
426    };
427    drop(path_refs);
428    allocations
429        .release(path_ref_bytes)
430        .map_err(|error| allocation_failure(error, *work))?;
431    allocations
432        .claim_bytes(path_bytes, 0, work, context.budget)
433        .map_err(|error| allocation_failure(error, *work))?;
434    Ok((paths, path_bytes))
435}
436
437#[allow(clippy::too_many_lines)]
438async fn load_identity_lookups<S: AsyncObjectStore>(
439    context: &FreshnessContext<'_, S>,
440    plan: &MutationPlan,
441    created_ids: &[FileId],
442    allocations: &mut AllocationLedger,
443    work: &mut WorkCounters,
444) -> Result<(Vec<Option<FileRecord>>, u64), GenerationMutationFailure> {
445    let mut maximum_identity_uses = 0_usize;
446    for operation in plan.operations() {
447        maximum_identity_uses = maximum_identity_uses
448            .checked_add(match operation {
449                Mutation::File { .. } | Mutation::Restore { .. } => 1,
450                Mutation::CloneFileRange { .. } => 2,
451                _ => 0,
452            })
453            .ok_or_else(|| failed(WorkError::Overflow.into(), *work))?;
454    }
455    if maximum_identity_uses == 0 {
456        return Ok((Vec::new(), 0));
457    }
458    let mut identity_ids =
459        reserve_exact::<FileId>(maximum_identity_uses, allocations, work, context.budget)?;
460    for operation in plan.operations() {
461        match operation {
462            Mutation::File { file_id, .. } => identity_ids.push(*file_id),
463            Mutation::Restore { record, .. } => identity_ids.push(record.file_id),
464            Mutation::CloneFileRange {
465                source_file_id,
466                destination_file_id,
467                ..
468            } => {
469                identity_ids.push(*source_file_id);
470                identity_ids.push(*destination_file_id);
471            }
472            _ => {}
473        }
474    }
475    let comparisons = Cell::new(0_u64);
476    identity_ids.sort_unstable_by(|left, right| {
477        comparisons.set(comparisons.get().saturating_add(1));
478        left.cmp(right)
479    });
480    charge_items(work, comparisons.get(), context.budget)?;
481    // `dedup` examines every sorted identity once. Keep that deterministic
482    // linear pass visible in the exact-work receipt instead of hiding it in
483    // standard-library collection maintenance.
484    charge_items(
485        work,
486        crate::foundation::usize_to_u64(identity_ids.len()),
487        context.budget,
488    )?;
489    identity_ids.dedup();
490    let identity_id_bytes = logical_vec_bytes(&identity_ids)?;
491    let identities = if identity_ids.is_empty() {
492        Vec::new()
493    } else {
494        let lookup = lookup_file_records_async(
495            context.store,
496            context.generation.file_table,
497            &identity_ids,
498            u32::try_from(identity_ids.len())
499                .map_err(|_| failed(WorkError::Overflow.into(), *work))?,
500            decode_limits(context.config),
501            nested_budget(*work, context.budget, allocations.live_bytes())?,
502            context.cancellation,
503        )
504        .await
505        .map_err(|failure| {
506            nested_failure(
507                *work,
508                *failure.work,
509                allocations.live_bytes(),
510                failure.error.into(),
511            )
512        })?;
513        *work = merge_nested(*work, lookup.work, allocations.live_bytes(), context.budget)?;
514        for (file_id, record) in identity_ids.iter().zip(&lookup.records) {
515            if record.is_none() {
516                let comparisons = Cell::new(0_u64);
517                let created = created_ids.binary_search_by(|candidate| {
518                    comparisons.set(comparisons.get().saturating_add(1));
519                    candidate.cmp(file_id)
520                });
521                charge_items(work, comparisons.get(), context.budget)?;
522                let mut restored = false;
523                let mut restore_examined = 0_u64;
524                for operation in plan.operations() {
525                    restore_examined = restore_examined
526                        .checked_add(1)
527                        .ok_or_else(|| failed(WorkError::Overflow.into(), *work))?;
528                    if matches!(operation, Mutation::Restore { record, .. } if record.file_id == *file_id)
529                    {
530                        restored = true;
531                        break;
532                    }
533                }
534                charge_items(work, restore_examined, context.budget)?;
535                if created.is_err() && !restored {
536                    return Err(failed(GenerationMutationError::MissingSource, *work));
537                }
538            }
539        }
540        lookup.records
541    };
542    let identity_bytes = logical_vec_bytes(&identities)?;
543    allocations
544        .claim_bytes(identity_bytes, 0, work, context.budget)
545        .map_err(|error| allocation_failure(error, *work))?;
546    drop(identity_ids);
547    allocations
548        .release(identity_id_bytes)
549        .map_err(|error| allocation_failure(error, *work))?;
550    Ok((identities, identity_bytes))
551}
552
553struct TransactionState {
554    paths: Vec<PathState>,
555    operation_paths: Vec<[usize; 2]>,
556    records: Vec<RecordState>,
557    directory_edits: Vec<DirectoryEdit>,
558    removed_directories: Vec<FileId>,
559    allocations: AllocationLedger,
560    work: WorkCounters,
561    budget: WorkBudget,
562    config: VolumeConfig,
563}
564
565impl TransactionState {
566    #[allow(clippy::too_many_lines)]
567    fn new(
568        plan: &MutationPlan,
569        lookups: InitialLookups,
570        config: VolumeConfig,
571        mut allocations: AllocationLedger,
572        mut work: WorkCounters,
573        budget: WorkBudget,
574    ) -> Result<Self, GenerationMutationFailure> {
575        let InitialLookups {
576            paths: lookup,
577            path_bytes: lookup_retained_bytes,
578            identities: identity_records,
579            identity_bytes: identity_record_bytes,
580        } = lookups;
581        let operation_count = plan.operations().len();
582        let mut operation_paths =
583            reserve_exact::<[usize; 2]>(operation_count, &mut allocations, &mut work, budget)?;
584        operation_paths.resize(operation_count, [usize::MAX; 2]);
585        let mut paths =
586            reserve_exact::<PathState>(lookup.len(), &mut allocations, &mut work, budget)?;
587        if plan.ordered_operation_paths().len() != lookup.len() {
588            return Err(failed(GenerationMutationError::InconsistentState, work));
589        }
590        let mut seeds = reserve_exact::<RecordSeed>(
591            lookup
592                .len()
593                .checked_mul(2)
594                .and_then(|value| value.checked_add(operation_count))
595                .and_then(|value| value.checked_add(identity_records.len()))
596                .ok_or_else(|| failed(WorkError::Overflow.into(), work))?,
597            &mut allocations,
598            &mut work,
599            budget,
600        )?;
601        let mut previous: Option<&NamespacePath> = None;
602        let mut previous_entry: Option<PathBatchEntry> = None;
603        let mut current_state = usize::MAX;
604        for ((operation, endpoint, path), entry) in
605            plan.ordered_operation_paths().zip(lookup.iter())
606        {
607            if previous != Some(path) {
608                current_state = paths.len();
609                let binding = entry.record.map(|record| Binding {
610                    file_id: record.file_id,
611                    kind: record.kind,
612                });
613                paths.push(PathState {
614                    operation,
615                    endpoint,
616                    binding,
617                    base_parent: entry.parent.map(|record| record.file_id),
618                    base_parent_exact: (entry.record.is_some()
619                        && usize::from(entry.resolved_components) == path.depth())
620                        || (entry.record.is_none()
621                            && usize::from(entry.resolved_components).saturating_add(1)
622                                == path.depth()),
623                    parent_state: None,
624                });
625                previous = Some(path);
626            } else if previous_entry != Some(*entry) {
627                return Err(failed(GenerationMutationError::InconsistentState, work));
628            }
629            previous_entry = Some(*entry);
630            #[allow(
631                clippy::indexing_slicing,
632                reason = "`operation` and `endpoint` come from `plan.ordered_operation_paths()`, \
633                          whose (operation, endpoint) pairs are built in \
634                          `MutationPlan::compile` from `operations.iter().enumerate()` (so \
635                          `operation < plan.operations().len()`) with `endpoint` only ever 0 or \
636                          1; `operation_paths` above is reserved and `resize`d to exactly \
637                          `operation_count == plan.operations().len()` entries of `[usize; 2]`, \
638                          so both index operations are in-bounds by construction"
639            )]
640            {
641                operation_paths[operation][endpoint] = current_state;
642                if endpoint == 0 && operation_uses_one_path(&plan.operations()[operation]) {
643                    operation_paths[operation][1] = current_state;
644                }
645            }
646            if let Some(record) = entry.record {
647                seeds.push(RecordSeed {
648                    record,
649                    created: false,
650                    replacement: false,
651                });
652            }
653            if let Some(record) = entry.parent {
654                seeds.push(RecordSeed {
655                    record,
656                    created: false,
657                    replacement: false,
658                });
659            }
660        }
661        for operation in plan.operations() {
662            if let Mutation::Create { record, .. } = operation {
663                seeds.push(RecordSeed {
664                    record: *record,
665                    created: true,
666                    replacement: false,
667                });
668            }
669            if let Mutation::Restore { record, .. } = operation {
670                seeds.push(RecordSeed {
671                    record: *record,
672                    created: false,
673                    replacement: true,
674                });
675            }
676        }
677        for record in identity_records.iter().flatten() {
678            seeds.push(RecordSeed {
679                record: *record,
680                created: false,
681                replacement: false,
682            });
683        }
684        let comparisons = Cell::new(0_u64);
685        seeds.sort_unstable_by(|left, right| {
686            comparisons.set(comparisons.get().saturating_add(1));
687            left.record.file_id.cmp(&right.record.file_id)
688        });
689        charge_items(&mut work, comparisons.get(), budget)?;
690        let mut records =
691            reserve_exact::<RecordState>(seeds.len(), &mut allocations, &mut work, budget)?;
692        for group in seeds.chunk_by(|left, right| left.record.file_id == right.record.file_id) {
693            let mut base = None;
694            let mut created = None;
695            let mut replacement = None;
696            let mut group_examined = 0_u64;
697            for seed in group {
698                group_examined = group_examined
699                    .checked_add(1)
700                    .ok_or_else(|| failed(WorkError::Overflow.into(), work))?;
701                if seed.created {
702                    if created.replace(seed.record).is_some() {
703                        charge_items(&mut work, group_examined, budget)?;
704                        return Err(failed(
705                            GenerationMutationError::FileIdentityAlreadyExists,
706                            work,
707                        ));
708                    }
709                } else if seed.replacement {
710                    if replacement.is_some_and(|value| value != seed.record) {
711                        charge_items(&mut work, group_examined, budget)?;
712                        return Err(failed(GenerationMutationError::InconsistentState, work));
713                    }
714                    replacement = Some(seed.record);
715                } else if base.is_some_and(|value| value != seed.record) {
716                    charge_items(&mut work, group_examined, budget)?;
717                    return Err(failed(
718                        GenerationMutationError::FileIdentityAlreadyExists,
719                        work,
720                    ));
721                } else {
722                    base = Some(seed.record);
723                }
724            }
725            charge_items(&mut work, group_examined, budget)?;
726            if base.is_some() && created.is_some() {
727                return Err(failed(
728                    GenerationMutationError::FileIdentityAlreadyExists,
729                    work,
730                ));
731            }
732            let working = base
733                .or(created)
734                .or(replacement)
735                .ok_or_else(|| failed(GenerationMutationError::InconsistentState, work))?;
736            records.push(RecordState {
737                base,
738                working,
739                present: base.is_some(),
740            });
741        }
742        let path_comparisons = Cell::new(0_u64);
743        #[allow(
744            clippy::indexing_slicing,
745            reason = "`index` is bounded by the `for index in 0..paths.len()` loop here; each \
746                      iteration first reads `&paths[index]` and computes `find_path_state` from \
747                      the whole (unmutated) `paths` slice, then writes back to that same \
748                      in-bounds `paths[index]` — a manual split of read-all/write-one that \
749                      `iter_mut` cannot express without an extra untracked allocation, which \
750                      this budget-accounted kernel path avoids"
751        )]
752        for index in 0..paths.len() {
753            let path = state_path(plan, &paths[index]);
754            let Some((parent, _)) = path.split_last() else {
755                return Err(failed(GenerationMutationError::InconsistentState, work));
756            };
757            paths[index].parent_state = find_path_state(plan, &paths, parent, &path_comparisons);
758        }
759        charge_items(&mut work, path_comparisons.get(), budget)?;
760        let directory_edits = reserve_exact::<DirectoryEdit>(
761            operation_count
762                .checked_mul(2)
763                .ok_or_else(|| failed(WorkError::Overflow.into(), work))?,
764            &mut allocations,
765            &mut work,
766            budget,
767        )?;
768        let removed_directories =
769            reserve_exact::<FileId>(operation_count, &mut allocations, &mut work, budget)?;
770        let seed_bytes = logical_vec_bytes(&seeds)?;
771        drop(seeds);
772        allocations
773            .release(seed_bytes)
774            .map_err(|error| allocation_failure(error, work))?;
775        drop(lookup);
776        allocations
777            .release(lookup_retained_bytes)
778            .map_err(|error| allocation_failure(error, work))?;
779        drop(identity_records);
780        allocations
781            .release(identity_record_bytes)
782            .map_err(|error| allocation_failure(error, work))?;
783        Ok(Self {
784            paths,
785            operation_paths,
786            records,
787            directory_edits,
788            removed_directories,
789            allocations,
790            work,
791            budget,
792            config,
793        })
794    }
795
796    #[allow(clippy::too_many_lines)]
797    async fn simulate<S: AsyncObjectStore>(
798        &mut self,
799        store: &S,
800        plan: &MutationPlan,
801        config: VolumeConfig,
802        cancellation: &CancellationToken,
803    ) -> Result<(), GenerationMutationFailure> {
804        for (ordinal, operation) in plan.operations().iter().enumerate() {
805            let order = u32::try_from(ordinal)
806                .map_err(|_| failed(GenerationMutationError::InconsistentState, self.work))?;
807            match operation {
808                Mutation::Create { record, .. } => self.create(plan, ordinal, *record, order)?,
809                Mutation::Restore { record, .. } => self.restore(plan, ordinal, *record, order)?,
810                Mutation::Remove {
811                    expected_file_id, ..
812                } => {
813                    let expected = match expected_file_id {
814                        MetadataField::Unavailable => None,
815                        MetadataField::Value(value) => Some(*value),
816                    };
817                    self.remove(plan, ordinal, expected, order)?;
818                }
819                Mutation::Rename {
820                    source,
821                    destination,
822                    replace,
823                } => self.rename(plan, ordinal, source, destination, *replace, order)?,
824                Mutation::Link { .. } => self.link(plan, ordinal, order)?,
825                Mutation::SetMetadata { metadata, .. } => {
826                    let binding = self.source_binding(ordinal)?;
827                    self.record_mut(binding.file_id)?.working.metadata = *metadata;
828                }
829                Mutation::Write {
830                    offset,
831                    length,
832                    content,
833                    content_offset,
834                    ..
835                } => {
836                    self.mutate_regular(
837                        store,
838                        ordinal,
839                        RegularMutation::Write {
840                            offset: *offset,
841                            length: *length,
842                            content: *content,
843                            content_offset: *content_offset,
844                        },
845                        config,
846                        cancellation,
847                    )
848                    .await?;
849                }
850                Mutation::ValidateRegular { .. } => {
851                    let binding = self.source_binding(ordinal)?;
852                    if binding.kind != FileKind::Regular {
853                        return Err(failed(
854                            GenerationMutationError::InconsistentState,
855                            self.work,
856                        ));
857                    }
858                }
859                Mutation::Resize { logical_bytes, .. } => {
860                    self.mutate_regular(
861                        store,
862                        ordinal,
863                        RegularMutation::Resize {
864                            logical_bytes: *logical_bytes,
865                        },
866                        config,
867                        cancellation,
868                    )
869                    .await?;
870                }
871                Mutation::ZeroRange {
872                    offset,
873                    length,
874                    allocated,
875                    extend,
876                    ..
877                } => {
878                    self.mutate_regular(
879                        store,
880                        ordinal,
881                        RegularMutation::ZeroRange {
882                            offset: *offset,
883                            length: *length,
884                            allocated: *allocated,
885                            extend: *extend,
886                        },
887                        config,
888                        cancellation,
889                    )
890                    .await?;
891                }
892                Mutation::Preallocate {
893                    offset,
894                    length,
895                    keep_size,
896                    ..
897                } => {
898                    self.mutate_regular(
899                        store,
900                        ordinal,
901                        RegularMutation::Preallocate {
902                            offset: *offset,
903                            length: *length,
904                            keep_size: *keep_size,
905                        },
906                        config,
907                        cancellation,
908                    )
909                    .await?;
910                }
911                Mutation::CloneRange {
912                    source_offset,
913                    destination_offset,
914                    length,
915                    ..
916                } => {
917                    self.clone_regular(
918                        store,
919                        ordinal,
920                        *source_offset,
921                        *destination_offset,
922                        *length,
923                        config,
924                        cancellation,
925                    )
926                    .await?;
927                }
928                Mutation::File { file_id, mutation } => match mutation {
929                    FileMutation::ReplaceRecord { record } => {
930                        let binding = self.identity_binding(*file_id)?;
931                        if binding.kind != record.kind || binding.kind == FileKind::Directory {
932                            return Err(failed(
933                                GenerationMutationError::InconsistentState,
934                                self.work,
935                            ));
936                        }
937                        let link_count = self.record(*file_id)?.working.link_count;
938                        self.record_mut(*file_id)?.working = FileRecord {
939                            link_count,
940                            ..*record
941                        };
942                    }
943                    FileMutation::SetMetadata { metadata } => {
944                        self.identity_binding(*file_id)?;
945                        self.record_mut(*file_id)?.working.metadata = *metadata;
946                    }
947                    FileMutation::Write {
948                        offset,
949                        length,
950                        content,
951                        content_offset,
952                    } => {
953                        self.mutate_regular_id(
954                            store,
955                            *file_id,
956                            RegularMutation::Write {
957                                offset: *offset,
958                                length: *length,
959                                content: *content,
960                                content_offset: *content_offset,
961                            },
962                            config,
963                            cancellation,
964                        )
965                        .await?;
966                    }
967                    FileMutation::ValidateRegular => {
968                        let binding = self.identity_binding(*file_id)?;
969                        if binding.kind != FileKind::Regular {
970                            return Err(failed(
971                                GenerationMutationError::InconsistentState,
972                                self.work,
973                            ));
974                        }
975                    }
976                    FileMutation::Resize { logical_bytes } => {
977                        self.mutate_regular_id(
978                            store,
979                            *file_id,
980                            RegularMutation::Resize {
981                                logical_bytes: *logical_bytes,
982                            },
983                            config,
984                            cancellation,
985                        )
986                        .await?;
987                    }
988                    FileMutation::ZeroRange {
989                        offset,
990                        length,
991                        allocated,
992                        extend,
993                    } => {
994                        self.mutate_regular_id(
995                            store,
996                            *file_id,
997                            RegularMutation::ZeroRange {
998                                offset: *offset,
999                                length: *length,
1000                                allocated: *allocated,
1001                                extend: *extend,
1002                            },
1003                            config,
1004                            cancellation,
1005                        )
1006                        .await?;
1007                    }
1008                    FileMutation::Preallocate {
1009                        offset,
1010                        length,
1011                        keep_size,
1012                    } => {
1013                        self.mutate_regular_id(
1014                            store,
1015                            *file_id,
1016                            RegularMutation::Preallocate {
1017                                offset: *offset,
1018                                length: *length,
1019                                keep_size: *keep_size,
1020                            },
1021                            config,
1022                            cancellation,
1023                        )
1024                        .await?;
1025                    }
1026                },
1027                Mutation::CloneFileRange {
1028                    source_file_id,
1029                    source_offset,
1030                    destination_file_id,
1031                    destination_offset,
1032                    length,
1033                } => {
1034                    self.clone_regular_ids(
1035                        store,
1036                        *source_file_id,
1037                        *source_offset,
1038                        *destination_file_id,
1039                        *destination_offset,
1040                        *length,
1041                        config,
1042                        cancellation,
1043                    )
1044                    .await?;
1045                }
1046            }
1047        }
1048        Ok(())
1049    }
1050
1051    async fn mutate_regular<S: AsyncObjectStore>(
1052        &mut self,
1053        store: &S,
1054        operation: usize,
1055        mutation: RegularMutation,
1056        config: VolumeConfig,
1057        cancellation: &CancellationToken,
1058    ) -> Result<(), GenerationMutationFailure> {
1059        let binding = self.source_binding(operation)?;
1060        Box::pin(self.mutate_regular_id(store, binding.file_id, mutation, config, cancellation))
1061            .await
1062    }
1063
1064    fn mutate_regular_id<'a, S: AsyncObjectStore>(
1065        &'a mut self,
1066        store: &'a S,
1067        file_id: FileId,
1068        mutation: RegularMutation,
1069        config: VolumeConfig,
1070        cancellation: &'a CancellationToken,
1071    ) -> BoxStorageFuture<'a, Result<(), GenerationMutationFailure>> {
1072        Box::pin(async move {
1073            let binding = self.identity_binding(file_id)?;
1074            if binding.kind != FileKind::Regular {
1075                return Err(failed(
1076                    GenerationMutationError::InconsistentState,
1077                    self.work,
1078                ));
1079            }
1080            let payload = self.record(file_id)?.working.payload;
1081            let receipt = apply_regular_mutation_async(
1082                store,
1083                payload,
1084                mutation,
1085                config,
1086                nested_budget(self.work, self.budget, self.allocations.live_bytes())?,
1087                cancellation,
1088            )
1089            .await
1090            .map_err(|failure| {
1091                nested_failure(
1092                    self.work,
1093                    *failure.work,
1094                    self.allocations.live_bytes(),
1095                    failure.error.into(),
1096                )
1097            })?;
1098            self.work = merge_nested(
1099                self.work,
1100                receipt.work,
1101                self.allocations.live_bytes(),
1102                self.budget,
1103            )?;
1104            self.record_mut(file_id)?.working.payload = receipt.payload;
1105            Ok(())
1106        })
1107    }
1108
1109    #[allow(clippy::too_many_arguments)]
1110    async fn clone_regular<S: AsyncObjectStore>(
1111        &mut self,
1112        store: &S,
1113        operation: usize,
1114        source_offset: u64,
1115        destination_offset: u64,
1116        length: u64,
1117        config: VolumeConfig,
1118        cancellation: &CancellationToken,
1119    ) -> Result<(), GenerationMutationFailure> {
1120        let source = self.binding(operation, 0)?;
1121        let destination = self.binding(operation, 1)?;
1122        self.clone_regular_ids(
1123            store,
1124            source.file_id,
1125            source_offset,
1126            destination.file_id,
1127            destination_offset,
1128            length,
1129            config,
1130            cancellation,
1131        )
1132        .await
1133    }
1134
1135    #[allow(clippy::too_many_arguments)]
1136    async fn clone_regular_ids<S: AsyncObjectStore>(
1137        &mut self,
1138        store: &S,
1139        source_file_id: FileId,
1140        source_offset: u64,
1141        destination_file_id: FileId,
1142        destination_offset: u64,
1143        length: u64,
1144        config: VolumeConfig,
1145        cancellation: &CancellationToken,
1146    ) -> Result<(), GenerationMutationFailure> {
1147        let source = self.identity_binding(source_file_id)?;
1148        let destination = self.identity_binding(destination_file_id)?;
1149        if source.kind != FileKind::Regular || destination.kind != FileKind::Regular {
1150            return Err(failed(
1151                GenerationMutationError::InconsistentState,
1152                self.work,
1153            ));
1154        }
1155        let source_payload = self.record(source_file_id)?.working.payload;
1156        let destination_payload = self.record(destination_file_id)?.working.payload;
1157        let receipt = apply_regular_clone_async(
1158            store,
1159            source_payload,
1160            source_offset,
1161            destination_payload,
1162            destination_offset,
1163            length,
1164            config,
1165            nested_budget(self.work, self.budget, self.allocations.live_bytes())?,
1166            cancellation,
1167        )
1168        .await
1169        .map_err(|failure| {
1170            nested_failure(
1171                self.work,
1172                *failure.work,
1173                self.allocations.live_bytes(),
1174                failure.error.into(),
1175            )
1176        })?;
1177        self.work = merge_nested(
1178            self.work,
1179            receipt.work,
1180            self.allocations.live_bytes(),
1181            self.budget,
1182        )?;
1183        self.record_mut(destination_file_id)?.working.payload = receipt.destination;
1184        Ok(())
1185    }
1186
1187    #[allow(
1188        clippy::indexing_slicing,
1189        reason = "`operation` is always the enumerated ordinal from `simulate`'s \
1190                  `for (ordinal, operation) in plan.operations().iter().enumerate()`, so \
1191                  `operation < plan.operations().len() == self.operation_paths.len()`; this \
1192                  method is only reached for `Mutation::Create`, whose `paths()` always returns \
1193                  `Some`, so `TransactionState::new`'s construction loop already overwrote \
1194                  `operation_paths[operation]` away from its `[usize::MAX; 2]` resize sentinel \
1195                  with a valid index into `self.paths`"
1196    )]
1197    fn create(
1198        &mut self,
1199        plan: &MutationPlan,
1200        operation: usize,
1201        record: FileRecord,
1202        order: u32,
1203    ) -> Result<(), GenerationMutationFailure> {
1204        let state = self.operation_paths[operation][0];
1205        if self.paths[state].binding.is_some() {
1206            return Err(failed(GenerationMutationError::AlreadyExists, self.work));
1207        }
1208        let directory_id = self.parent_directory(state)?;
1209        let record_state = self.record_mut(record.file_id)?;
1210        if record_state.base.is_some() || record_state.present {
1211            return Err(failed(
1212                GenerationMutationError::FileIdentityAlreadyExists,
1213                self.work,
1214            ));
1215        }
1216        record_state.working = record;
1217        record_state.present = true;
1218        self.paths[state].binding = Some(Binding {
1219            file_id: record.file_id,
1220            kind: record.kind,
1221        });
1222        let (name, retained_name_bytes) = self.clone_terminal_name(plan, operation, 0)?;
1223        self.push_edit(
1224            directory_id,
1225            order,
1226            retained_name_bytes,
1227            TreeMutation::Insert(TreeEntry {
1228                name,
1229                file_id: record.file_id,
1230                kind: record.kind,
1231            }),
1232        );
1233        Ok(())
1234    }
1235
1236    /// Restores one path from an immutable generation without flattening the
1237    /// live hard-link graph. Directory payloads are retained when the
1238    /// destination is already a directory: Git-shaped history tracks the
1239    /// children individually and must not erase live ignored descendants.
1240    fn restore(
1241        &mut self,
1242        plan: &MutationPlan,
1243        operation: usize,
1244        source: FileRecord,
1245        order: u32,
1246    ) -> Result<(), GenerationMutationFailure> {
1247        #[allow(
1248            clippy::indexing_slicing,
1249            reason = "operation paths are preflighted and indexed by the enumerated mutation ordinal"
1250        )]
1251        let state = self.operation_paths[operation][0];
1252        #[allow(
1253            clippy::indexing_slicing,
1254            reason = "operation path state indices are constructed from the same path table"
1255        )]
1256        let current = self.paths[state].binding;
1257        if let Some(current) = current {
1258            if current.kind == FileKind::Directory && source.kind == FileKind::Directory {
1259                self.record_mut(current.file_id)?.working.metadata = source.metadata;
1260                return Ok(());
1261            }
1262            if current.file_id == source.file_id {
1263                let link_count = self.record(source.file_id)?.working.link_count;
1264                self.record_mut(source.file_id)?.working = FileRecord {
1265                    link_count,
1266                    ..source
1267                };
1268                return Ok(());
1269            }
1270        }
1271
1272        let directory_id = self.parent_directory(state)?;
1273        let (name, retained_name_bytes) = self.clone_terminal_name(plan, operation, 0)?;
1274        let entry = TreeEntry {
1275            name,
1276            file_id: source.file_id,
1277            kind: source.kind,
1278        };
1279        self.push_edit(
1280            directory_id,
1281            order,
1282            retained_name_bytes,
1283            match current {
1284                Some(existing) => TreeMutation::Replace {
1285                    entry,
1286                    expected_file_id: existing.file_id,
1287                },
1288                None => TreeMutation::Insert(entry),
1289            },
1290        );
1291        if let Some(existing) = current {
1292            self.drop_link(existing)?;
1293        }
1294        let current_work = self.work;
1295        let desired = self.record_mut(source.file_id)?;
1296        if desired.present {
1297            if source.kind == FileKind::Directory {
1298                return Err(failed(
1299                    GenerationMutationError::UnsupportedHardLink,
1300                    self.work,
1301                ));
1302            }
1303            desired.working = FileRecord {
1304                link_count: desired
1305                    .working
1306                    .link_count
1307                    .checked_add(1)
1308                    .ok_or_else(|| failed(WorkError::Overflow.into(), current_work))?,
1309                ..source
1310            };
1311        } else {
1312            desired.working = FileRecord {
1313                link_count: 1,
1314                ..source
1315            };
1316            desired.present = true;
1317        }
1318        #[allow(
1319            clippy::indexing_slicing,
1320            reason = "operation path state indices are constructed from the same path table"
1321        )]
1322        let path_state = &mut self.paths[state];
1323        path_state.binding = Some(Binding {
1324            file_id: source.file_id,
1325            kind: source.kind,
1326        });
1327        Ok(())
1328    }
1329
1330    #[allow(
1331        clippy::indexing_slicing,
1332        reason = "`operation` is always the enumerated ordinal from `simulate`'s \
1333                  `for (ordinal, operation) in plan.operations().iter().enumerate()`, so \
1334                  `operation < plan.operations().len() == self.operation_paths.len()`; this \
1335                  method is only reached for `Mutation::Remove`, whose `paths()` always returns \
1336                  `Some`, so `TransactionState::new`'s construction loop already overwrote \
1337                  `operation_paths[operation]` away from its `[usize::MAX; 2]` resize sentinel \
1338                  with a valid index into `self.paths`"
1339    )]
1340    fn remove(
1341        &mut self,
1342        plan: &MutationPlan,
1343        operation: usize,
1344        expected: Option<FileId>,
1345        order: u32,
1346    ) -> Result<(), GenerationMutationFailure> {
1347        let state = self.operation_paths[operation][0];
1348        let binding = self.paths[state]
1349            .binding
1350            .ok_or_else(|| failed(GenerationMutationError::MissingSource, self.work))?;
1351        if expected.is_some_and(|value| value != binding.file_id) {
1352            return Err(failed(
1353                GenerationMutationError::FileIdentityConflict,
1354                self.work,
1355            ));
1356        }
1357        let directory_id = self.parent_directory(state)?;
1358        let (name, retained_name_bytes) = self.clone_terminal_name(plan, operation, 0)?;
1359        self.push_edit(
1360            directory_id,
1361            order,
1362            retained_name_bytes,
1363            TreeMutation::Remove {
1364                name,
1365                expected_file_id: expected,
1366            },
1367        );
1368        self.paths[state].binding = None;
1369        self.drop_link(binding)
1370    }
1371
1372    #[allow(
1373        clippy::indexing_slicing,
1374        reason = "`operation` is always the enumerated ordinal from `simulate`'s \
1375                  `for (ordinal, operation) in plan.operations().iter().enumerate()`, so \
1376                  `operation < plan.operations().len() == self.operation_paths.len()`; this \
1377                  method is only reached for `Mutation::Rename`, whose `paths()` always returns \
1378                  `Some` for both endpoints, so `TransactionState::new`'s construction loop \
1379                  already overwrote `operation_paths[operation]` away from its \
1380                  `[usize::MAX; 2]` resize sentinel with valid indices into `self.paths`"
1381    )]
1382    fn rename(
1383        &mut self,
1384        plan: &MutationPlan,
1385        operation: usize,
1386        source_path: &NamespacePath,
1387        destination_path: &NamespacePath,
1388        replace: bool,
1389        order: u32,
1390    ) -> Result<(), GenerationMutationFailure> {
1391        let source_state = self.operation_paths[operation][0];
1392        let destination_state = self.operation_paths[operation][1];
1393        let source = self.paths[source_state]
1394            .binding
1395            .ok_or_else(|| failed(GenerationMutationError::MissingSource, self.work))?;
1396        if source.kind == FileKind::Directory && destination_path.is_within(source_path) {
1397            return Err(failed(GenerationMutationError::DirectoryCycle, self.work));
1398        }
1399        let destination = self.paths[destination_state].binding;
1400        // POSIX and Windows both define renaming one hard-link alias over
1401        // another alias of the same file as a successful no-op. In
1402        // particular, it must not remove a name or decrement the shared link
1403        // count.
1404        if destination.is_some_and(|existing| existing.file_id == source.file_id) {
1405            return Ok(());
1406        }
1407        if destination.is_some() && !replace {
1408            return Err(failed(GenerationMutationError::AlreadyExists, self.work));
1409        }
1410        let source_parent = self.parent_directory(source_state)?;
1411        let destination_parent = self.parent_directory(destination_state)?;
1412        let (source_name, source_name_bytes) = self.clone_terminal_name(plan, operation, 0)?;
1413        self.push_edit(
1414            source_parent,
1415            order,
1416            source_name_bytes,
1417            TreeMutation::Remove {
1418                name: source_name,
1419                expected_file_id: Some(source.file_id),
1420            },
1421        );
1422        let (destination_name, destination_name_bytes) =
1423            self.clone_terminal_name(plan, operation, 1)?;
1424        let entry = TreeEntry {
1425            name: destination_name,
1426            file_id: source.file_id,
1427            kind: source.kind,
1428        };
1429        self.push_edit(
1430            destination_parent,
1431            order,
1432            destination_name_bytes,
1433            match destination {
1434                Some(existing) => TreeMutation::Replace {
1435                    entry,
1436                    expected_file_id: existing.file_id,
1437                },
1438                None => TreeMutation::Insert(entry),
1439            },
1440        );
1441        if let Some(existing) = destination {
1442            self.drop_link(existing)?;
1443        }
1444        self.paths[source_state].binding = None;
1445        self.paths[destination_state].binding = Some(source);
1446        Ok(())
1447    }
1448
1449    #[allow(
1450        clippy::indexing_slicing,
1451        reason = "`operation` is always the enumerated ordinal from `simulate`'s \
1452                  `for (ordinal, operation) in plan.operations().iter().enumerate()`, so \
1453                  `operation < plan.operations().len() == self.operation_paths.len()`; this \
1454                  method is only reached for `Mutation::Link`, whose `paths()` always returns \
1455                  `Some` for both endpoints, so `TransactionState::new`'s construction loop \
1456                  already overwrote `operation_paths[operation]` away from its \
1457                  `[usize::MAX; 2]` resize sentinel with valid indices into `self.paths`"
1458    )]
1459    fn link(
1460        &mut self,
1461        plan: &MutationPlan,
1462        operation: usize,
1463        order: u32,
1464    ) -> Result<(), GenerationMutationFailure> {
1465        let source = self.source_binding(operation)?;
1466        if source.kind == FileKind::Directory {
1467            return Err(failed(
1468                GenerationMutationError::UnsupportedHardLink,
1469                self.work,
1470            ));
1471        }
1472        let destination_state = self.operation_paths[operation][1];
1473        if self.paths[destination_state].binding.is_some() {
1474            return Err(failed(GenerationMutationError::AlreadyExists, self.work));
1475        }
1476        let destination_parent = self.parent_directory(destination_state)?;
1477        let current_links = self.record(source.file_id)?.working.link_count;
1478        let next_links = current_links
1479            .checked_add(1)
1480            .ok_or_else(|| failed(WorkError::Overflow.into(), self.work))?;
1481        self.record_mut(source.file_id)?.working.link_count = next_links;
1482        self.paths[destination_state].binding = Some(source);
1483        let (name, retained_name_bytes) = self.clone_terminal_name(plan, operation, 1)?;
1484        self.push_edit(
1485            destination_parent,
1486            order,
1487            retained_name_bytes,
1488            TreeMutation::Insert(TreeEntry {
1489                name,
1490                file_id: source.file_id,
1491                kind: source.kind,
1492            }),
1493        );
1494        Ok(())
1495    }
1496
1497    fn source_binding(&self, operation: usize) -> Result<Binding, GenerationMutationFailure> {
1498        self.binding(operation, 0)
1499    }
1500
1501    #[allow(
1502        clippy::indexing_slicing,
1503        reason = "`operation` is always the enumerated ordinal from `simulate`'s \
1504                  `for (ordinal, operation) in plan.operations().iter().enumerate()`, so \
1505                  `operation < plan.operations().len() == self.operation_paths.len()`; every \
1506                  caller of `binding`/`source_binding` only does so for a `Mutation` variant \
1507                  whose `paths()` returns `Some` for the requested `endpoint` (0 or 1), so \
1508                  `TransactionState::new`'s construction loop already overwrote \
1509                  `operation_paths[operation][endpoint]` away from its `[usize::MAX; 2]` resize \
1510                  sentinel with a valid index into `self.paths`"
1511    )]
1512    fn binding(
1513        &self,
1514        operation: usize,
1515        endpoint: usize,
1516    ) -> Result<Binding, GenerationMutationFailure> {
1517        self.paths[self.operation_paths[operation][endpoint]]
1518            .binding
1519            .ok_or_else(|| failed(GenerationMutationError::MissingSource, self.work))
1520    }
1521
1522    #[allow(
1523        clippy::indexing_slicing,
1524        reason = "`state` is always either an `operation_paths[operation][endpoint]` value \
1525                  (see `binding`'s invariant above: a valid index into `self.paths`) or a \
1526                  `PathState.parent_state`/`find_path_state` result, which is produced only by \
1527                  `states.binary_search_by(...).ok()` over the very same `self.paths` slice \
1528                  (`find_path_state`, below), so any `Some(parent_state)` it returns is also a \
1529                  valid index into `self.paths`"
1530    )]
1531    fn parent_directory(&mut self, state: usize) -> Result<FileId, GenerationMutationFailure> {
1532        let file_id = match self.paths[state].parent_state {
1533            Some(parent_state) => {
1534                self.paths[parent_state]
1535                    .binding
1536                    .ok_or_else(|| failed(GenerationMutationError::MissingParent, self.work))?
1537                    .file_id
1538            }
1539            None if self.paths[state].base_parent_exact => self.paths[state]
1540                .base_parent
1541                .ok_or_else(|| failed(GenerationMutationError::MissingParent, self.work))?,
1542            None => return Err(failed(GenerationMutationError::MissingParent, self.work)),
1543        };
1544        let record = self.record(file_id)?;
1545        if record.working.kind != FileKind::Directory || !record.present {
1546            return Err(failed(GenerationMutationError::MissingParent, self.work));
1547        }
1548        Ok(file_id)
1549    }
1550
1551    fn drop_link(&mut self, binding: Binding) -> Result<(), GenerationMutationFailure> {
1552        let current_links = self.record(binding.file_id)?.working.link_count;
1553        if current_links == 0 {
1554            return Err(failed(
1555                GenerationMutationError::InconsistentState,
1556                self.work,
1557            ));
1558        }
1559        let next_links = current_links - 1;
1560        let record = self.record_mut(binding.file_id)?;
1561        record.working.link_count = next_links;
1562        record.present = next_links != 0;
1563        if binding.kind == FileKind::Directory {
1564            self.removed_directories.push(binding.file_id);
1565        }
1566        Ok(())
1567    }
1568
1569    fn clone_terminal_name(
1570        &mut self,
1571        plan: &MutationPlan,
1572        operation: usize,
1573        endpoint: usize,
1574    ) -> Result<(LogicalName, u64), GenerationMutationFailure> {
1575        let path = Self::operation_path(plan, operation, endpoint);
1576        let name = path
1577            .split_last()
1578            .map(|(_, name)| name)
1579            .ok_or_else(|| failed(GenerationMutationError::InconsistentState, self.work))?;
1580        let bytes = u64::try_from(name.as_bytes().len()).unwrap_or(u64::MAX);
1581        self.allocations
1582            .claim_bytes(bytes, 1, &mut self.work, self.budget)
1583            .map_err(|error| allocation_failure(error, self.work))?;
1584        let admitted = self.work;
1585        let mut copied = Vec::new();
1586        if copied.try_reserve_exact(name.as_bytes().len()).is_err() {
1587            self.allocations
1588                .release(bytes)
1589                .map_err(|error| allocation_failure(error, self.work))?;
1590            return Err(failed(GenerationMutationError::AllocationFailed, admitted));
1591        }
1592        let copied_work = self
1593            .work
1594            .checked_add(WorkCounters {
1595                bytes_copied: bytes,
1596                ..WorkCounters::default()
1597            })
1598            .map_err(|error| failed(error.into(), self.work))?;
1599        copied_work
1600            .verify(self.budget)
1601            .map_err(|error| failed(error.into(), self.work))?;
1602        copied.extend_from_slice(name.as_bytes());
1603        self.work = copied_work;
1604        let name = LogicalName::new(
1605            name.encoding(),
1606            copied,
1607            self.config.limits.maximum_component_bytes,
1608        )
1609        .map_err(|_| failed(GenerationMutationError::InconsistentState, self.work))?;
1610        Ok((name, bytes))
1611    }
1612
1613    // Every caller obtains the operation from a planner-owned `PathState`,
1614    // which can only be constructed for a path-bearing mutation.
1615    #[allow(clippy::expect_used)]
1616    #[allow(
1617        clippy::indexing_slicing,
1618        reason = "every caller reaches this only via `clone_terminal_name`, whose `operation` \
1619                  is always the `simulate`-enumerated ordinal `< plan.operations().len()` and \
1620                  whose `endpoint` argument is always the literal `0` or `1` (see the \
1621                  `clone_terminal_name` call sites in `create`/`remove`/`rename`/`link`), and \
1622                  `plan.operations()[operation].paths()` is `Some` for exactly those callers, \
1623                  so both index operations are in-bounds by construction"
1624    )]
1625    fn operation_path(plan: &MutationPlan, operation: usize, endpoint: usize) -> &NamespacePath {
1626        let paths = plan.operations()[operation]
1627            .paths()
1628            .expect("path operation must retain endpoints");
1629        paths[endpoint]
1630    }
1631
1632    fn push_edit(
1633        &mut self,
1634        directory_id: FileId,
1635        order: u32,
1636        retained_name_bytes: u64,
1637        mutation: TreeMutation,
1638    ) {
1639        self.directory_edits.push(DirectoryEdit {
1640            directory_id,
1641            order,
1642            retained_name_bytes,
1643            mutation: Some(mutation),
1644        });
1645    }
1646
1647    #[allow(
1648        clippy::indexing_slicing,
1649        reason = "`record_index` returns `Ok(index)` only from a successful \
1650                  `self.records.binary_search_by(...)` over this same `self.records` slice \
1651                  (see `record_index` below), which guarantees `index < self.records.len()`, \
1652                  and nothing resizes `self.records` between that call and this indexing"
1653    )]
1654    fn record(&mut self, file_id: FileId) -> Result<&RecordState, GenerationMutationFailure> {
1655        let index = self.record_index(file_id)?;
1656        Ok(&self.records[index])
1657    }
1658
1659    fn identity_binding(&mut self, file_id: FileId) -> Result<Binding, GenerationMutationFailure> {
1660        let record = self.record(file_id)?;
1661        if !record.present || record.working.link_count == 0 {
1662            return Err(failed(GenerationMutationError::MissingSource, self.work));
1663        }
1664        Ok(Binding {
1665            file_id,
1666            kind: record.working.kind,
1667        })
1668    }
1669
1670    #[allow(
1671        clippy::indexing_slicing,
1672        reason = "`record_index` returns `Ok(index)` only from a successful \
1673                  `self.records.binary_search_by(...)` over this same `self.records` slice \
1674                  (see `record_index` below), which guarantees `index < self.records.len()`, \
1675                  and nothing resizes `self.records` between that call and this indexing"
1676    )]
1677    fn record_mut(
1678        &mut self,
1679        file_id: FileId,
1680    ) -> Result<&mut RecordState, GenerationMutationFailure> {
1681        let index = self.record_index(file_id)?;
1682        Ok(&mut self.records[index])
1683    }
1684
1685    fn record_index(&mut self, file_id: FileId) -> Result<usize, GenerationMutationFailure> {
1686        let comparisons = Cell::new(0_u64);
1687        let result = self.records.binary_search_by(|record| {
1688            comparisons.set(comparisons.get().saturating_add(1));
1689            record.working.file_id.cmp(&file_id)
1690        });
1691        charge_items(&mut self.work, comparisons.get(), self.budget)?;
1692        result.map_err(|_| failed(GenerationMutationError::InconsistentState, self.work))
1693    }
1694
1695    #[allow(
1696        clippy::indexing_slicing,
1697        reason = "`cursor` is only ever read after a `cursor < self.directory_edits.len()` \
1698                  guard (the outer `while` here, and the inner `while` just below), so both \
1699                  point reads of `self.directory_edits[cursor]` are in-bounds; `start` is the \
1700                  `cursor` value captured before either loop advances it further, so \
1701                  `start <= cursor <= self.directory_edits.len()` always holds, which is exactly \
1702                  what makes `self.directory_edits[start..cursor]` a valid range"
1703    )]
1704    async fn rewrite_directories<S: AsyncObjectStore>(
1705        &mut self,
1706        store: &S,
1707        config: VolumeConfig,
1708        cancellation: &CancellationToken,
1709    ) -> Result<(), GenerationMutationFailure> {
1710        let comparisons = Cell::new(0_u64);
1711        self.directory_edits.sort_unstable_by(|left, right| {
1712            comparisons.set(comparisons.get().saturating_add(1));
1713            left.directory_id
1714                .cmp(&right.directory_id)
1715                .then_with(|| left.order.cmp(&right.order))
1716        });
1717        charge_items(&mut self.work, comparisons.get(), self.budget)?;
1718        let mut cursor = 0;
1719        while cursor < self.directory_edits.len() {
1720            cancellation
1721                .check()
1722                .map_err(|_| failed(path_cancelled(), self.work))?;
1723            let directory_id = self.directory_edits[cursor].directory_id;
1724            let start = cursor;
1725            cursor += 1;
1726            while cursor < self.directory_edits.len()
1727                && self.directory_edits[cursor].directory_id == directory_id
1728            {
1729                cursor += 1;
1730            }
1731            let mut mutations = reserve_exact::<TreeMutation>(
1732                cursor - start,
1733                &mut self.allocations,
1734                &mut self.work,
1735                self.budget,
1736            )?;
1737            let mutation_bytes = logical_vec_bytes(&mutations)?;
1738            let retained_name_bytes =
1739                self.directory_edits[start..cursor]
1740                    .iter()
1741                    .try_fold(0_u64, |total, edit| {
1742                        total
1743                            .checked_add(edit.retained_name_bytes)
1744                            .ok_or_else(|| failed(WorkError::Overflow.into(), self.work))
1745                    })?;
1746            for edit in &mut self.directory_edits[start..cursor] {
1747                mutations.push(edit.mutation.take().ok_or_else(|| {
1748                    failed(GenerationMutationError::InconsistentState, self.work)
1749                })?);
1750            }
1751            let directory = self.record(directory_id)?.working;
1752            let FilePayload::Directory { entries } = directory.payload else {
1753                return Err(failed(GenerationMutationError::MissingParent, self.work));
1754            };
1755            let receipt = apply_tree_mutations_async(
1756                store,
1757                entries,
1758                mutations,
1759                config.limits.maximum_mutations_per_batch,
1760                decode_limits(config),
1761                nested_budget(self.work, self.budget, self.allocations.live_bytes())?,
1762                cancellation,
1763            )
1764            .await
1765            .map_err(|failure| {
1766                nested_failure(
1767                    self.work,
1768                    *failure.work,
1769                    self.allocations.live_bytes(),
1770                    failure.error.into(),
1771                )
1772            })?;
1773            self.work = merge_nested(
1774                self.work,
1775                receipt.work,
1776                self.allocations.live_bytes(),
1777                self.budget,
1778            )?;
1779            self.allocations
1780                .release(
1781                    mutation_bytes
1782                        .checked_add(retained_name_bytes)
1783                        .ok_or_else(|| failed(WorkError::Overflow.into(), self.work))?,
1784                )
1785                .map_err(|error| allocation_failure(error, self.work))?;
1786            self.record_mut(directory_id)?.working.payload = FilePayload::Directory {
1787                entries: receipt.root,
1788            };
1789        }
1790        Ok(())
1791    }
1792
1793    async fn verify_removed_directories<S: AsyncObjectStore>(
1794        &mut self,
1795        store: &S,
1796        config: VolumeConfig,
1797        cancellation: &CancellationToken,
1798    ) -> Result<(), GenerationMutationFailure> {
1799        let comparisons = Cell::new(0_u64);
1800        self.removed_directories.sort_unstable_by(|left, right| {
1801            comparisons.set(comparisons.get().saturating_add(1));
1802            left.cmp(right)
1803        });
1804        charge_items(&mut self.work, comparisons.get(), self.budget)?;
1805        charge_items(
1806            &mut self.work,
1807            u64::try_from(self.removed_directories.len().saturating_sub(1)).unwrap_or(u64::MAX),
1808            self.budget,
1809        )?;
1810        self.removed_directories.dedup();
1811        #[allow(
1812            clippy::indexing_slicing,
1813            reason = "`index` is bounded by the `for index in 0..self.removed_directories.len()` \
1814                      loop here; a by-value `for file_id in &self.removed_directories` iterator \
1815                      would hold an immutable borrow of `self` across the `self.record(file_id)` \
1816                      call below, which needs `&mut self`, so the index form is required to \
1817                      satisfy the borrow checker without cloning this ledger-tracked vector"
1818        )]
1819        for index in 0..self.removed_directories.len() {
1820            let file_id = self.removed_directories[index];
1821            let record = self.record(file_id)?.working;
1822            let FilePayload::Directory { entries } = record.payload else {
1823                return Err(failed(
1824                    GenerationMutationError::InconsistentState,
1825                    self.work,
1826                ));
1827            };
1828            let listing = list_tree_entries_async(
1829                store,
1830                entries,
1831                None,
1832                1,
1833                decode_limits(config),
1834                nested_budget(self.work, self.budget, self.allocations.live_bytes())?,
1835                cancellation,
1836            )
1837            .await
1838            .map_err(|failure| {
1839                nested_failure(
1840                    self.work,
1841                    *failure.work,
1842                    self.allocations.live_bytes(),
1843                    failure.error.into(),
1844                )
1845            })?;
1846            self.work = merge_nested(
1847                self.work,
1848                listing.work,
1849                self.allocations.live_bytes(),
1850                self.budget,
1851            )?;
1852            if !listing.entries.is_empty() {
1853                return Err(failed(
1854                    GenerationMutationError::DirectoryNotEmpty,
1855                    self.work,
1856                ));
1857            }
1858        }
1859        Ok(())
1860    }
1861
1862    async fn rewrite_file_table<S: AsyncObjectStore>(
1863        mut self,
1864        store: &S,
1865        generation: &GenerationRoot,
1866        config: VolumeConfig,
1867        cancellation: &CancellationToken,
1868    ) -> Result<GenerationMutationReceipt, GenerationMutationFailure> {
1869        let mutation_count = self
1870            .records
1871            .iter()
1872            .filter(|record| {
1873                matches!(
1874                    (record.base, record.present),
1875                    (None, true) | (Some(_), false)
1876                ) || matches!(
1877                    (record.base, record.present),
1878                    (Some(expected), true) if expected != record.working
1879                )
1880            })
1881            .count();
1882        charge_items(
1883            &mut self.work,
1884            u64::try_from(self.records.len()).unwrap_or(u64::MAX),
1885            self.budget,
1886        )?;
1887        if mutation_count == 0 {
1888            let root = self.clone_generation_root(generation, generation.file_table)?;
1889            return Ok(GenerationMutationReceipt {
1890                root,
1891                work: self.work,
1892            });
1893        }
1894        let mut mutations = reserve_exact::<FileTableMutation>(
1895            mutation_count,
1896            &mut self.allocations,
1897            &mut self.work,
1898            self.budget,
1899        )?;
1900        let mutation_bytes = logical_vec_bytes(&mutations)?;
1901        for record in &self.records {
1902            match (record.base, record.present) {
1903                (None, true) => mutations.push(FileTableMutation::Insert(record.working)),
1904                (Some(expected), false) => mutations.push(FileTableMutation::Remove {
1905                    file_id: expected.file_id,
1906                    expected: Some(expected),
1907                }),
1908                (Some(expected), true) if expected != record.working => {
1909                    mutations.push(FileTableMutation::Replace {
1910                        expected,
1911                        replacement: record.working,
1912                    });
1913                }
1914                (None, false) | (Some(_), true) => {}
1915            }
1916        }
1917        let receipt = apply_file_table_mutations_async(
1918            store,
1919            generation.file_table,
1920            mutations,
1921            u32::try_from(mutation_count)
1922                .map_err(|_| failed(WorkError::Overflow.into(), self.work))?,
1923            decode_limits(config),
1924            nested_budget(self.work, self.budget, self.allocations.live_bytes())?,
1925            cancellation,
1926        )
1927        .await
1928        .map_err(|failure| {
1929            nested_failure(
1930                self.work,
1931                *failure.work,
1932                self.allocations.live_bytes(),
1933                failure.error.into(),
1934            )
1935        })?;
1936        self.work = merge_nested(
1937            self.work,
1938            receipt.work,
1939            self.allocations.live_bytes(),
1940            self.budget,
1941        )?;
1942        self.allocations
1943            .release(mutation_bytes)
1944            .map_err(|error| allocation_failure(error, self.work))?;
1945        let root = self.clone_generation_root(generation, receipt.root)?;
1946        Ok(GenerationMutationReceipt {
1947            root,
1948            work: self.work,
1949        })
1950    }
1951
1952    fn clone_generation_root(
1953        &mut self,
1954        generation: &GenerationRoot,
1955        file_table: ObjectId,
1956    ) -> Result<GenerationRoot, GenerationMutationFailure> {
1957        let mut parents = reserve_exact::<GenerationId>(
1958            generation.parents.len(),
1959            &mut self.allocations,
1960            &mut self.work,
1961            self.budget,
1962        )?;
1963        let parent_bytes = logical_vec_bytes(&parents)?;
1964        let prospective = self
1965            .work
1966            .checked_add(WorkCounters {
1967                bytes_copied: parent_bytes,
1968                ..WorkCounters::default()
1969            })
1970            .map_err(|error| failed(error.into(), self.work))?;
1971        prospective
1972            .verify(self.budget)
1973            .map_err(|error| failed(error.into(), self.work))?;
1974        parents.extend_from_slice(&generation.parents);
1975        self.work = prospective;
1976        Ok(GenerationRoot {
1977            volume_id: generation.volume_id,
1978            root_file_id: generation.root_file_id,
1979            file_table,
1980            parents,
1981            required_features: generation.required_features,
1982        })
1983    }
1984}
1985
1986fn operation_uses_one_path(mutation: &Mutation) -> bool {
1987    mutation
1988        .paths()
1989        .is_some_and(|[first, second]| first == second)
1990}
1991
1992fn preflight_generation_mutations(
1993    operations: &[Mutation],
1994    config: VolumeConfig,
1995) -> Result<(), GenerationMutationFailure> {
1996    config
1997        .validate()
1998        .map_err(|error| OperationFailure::before_work(error.into()))?;
1999    let mut endpoints = 0_u32;
2000    for operation in operations {
2001        if let Some([first, second]) = operation.paths() {
2002            endpoints = endpoints
2003                .checked_add(if first == second { 1 } else { 2 })
2004                .ok_or_else(|| {
2005                    OperationFailure::before_work(GenerationMutationError::TooManyPaths)
2006                })?;
2007        }
2008        match operation {
2009            Mutation::Create { record, .. } | Mutation::Restore { record, .. } => {
2010                if !record.kind.is_supported_by_profile(config.profile) {
2011                    return Err(OperationFailure::before_work(
2012                        GenerationMutationError::UnsupportedFileKind,
2013                    ));
2014                }
2015                if matches!(operation, Mutation::Create { .. }) && record.link_count != 1 {
2016                    return Err(OperationFailure::before_work(
2017                        GenerationMutationError::InvalidInitialLinkCount,
2018                    ));
2019                }
2020                if record.kind == FileKind::SymbolicLink && !config.symbolic_links {
2021                    return Err(OperationFailure::before_work(
2022                        GenerationMutationError::UnsupportedSymbolicLink,
2023                    ));
2024                }
2025            }
2026            Mutation::Link { .. } if !config.hard_links => {
2027                return Err(OperationFailure::before_work(
2028                    GenerationMutationError::UnsupportedHardLink,
2029                ));
2030            }
2031            Mutation::ZeroRange { .. }
2032            | Mutation::Preallocate { .. }
2033            | Mutation::CloneRange { .. }
2034            | Mutation::File {
2035                mutation: FileMutation::ZeroRange { .. } | FileMutation::Preallocate { .. },
2036                ..
2037            }
2038            | Mutation::CloneFileRange { .. }
2039                if !config.sparse_files =>
2040            {
2041                return Err(OperationFailure::before_work(
2042                    GenerationMutationError::UnsupportedSparseMutation,
2043                ));
2044            }
2045            Mutation::Remove { .. }
2046            | Mutation::Rename { .. }
2047            | Mutation::Link { .. }
2048            | Mutation::SetMetadata { .. }
2049            | Mutation::Write { .. }
2050            | Mutation::ValidateRegular { .. }
2051            | Mutation::Resize { .. }
2052            | Mutation::ZeroRange { .. }
2053            | Mutation::Preallocate { .. }
2054            | Mutation::CloneRange { .. }
2055            | Mutation::File { .. }
2056            | Mutation::CloneFileRange { .. } => {}
2057        }
2058    }
2059    if endpoints > config.limits.maximum_paths_per_batch {
2060        return Err(OperationFailure::before_work(
2061            GenerationMutationError::TooManyPaths,
2062        ));
2063    }
2064    Ok(())
2065}
2066
2067// `PathState` is private and exists only for planner-validated path uses.
2068#[allow(clippy::expect_used)]
2069#[allow(
2070    clippy::indexing_slicing,
2071    reason = "every `PathState` is built in `TransactionState::new` from a \
2072              `plan.ordered_operation_paths()` tuple, whose `operation` and `endpoint` fields \
2073              are always `< plan.operations().len()` and always `0` or `1` respectively (see \
2074              `MutationPlan::ordered_operation_paths`/`compile` in mutation.rs), and only for \
2075              operations whose `paths()` returns `Some`, so both index operations here are \
2076              in-bounds by construction"
2077)]
2078fn state_path<'a>(plan: &'a MutationPlan, state: &PathState) -> &'a NamespacePath {
2079    plan.operations()[state.operation]
2080        .paths()
2081        .expect("path state must reference a path operation")[state.endpoint]
2082}
2083
2084fn find_path_state(
2085    plan: &MutationPlan,
2086    states: &[PathState],
2087    components: &[LogicalName],
2088    comparisons: &Cell<u64>,
2089) -> Option<usize> {
2090    states
2091        .binary_search_by(|state| {
2092            comparisons.set(comparisons.get().saturating_add(1));
2093            state_path(plan, state).components().cmp(components)
2094        })
2095        .ok()
2096}
2097
2098fn reserve_exact<T>(
2099    count: usize,
2100    allocations: &mut AllocationLedger,
2101    work: &mut WorkCounters,
2102    budget: WorkBudget,
2103) -> Result<Vec<T>, GenerationMutationFailure> {
2104    let bytes = count
2105        .checked_mul(size_of::<T>())
2106        .map(crate::foundation::usize_to_u64)
2107        .ok_or_else(|| failed(WorkError::Overflow.into(), *work))?;
2108    allocations
2109        .claim_bytes(bytes, u64::from(count != 0), work, budget)
2110        .map_err(|error| allocation_failure(error, *work))?;
2111    let admitted = *work;
2112    let mut values = Vec::new();
2113    if values.try_reserve_exact(count).is_err() {
2114        allocations
2115            .release(bytes)
2116            .map_err(|error| allocation_failure(error, *work))?;
2117        return Err(failed(GenerationMutationError::AllocationFailed, admitted));
2118    }
2119    Ok(values)
2120}
2121
2122fn logical_vec_bytes<T>(values: &Vec<T>) -> Result<u64, GenerationMutationFailure> {
2123    values
2124        .capacity()
2125        .checked_mul(size_of::<T>())
2126        .map(crate::foundation::usize_to_u64)
2127        .ok_or_else(|| {
2128            OperationFailure::before_work(GenerationMutationError::Work(WorkError::Overflow))
2129        })
2130}
2131
2132fn decode_limits(config: VolumeConfig) -> DecodeLimits {
2133    DecodeLimits {
2134        maximum_object_bytes: config.limits.maximum_object_bytes,
2135        maximum_name_bytes: config.limits.maximum_component_bytes,
2136        maximum_page_items: config.limits.maximum_directory_page_entries,
2137        maximum_page_bytes: u32::try_from(config.limits.maximum_object_bytes).unwrap_or(u32::MAX),
2138        maximum_page_height: config.limits.maximum_page_height,
2139        maximum_visited_pages: u32::try_from(config.limits.maximum_objects_per_generation)
2140            .unwrap_or(u32::MAX),
2141    }
2142}
2143
2144fn nested_budget(
2145    work: WorkCounters,
2146    budget: WorkBudget,
2147    live_bytes: u64,
2148) -> Result<WorkBudget, GenerationMutationFailure> {
2149    let mut remaining = work
2150        .remaining(budget)
2151        .map_err(|error| failed(error.into(), work))?;
2152    remaining.peak_allocation_bytes = remaining
2153        .peak_allocation_bytes
2154        .checked_sub(live_bytes)
2155        .ok_or_else(|| failed(WorkError::Overflow.into(), work))?;
2156    Ok(remaining)
2157}
2158
2159fn merge_nested(
2160    prior: WorkCounters,
2161    mut nested: WorkCounters,
2162    live_bytes: u64,
2163    budget: WorkBudget,
2164) -> Result<WorkCounters, GenerationMutationFailure> {
2165    let peak = live_bytes
2166        .checked_add(nested.peak_allocation_bytes)
2167        .ok_or_else(|| failed(WorkError::Overflow.into(), prior))?;
2168    nested.peak_allocation_bytes = 0;
2169    let mut work = prior
2170        .checked_add(nested)
2171        .map_err(|error| failed(error.into(), prior))?;
2172    work.peak_allocation_bytes = work.peak_allocation_bytes.max(peak);
2173    work.verify(budget)
2174        .map_err(|error| failed(error.into(), work))?;
2175    Ok(work)
2176}
2177
2178fn nested_failure(
2179    prior: WorkCounters,
2180    mut nested: WorkCounters,
2181    live_bytes: u64,
2182    error: GenerationMutationError,
2183) -> GenerationMutationFailure {
2184    let Some(peak) = live_bytes.checked_add(nested.peak_allocation_bytes) else {
2185        return failed(WorkError::Overflow.into(), prior);
2186    };
2187    nested.peak_allocation_bytes = 0;
2188    let Ok(mut work) = prior.checked_add(nested) else {
2189        return failed(WorkError::Overflow.into(), prior);
2190    };
2191    work.peak_allocation_bytes = work.peak_allocation_bytes.max(peak);
2192    failed(error, work)
2193}
2194
2195fn charge_items(
2196    work: &mut WorkCounters,
2197    count: u64,
2198    budget: WorkBudget,
2199) -> Result<(), GenerationMutationFailure> {
2200    let prospective = work
2201        .checked_add(WorkCounters {
2202            items_examined: count,
2203            ..WorkCounters::default()
2204        })
2205        .map_err(|error| failed(error.into(), *work))?;
2206    prospective
2207        .verify(budget)
2208        .map_err(|error| failed(error.into(), *work))?;
2209    *work = prospective;
2210    Ok(())
2211}
2212
2213fn allocation_failure(error: AllocationError, work: WorkCounters) -> GenerationMutationFailure {
2214    match error {
2215        AllocationError::Work(error) => failed(error.into(), work),
2216        AllocationError::Overflow | AllocationError::ReleaseInvariant => {
2217            failed(WorkError::Overflow.into(), work)
2218        }
2219        AllocationError::InvalidCapacity
2220        | AllocationError::CapacityExceeded
2221        | AllocationError::AllocationFailed => {
2222            failed(GenerationMutationError::AllocationFailed, work)
2223        }
2224    }
2225}
2226
2227fn path_cancelled() -> GenerationMutationError {
2228    GenerationMutationError::Path(PathLookupError::Tree(super::TreeReadError::Cancelled))
2229}
2230
2231fn failed(error: GenerationMutationError, work: WorkCounters) -> GenerationMutationFailure {
2232    OperationFailure::new(error, work)
2233}
2234
2235#[cfg(all(test, feature = "memory"))]
2236#[path = "tests/generation_mutation.rs"]
2237mod tests;