1use 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#[derive(Clone, Debug, Eq, PartialEq)]
27pub struct GenerationMutationReceipt {
28 pub root: GenerationRoot,
30 pub work: WorkCounters,
32}
33
34pub type GenerationMutationFailure = OperationFailure<GenerationMutationError>;
36
37#[derive(Debug, Error)]
39pub enum GenerationMutationError {
40 #[error(transparent)]
42 Plan(#[from] MutationPlanError),
43 #[error(transparent)]
45 Config(#[from] VolumeConfigError),
46 #[error("generation mutation endpoint count exceeds the volume path limit")]
48 TooManyPaths,
49 #[error("new file record must have link count one")]
51 InvalidInitialLinkCount,
52 #[error("symbolic links are disabled for this volume")]
54 UnsupportedSymbolicLink,
55 #[error("file kind is unsupported by the volume profile")]
57 UnsupportedFileKind,
58 #[error("sparse mutation is disabled for this volume")]
60 UnsupportedSparseMutation,
61 #[error(transparent)]
63 Path(#[from] PathLookupError),
64 #[error(transparent)]
66 FileRecord(#[from] FileRecordReadError),
67 #[error("namespace mutation parent directory is missing")]
69 MissingParent,
70 #[error("namespace mutation source is missing")]
72 MissingSource,
73 #[error("namespace mutation destination already exists")]
75 AlreadyExists,
76 #[error("namespace mutation file identity precondition failed")]
78 FileIdentityConflict,
79 #[error("namespace mutation created file identity is not fresh")]
81 FileIdentityAlreadyExists,
82 #[error("namespace mutation hard link is unsupported")]
84 UnsupportedHardLink,
85 #[error("namespace mutation would create a directory cycle")]
87 DirectoryCycle,
88 #[error("namespace mutation directory is not empty")]
90 DirectoryNotEmpty,
91 #[error("namespace mutation authenticated state is inconsistent")]
93 InconsistentState,
94 #[error(transparent)]
96 Tree(#[from] TreeMutationError),
97 #[error(transparent)]
99 FileTable(#[from] FileTableMutationError),
100 #[error(transparent)]
102 Regular(#[from] RegularMutationError),
103 #[error(transparent)]
105 Directory(#[from] DirectoryReadError),
106 #[error("namespace mutation scratch allocation failed")]
108 AllocationFailed,
109 #[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
166pub 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
272pub 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 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 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 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 #[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#[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;