aster_forge_webdav/
mutation.rs

1//! Bounded recursive COPY, MOVE, and DELETE execution.
2
3use http::StatusCode;
4
5use crate::response::{no_store_empty_response, xml_document_response};
6use crate::{
7    DavBackendErrorKind, DavCancellation, DavDirectoryEntry, DavDirectoryEnumerator,
8    DavDirectoryPageLimits, DavDirectoryPageState, DavDirectoryReadError, DavErrorCondition,
9    DavMetaData, DavMultiStatusError, DavMutationFailure, DavPath, DavPathError, DavResourceKind,
10    DavResponse, DavTraversalBudget, DavTraversalError, DavTraversalErrorKind, DavTraversalLimits,
11    DavXmlError, dav_error_element, delete_success_response,
12    mutation_multistatus_response_with_limits, mutation_success_response, read_next_directory_page,
13};
14
15/// Recursive mutation selected by the request method.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum DavMutationOperation {
18    Copy,
19    Move,
20    Delete,
21}
22
23/// Product mutation performed by one authoritative commit.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum DavMutationStepKind {
26    CopyFile,
27    MoveFile,
28    PrepareCollection,
29    DeleteFile,
30    DeleteCollection,
31}
32
33/// Role of the resource modified by a mutation step.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum DavMutationTargetRole {
36    Source,
37    Destination,
38}
39
40/// One product-authoritative mutation step.
41///
42/// The product port must perform the resource write, transactional lock revalidation, rooted-lock
43/// cleanup or destination-lock rebinding, and derived lock-state synchronization in one commit.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct DavMutationCommand {
46    pub operation: DavMutationOperation,
47    pub step: DavMutationStepKind,
48    pub role: DavMutationTargetRole,
49    pub source: DavPath,
50    pub destination: Option<DavPath>,
51}
52
53impl DavMutationCommand {
54    #[must_use]
55    pub fn affected_path(&self) -> &DavPath {
56        self.destination.as_ref().unwrap_or(&self.source)
57    }
58}
59
60/// Typed failure returned by the authoritative product mutation port.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum DavMutationStepError {
63    Locked {
64        affected_path: DavPath,
65        lock_root: DavPath,
66    },
67    Status {
68        affected_path: DavPath,
69        status: u16,
70    },
71}
72
73impl DavMutationStepError {
74    #[must_use]
75    pub fn locked(affected_path: DavPath, lock_root: DavPath) -> Self {
76        Self::Locked {
77            affected_path,
78            lock_root,
79        }
80    }
81
82    #[must_use]
83    pub fn status(affected_path: DavPath, status: u16) -> Self {
84        Self::Status {
85            affected_path,
86            status,
87        }
88    }
89
90    #[must_use]
91    pub fn backend(affected_path: DavPath) -> Self {
92        Self::status(affected_path, StatusCode::INTERNAL_SERVER_ERROR.as_u16())
93    }
94
95    /// Maps one typed backend failure to the canonical resource status.
96    #[must_use]
97    pub fn from_backend(affected_path: DavPath, error: &crate::DavBackendError) -> Self {
98        let status = match error.kind {
99            DavBackendErrorKind::NotFound => StatusCode::NOT_FOUND,
100            DavBackendErrorKind::Forbidden => StatusCode::FORBIDDEN,
101            DavBackendErrorKind::Conflict | DavBackendErrorKind::AlreadyExists => {
102                StatusCode::CONFLICT
103            }
104            DavBackendErrorKind::InsufficientStorage => StatusCode::INSUFFICIENT_STORAGE,
105            DavBackendErrorKind::PayloadTooLarge => StatusCode::PAYLOAD_TOO_LARGE,
106            DavBackendErrorKind::Locked => StatusCode::LOCKED,
107            DavBackendErrorKind::InvalidInput => StatusCode::BAD_REQUEST,
108            DavBackendErrorKind::Unsupported => StatusCode::METHOD_NOT_ALLOWED,
109            DavBackendErrorKind::Internal => StatusCode::INTERNAL_SERVER_ERROR,
110        };
111        Self::status(affected_path, status.as_u16())
112    }
113
114    fn into_failure(self) -> DavMutationFailure {
115        match self {
116            Self::Locked {
117                affected_path,
118                lock_root,
119            } => DavMutationFailure::locked(affected_path, lock_root),
120            Self::Status {
121                affected_path,
122                status,
123            } => DavMutationFailure::status(affected_path, status),
124        }
125    }
126}
127
128/// Product port for a single atomic mutation step.
129///
130/// Forge deliberately does not expose a transaction type. The implementation owns its writer
131/// transaction and returns only after commit succeeds.
132pub trait DavMutationPort: Send + Sync {
133    fn execute(
134        &self,
135        command: DavMutationCommand,
136    ) -> impl Future<Output = Result<(), DavMutationStepError>> + Send;
137}
138
139/// Hard limits for one recursive mutation execution.
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141pub struct DavMutationExecutorLimits {
142    pub traversal: DavTraversalLimits,
143    pub directory_pages: DavDirectoryPageLimits,
144    pub directory_page_entries: usize,
145    pub maximum_directory_entries: usize,
146}
147
148impl DavMutationExecutorLimits {
149    #[must_use]
150    pub const fn new(
151        traversal: DavTraversalLimits,
152        directory_pages: DavDirectoryPageLimits,
153        directory_page_entries: usize,
154        maximum_directory_entries: usize,
155    ) -> Self {
156        Self {
157            traversal,
158            directory_pages,
159            directory_page_entries,
160            maximum_directory_entries,
161        }
162    }
163}
164
165/// Input for one recursive mutation.
166#[derive(Debug, Clone, PartialEq, Eq)]
167pub struct DavMutationRequest {
168    pub operation: DavMutationOperation,
169    pub source: DavPath,
170    pub source_kind: DavResourceKind,
171    pub destination: Option<DavPath>,
172    pub destination_kind: Option<DavResourceKind>,
173    pub destination_existed: bool,
174    pub recurse_collections: bool,
175}
176
177/// Terminal reason when bounded execution stops before draining its work queue.
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179pub enum DavMutationStop {
180    Cancelled,
181    InsufficientStorage,
182    Backend,
183}
184
185/// Complete or partial result of recursive mutation execution.
186#[derive(Debug, Clone, PartialEq, Eq)]
187pub struct DavMutationOutcome {
188    pub operation: DavMutationOperation,
189    pub request_target: DavPath,
190    pub destination_existed: bool,
191    pub failures: Vec<DavMutationFailure>,
192    pub stop: Option<DavMutationStop>,
193    pub progress: crate::DavTraversalProgress,
194}
195
196impl DavMutationOutcome {
197    #[must_use]
198    pub fn is_complete_success(&self) -> bool {
199        self.failures.is_empty() && self.stop.is_none()
200    }
201}
202
203#[derive(Debug, Clone)]
204struct Child {
205    name: bytes::Bytes,
206    key: bytes::Bytes,
207    path: DavPath,
208    is_collection: bool,
209}
210
211impl Child {
212    fn namespace_name(&self) -> &[u8] {
213        &self.name
214    }
215}
216
217#[derive(Debug, Clone)]
218enum Work {
219    TransferDirectory {
220        source: DavPath,
221        destination: DavPath,
222        depth: usize,
223        parent: Option<usize>,
224    },
225    TransferFile {
226        source: DavPath,
227        destination: DavPath,
228        depth: usize,
229        parent: Option<usize>,
230    },
231    ReplaceWithFile {
232        source: DavPath,
233        destination: DavPath,
234        depth: usize,
235        parent: Option<usize>,
236    },
237    DeleteNode {
238        path: DavPath,
239        is_collection: bool,
240        role: DavMutationTargetRole,
241        depth: usize,
242        parent: Option<usize>,
243    },
244    FinalizeTransfer {
245        source: DavPath,
246        frame: usize,
247    },
248    FinalizeFileReplacement {
249        source: DavPath,
250        destination: DavPath,
251        depth: usize,
252        frame: usize,
253    },
254    FinalizeDelete {
255        path: DavPath,
256        role: DavMutationTargetRole,
257        frame: usize,
258    },
259}
260
261#[derive(Debug, Clone, Copy)]
262struct Frame {
263    parent: Option<usize>,
264    failed: bool,
265}
266
267/// Executes a bounded recursive COPY, MOVE, or DELETE.
268///
269/// A collection failure prunes that subtree. Failures in one child are recorded while eligible
270/// siblings continue. Collection removal is post-order and is skipped after any descendant
271/// failure, preserving namespace consistency.
272///
273/// # Errors
274///
275/// Invalid request shapes return a typed backend-style stop outcome rather than invoking the port.
276#[expect(
277    clippy::too_many_lines,
278    reason = "the iterative state machine keeps queue accounting, frame propagation, and cancellation order visible in one loop"
279)]
280pub async fn execute_recursive_mutation<E, P, C>(
281    request: DavMutationRequest,
282    enumerator: &E,
283    port: &P,
284    cancellation: &C,
285    limits: DavMutationExecutorLimits,
286) -> DavMutationOutcome
287where
288    E: DavDirectoryEnumerator,
289    P: DavMutationPort,
290    C: DavCancellation,
291{
292    let operation = request.operation;
293    let destination_existed = request.destination_existed;
294    let recurse_collections = request.recurse_collections;
295    let request_target = request
296        .destination
297        .as_ref()
298        .unwrap_or(&request.source)
299        .clone();
300    let mut failures = Vec::new();
301    let mut stop = None;
302    let mut budget = match DavTraversalBudget::new(limits.traversal) {
303        Ok(budget)
304            if limits.directory_page_entries != 0 && limits.maximum_directory_entries != 0 =>
305        {
306            budget
307        }
308        Ok(budget) => {
309            return DavMutationOutcome {
310                operation,
311                request_target,
312                destination_existed,
313                failures,
314                stop: Some(DavMutationStop::InsufficientStorage),
315                progress: budget.progress(),
316            };
317        }
318        Err(error) => {
319            return DavMutationOutcome {
320                operation,
321                request_target,
322                destination_existed,
323                failures,
324                stop: Some(stop_from_traversal(error)),
325                progress: error.progress,
326            };
327        }
328    };
329
330    let root = match (
331        operation,
332        request.source_kind,
333        request.destination,
334        request.destination_kind,
335    ) {
336        (DavMutationOperation::Move, DavResourceKind::Collection, Some(_), _)
337            if !recurse_collections =>
338        {
339            return DavMutationOutcome {
340                operation,
341                request_target,
342                destination_existed,
343                failures,
344                stop: Some(DavMutationStop::Backend),
345                progress: budget.progress(),
346            };
347        }
348        (DavMutationOperation::Delete, kind, _, _) => Work::DeleteNode {
349            path: request.source,
350            is_collection: kind == DavResourceKind::Collection,
351            role: DavMutationTargetRole::Source,
352            depth: 0,
353            parent: None,
354        },
355        (
356            DavMutationOperation::Copy | DavMutationOperation::Move,
357            DavResourceKind::File,
358            Some(destination),
359            Some(DavResourceKind::Collection),
360        ) => Work::ReplaceWithFile {
361            source: request.source,
362            destination,
363            depth: 0,
364            parent: None,
365        },
366        (
367            DavMutationOperation::Copy | DavMutationOperation::Move,
368            DavResourceKind::File,
369            Some(destination),
370            _,
371        ) => Work::TransferFile {
372            source: request.source,
373            destination,
374            depth: 0,
375            parent: None,
376        },
377        (
378            DavMutationOperation::Copy | DavMutationOperation::Move,
379            DavResourceKind::Collection,
380            Some(destination),
381            _,
382        ) => Work::TransferDirectory {
383            source: request.source,
384            destination,
385            depth: 0,
386            parent: None,
387        },
388        _ => {
389            return DavMutationOutcome {
390                operation,
391                request_target,
392                destination_existed,
393                failures,
394                stop: Some(DavMutationStop::Backend),
395                progress: budget.progress(),
396            };
397        }
398    };
399
400    let mut work = vec![root];
401    if let Err(error) = budget.reserve_work(1) {
402        stop = Some(stop_from_traversal(error));
403    }
404    let mut frames = Vec::new();
405
406    while stop.is_none() {
407        if let Err(error) = budget.checkpoint(cancellation) {
408            stop = Some(stop_from_traversal(error));
409            break;
410        }
411        let Some(item) = work.pop() else {
412            break;
413        };
414        budget.complete_work();
415
416        match item {
417            Work::TransferFile {
418                source,
419                destination,
420                depth,
421                parent,
422            } => {
423                if let Err(error) = budget.visit(depth) {
424                    stop = Some(stop_from_traversal(error));
425                    continue;
426                }
427                let frame = parent.unwrap_or_else(|| {
428                    let frame = frames.len();
429                    frames.push(Frame {
430                        parent: None,
431                        failed: false,
432                    });
433                    frame
434                });
435                let step_kind = if operation == DavMutationOperation::Move {
436                    DavMutationStepKind::MoveFile
437                } else {
438                    DavMutationStepKind::CopyFile
439                };
440                let command = DavMutationCommand {
441                    operation,
442                    step: step_kind,
443                    role: DavMutationTargetRole::Destination,
444                    source,
445                    destination: Some(destination),
446                };
447                if let Err(error) = apply_step(
448                    port,
449                    command,
450                    frame,
451                    &mut frames,
452                    &mut failures,
453                    &mut budget,
454                )
455                .await
456                {
457                    stop = Some(stop_from_traversal(error));
458                }
459            }
460            Work::ReplaceWithFile {
461                source,
462                destination,
463                depth,
464                parent,
465            } => {
466                let frame = frames.len();
467                frames.push(Frame {
468                    parent,
469                    failed: false,
470                });
471                let next = [
472                    Work::DeleteNode {
473                        path: destination.clone(),
474                        is_collection: true,
475                        role: DavMutationTargetRole::Destination,
476                        depth,
477                        parent: Some(frame),
478                    },
479                    Work::FinalizeFileReplacement {
480                        source,
481                        destination,
482                        depth,
483                        frame,
484                    },
485                ];
486                if let Err(error) = budget.reserve_work(next.len()) {
487                    stop = Some(stop_from_traversal(error));
488                    continue;
489                }
490                work.extend(next.into_iter().rev());
491            }
492            Work::TransferDirectory {
493                source,
494                destination,
495                depth,
496                parent,
497            } => {
498                if let Err(error) = budget.visit(depth) {
499                    stop = Some(stop_from_traversal(error));
500                    continue;
501                }
502                let frame = frames.len();
503                frames.push(Frame {
504                    parent,
505                    failed: false,
506                });
507                let command = DavMutationCommand {
508                    operation,
509                    step: DavMutationStepKind::PrepareCollection,
510                    role: DavMutationTargetRole::Destination,
511                    source: source.clone(),
512                    destination: Some(destination.clone()),
513                };
514                let applied = match apply_step(
515                    port,
516                    command,
517                    frame,
518                    &mut frames,
519                    &mut failures,
520                    &mut budget,
521                )
522                .await
523                {
524                    Ok(applied) => applied,
525                    Err(error) => {
526                        stop = Some(stop_from_traversal(error));
527                        false
528                    }
529                };
530                if !applied {
531                    propagate_frame_failure(frame, &mut frames);
532                    continue;
533                }
534                if !recurse_collections {
535                    propagate_frame_failure(frame, &mut frames);
536                    continue;
537                }
538
539                let source_children =
540                    match read_children(enumerator, &source, cancellation, limits).await {
541                        Ok(children) => children,
542                        Err(error) => {
543                            record_directory_read_failure(
544                                error,
545                                source,
546                                frame,
547                                &mut frames,
548                                &mut failures,
549                                &mut budget,
550                                &mut stop,
551                            );
552                            propagate_frame_failure(frame, &mut frames);
553                            continue;
554                        }
555                    };
556                let destination_children =
557                    match read_children(enumerator, &destination, cancellation, limits).await {
558                        Ok(children) => children,
559                        Err(error) => {
560                            record_directory_read_failure(
561                                error,
562                                destination,
563                                frame,
564                                &mut frames,
565                                &mut failures,
566                                &mut budget,
567                                &mut stop,
568                            );
569                            propagate_frame_failure(frame, &mut frames);
570                            continue;
571                        }
572                    };
573                let Some(child_depth) = depth.checked_add(1) else {
574                    stop = Some(DavMutationStop::InsufficientStorage);
575                    continue;
576                };
577                let Some(mut next) = merge_transfer_children_or_stop(
578                    &destination,
579                    &source_children,
580                    &destination_children,
581                    child_depth,
582                    frame,
583                    &mut frames,
584                    &mut stop,
585                ) else {
586                    continue;
587                };
588                next.push(Work::FinalizeTransfer { source, frame });
589                if let Err(error) = budget.reserve_work(next.len()) {
590                    stop = Some(stop_from_traversal(error));
591                    continue;
592                }
593                work.extend(next.into_iter().rev());
594            }
595            Work::DeleteNode {
596                path,
597                is_collection: false,
598                role,
599                depth,
600                parent,
601            } => {
602                if let Err(error) = budget.visit(depth) {
603                    stop = Some(stop_from_traversal(error));
604                    continue;
605                }
606                let frame = parent.unwrap_or_else(|| {
607                    let frame = frames.len();
608                    frames.push(Frame {
609                        parent: None,
610                        failed: false,
611                    });
612                    frame
613                });
614                let command = DavMutationCommand {
615                    operation,
616                    step: DavMutationStepKind::DeleteFile,
617                    role,
618                    source: path,
619                    destination: None,
620                };
621                if let Err(error) = apply_step(
622                    port,
623                    command,
624                    frame,
625                    &mut frames,
626                    &mut failures,
627                    &mut budget,
628                )
629                .await
630                {
631                    stop = Some(stop_from_traversal(error));
632                }
633            }
634            Work::DeleteNode {
635                path,
636                is_collection: true,
637                role,
638                depth,
639                parent,
640            } => {
641                if let Err(error) = budget.visit(depth) {
642                    stop = Some(stop_from_traversal(error));
643                    continue;
644                }
645                let frame = frames.len();
646                frames.push(Frame {
647                    parent,
648                    failed: false,
649                });
650                let children = match read_children(enumerator, &path, cancellation, limits).await {
651                    Ok(children) => children,
652                    Err(error) => {
653                        record_directory_read_failure(
654                            error,
655                            path.clone(),
656                            frame,
657                            &mut frames,
658                            &mut failures,
659                            &mut budget,
660                            &mut stop,
661                        );
662                        propagate_frame_failure(frame, &mut frames);
663                        continue;
664                    }
665                };
666                let Some(child_depth) = depth.checked_add(1) else {
667                    stop = Some(DavMutationStop::InsufficientStorage);
668                    continue;
669                };
670                let mut next = Vec::with_capacity(children.len().saturating_add(1));
671                for child in children {
672                    next.push(Work::DeleteNode {
673                        path: child.path,
674                        is_collection: child.is_collection,
675                        role,
676                        depth: child_depth,
677                        parent: Some(frame),
678                    });
679                }
680                next.push(Work::FinalizeDelete { path, role, frame });
681                if let Err(error) = budget.reserve_work(next.len()) {
682                    stop = Some(stop_from_traversal(error));
683                    continue;
684                }
685                work.extend(next.into_iter().rev());
686            }
687            Work::FinalizeTransfer { source, frame } => {
688                if operation != DavMutationOperation::Move || frames[frame].failed {
689                    propagate_frame_failure(frame, &mut frames);
690                    continue;
691                }
692                let command = DavMutationCommand {
693                    operation,
694                    step: DavMutationStepKind::DeleteCollection,
695                    role: DavMutationTargetRole::Source,
696                    source,
697                    destination: None,
698                };
699                if let Err(error) = apply_step(
700                    port,
701                    command,
702                    frame,
703                    &mut frames,
704                    &mut failures,
705                    &mut budget,
706                )
707                .await
708                {
709                    stop = Some(stop_from_traversal(error));
710                }
711                propagate_frame_failure(frame, &mut frames);
712            }
713            Work::FinalizeFileReplacement {
714                source,
715                destination,
716                depth,
717                frame,
718            } => {
719                if frames[frame].failed {
720                    propagate_frame_failure(frame, &mut frames);
721                    continue;
722                }
723                if let Err(error) = budget.visit(depth) {
724                    stop = Some(stop_from_traversal(error));
725                    continue;
726                }
727                let step_kind = if operation == DavMutationOperation::Move {
728                    DavMutationStepKind::MoveFile
729                } else {
730                    DavMutationStepKind::CopyFile
731                };
732                let command = DavMutationCommand {
733                    operation,
734                    step: step_kind,
735                    role: DavMutationTargetRole::Destination,
736                    source,
737                    destination: Some(destination),
738                };
739                if let Err(error) = apply_step(
740                    port,
741                    command,
742                    frame,
743                    &mut frames,
744                    &mut failures,
745                    &mut budget,
746                )
747                .await
748                {
749                    stop = Some(stop_from_traversal(error));
750                }
751                propagate_frame_failure(frame, &mut frames);
752            }
753            Work::FinalizeDelete { path, role, frame } => {
754                if frames[frame].failed {
755                    propagate_frame_failure(frame, &mut frames);
756                    continue;
757                }
758                let command = DavMutationCommand {
759                    operation,
760                    step: DavMutationStepKind::DeleteCollection,
761                    role,
762                    source: path,
763                    destination: None,
764                };
765                if let Err(error) = apply_step(
766                    port,
767                    command,
768                    frame,
769                    &mut frames,
770                    &mut failures,
771                    &mut budget,
772                )
773                .await
774                {
775                    stop = Some(stop_from_traversal(error));
776                }
777                propagate_frame_failure(frame, &mut frames);
778            }
779        }
780    }
781
782    DavMutationOutcome {
783        operation,
784        request_target,
785        destination_existed,
786        failures,
787        stop,
788        progress: budget.progress(),
789    }
790}
791
792async fn apply_step<P: DavMutationPort>(
793    port: &P,
794    command: DavMutationCommand,
795    frame: usize,
796    frames: &mut [Frame],
797    failures: &mut Vec<DavMutationFailure>,
798    budget: &mut DavTraversalBudget,
799) -> Result<bool, DavTraversalError> {
800    match port.execute(command).await {
801        Ok(()) => {
802            budget.record_completed_mutation();
803            Ok(true)
804        }
805        Err(error) => {
806            frames[frame].failed = true;
807            budget.record_failure()?;
808            failures.push(error.into_failure());
809            Ok(false)
810        }
811    }
812}
813
814fn propagate_frame_failure(frame: usize, frames: &mut [Frame]) {
815    if !frames[frame].failed {
816        return;
817    }
818    if let Some(parent) = frames[frame].parent {
819        frames[parent].failed = true;
820    }
821}
822
823async fn read_children<E: DavDirectoryEnumerator, C: DavCancellation>(
824    enumerator: &E,
825    path: &DavPath,
826    cancellation: &C,
827    limits: DavMutationExecutorLimits,
828) -> Result<Vec<Child>, DavDirectoryReadError> {
829    let mut state = DavDirectoryPageState::new();
830    let mut children = Vec::new();
831    while let Some(page) = read_next_directory_page(
832        enumerator,
833        path,
834        &mut state,
835        limits.directory_page_entries,
836        limits.directory_pages,
837        cancellation,
838    )
839    .await?
840    {
841        if children
842            .len()
843            .checked_add(page.entries.len())
844            .is_none_or(|count| count > limits.maximum_directory_entries)
845        {
846            return Err(DavDirectoryReadError::Backend(crate::DavBackendError::new(
847                DavBackendErrorKind::InsufficientStorage,
848            )));
849        }
850        for entry in page.entries {
851            let is_collection = entry.metadata().is_dir();
852            let name = bytes::Bytes::copy_from_slice(entry.name());
853            let child_path = path.join_child(&name, is_collection).map_err(|_| {
854                DavDirectoryReadError::Backend(crate::DavBackendError::new(
855                    DavBackendErrorKind::Internal,
856                ))
857            })?;
858            children.push(Child {
859                name,
860                key: bytes::Bytes::copy_from_slice(entry.stable_key()),
861                path: child_path,
862                is_collection,
863            });
864        }
865    }
866    Ok(children)
867}
868
869fn merge_transfer_children(
870    destination_root: &DavPath,
871    source: &[Child],
872    destination: &[Child],
873    depth: usize,
874    frame: usize,
875) -> Result<Vec<Work>, DavPathError> {
876    let mut work = Vec::with_capacity(source.len().saturating_add(destination.len()));
877    let mut source_order = (0..source.len()).collect::<Vec<_>>();
878    source_order.sort_unstable_by(|left, right| {
879        source[*left]
880            .namespace_name()
881            .cmp(source[*right].namespace_name())
882            .then_with(|| source[*left].key.cmp(&source[*right].key))
883    });
884    let mut destination_order = (0..destination.len()).collect::<Vec<_>>();
885    destination_order.sort_unstable_by(|left, right| {
886        destination[*left]
887            .namespace_name()
888            .cmp(destination[*right].namespace_name())
889            .then_with(|| destination[*left].key.cmp(&destination[*right].key))
890    });
891    let mut source_index = 0;
892    let mut destination_index = 0;
893    while source_index < source.len() || destination_index < destination.len() {
894        match (
895            source_order
896                .get(source_index)
897                .and_then(|index| source.get(*index)),
898            destination_order
899                .get(destination_index)
900                .and_then(|index| destination.get(*index)),
901        ) {
902            (Some(source_child), Some(destination_child)) => {
903                match source_child
904                    .namespace_name()
905                    .cmp(destination_child.namespace_name())
906                {
907                    std::cmp::Ordering::Less => {
908                        push_source_child(&mut work, destination_root, source_child, depth, frame)?;
909                        source_index += 1;
910                    }
911                    std::cmp::Ordering::Equal => {
912                        push_matching_children(
913                            &mut work,
914                            destination_root,
915                            source_child,
916                            destination_child,
917                            depth,
918                            frame,
919                        )?;
920                        source_index += 1;
921                        destination_index += 1;
922                    }
923                    std::cmp::Ordering::Greater => {
924                        work.push(destination_delete_work(destination_child, depth, frame));
925                        destination_index += 1;
926                    }
927                }
928            }
929            (Some(source_child), None) => {
930                push_source_child(&mut work, destination_root, source_child, depth, frame)?;
931                source_index += 1;
932            }
933            (None, Some(destination_child)) => {
934                work.push(destination_delete_work(destination_child, depth, frame));
935                destination_index += 1;
936            }
937            (None, None) => break,
938        }
939    }
940    Ok(work)
941}
942
943fn merge_transfer_children_or_stop(
944    destination_root: &DavPath,
945    source: &[Child],
946    destination: &[Child],
947    depth: usize,
948    frame: usize,
949    frames: &mut [Frame],
950    stop: &mut Option<DavMutationStop>,
951) -> Option<Vec<Work>> {
952    if let Ok(work) = merge_transfer_children(destination_root, source, destination, depth, frame) {
953        Some(work)
954    } else {
955        *stop = Some(DavMutationStop::Backend);
956        propagate_frame_failure(frame, frames);
957        None
958    }
959}
960
961fn push_source_child(
962    work: &mut Vec<Work>,
963    destination_root: &DavPath,
964    child: &Child,
965    depth: usize,
966    frame: usize,
967) -> Result<(), DavPathError> {
968    let destination = destination_root.join_child(&child.name, child.is_collection)?;
969    if child.is_collection {
970        work.push(Work::TransferDirectory {
971            source: child.path.clone(),
972            destination,
973            depth,
974            parent: Some(frame),
975        });
976    } else {
977        work.push(Work::TransferFile {
978            source: child.path.clone(),
979            destination,
980            depth,
981            parent: Some(frame),
982        });
983    }
984    Ok(())
985}
986
987fn push_matching_children(
988    work: &mut Vec<Work>,
989    destination_root: &DavPath,
990    source: &Child,
991    destination: &Child,
992    depth: usize,
993    frame: usize,
994) -> Result<(), DavPathError> {
995    let destination_path = destination_root.join_child(&source.name, source.is_collection)?;
996    if source.is_collection {
997        work.push(Work::TransferDirectory {
998            source: source.path.clone(),
999            destination: destination_path,
1000            depth,
1001            parent: Some(frame),
1002        });
1003    } else if destination.is_collection {
1004        work.push(Work::ReplaceWithFile {
1005            source: source.path.clone(),
1006            destination: destination.path.clone(),
1007            depth,
1008            parent: Some(frame),
1009        });
1010    } else {
1011        work.push(Work::TransferFile {
1012            source: source.path.clone(),
1013            destination: destination_path,
1014            depth,
1015            parent: Some(frame),
1016        });
1017    }
1018    Ok(())
1019}
1020
1021fn destination_delete_work(child: &Child, depth: usize, frame: usize) -> Work {
1022    Work::DeleteNode {
1023        path: child.path.clone(),
1024        is_collection: child.is_collection,
1025        role: DavMutationTargetRole::Destination,
1026        depth,
1027        parent: Some(frame),
1028    }
1029}
1030
1031fn record_directory_read_failure(
1032    error: DavDirectoryReadError,
1033    path: DavPath,
1034    frame: usize,
1035    frames: &mut [Frame],
1036    failures: &mut Vec<DavMutationFailure>,
1037    budget: &mut DavTraversalBudget,
1038    stop: &mut Option<DavMutationStop>,
1039) {
1040    match error {
1041        DavDirectoryReadError::Cancelled => *stop = Some(DavMutationStop::Cancelled),
1042        DavDirectoryReadError::Backend(error)
1043            if error.kind == DavBackendErrorKind::InsufficientStorage =>
1044        {
1045            *stop = Some(DavMutationStop::InsufficientStorage);
1046        }
1047        DavDirectoryReadError::PageLimitExceeded
1048        | DavDirectoryReadError::InvalidLimit
1049        | DavDirectoryReadError::InvalidPage(_) => {
1050            *stop = Some(DavMutationStop::InsufficientStorage);
1051        }
1052        DavDirectoryReadError::Backend(error) => {
1053            frames[frame].failed = true;
1054            if let Err(error) = budget.record_failure() {
1055                *stop = Some(stop_from_traversal(error));
1056                return;
1057            }
1058            failures.push(DavMutationStepError::from_backend(path, &error).into_failure());
1059        }
1060    }
1061}
1062
1063fn stop_from_traversal(error: DavTraversalError) -> DavMutationStop {
1064    match error.kind {
1065        DavTraversalErrorKind::Cancelled => DavMutationStop::Cancelled,
1066        DavTraversalErrorKind::InvalidLimits
1067        | DavTraversalErrorKind::VisitedResourceLimitExceeded
1068        | DavTraversalErrorKind::QueuedWorkLimitExceeded
1069        | DavTraversalErrorKind::FailureLimitExceeded
1070        | DavTraversalErrorKind::DepthLimitExceeded => DavMutationStop::InsufficientStorage,
1071    }
1072}
1073
1074/// Composes the canonical response for a recursive mutation outcome.
1075///
1076/// # Errors
1077///
1078/// Returns a Multi-Status serialization error when accumulated failures exceed response limits.
1079pub fn mutation_outcome_response(
1080    prefix: &str,
1081    outcome: &DavMutationOutcome,
1082    limits: crate::DavMultiStatusLimits,
1083) -> Result<DavResponse, DavMutationComposeError> {
1084    let stop_status = outcome.stop.map(stop_status);
1085    let partial_execution = outcome.progress.completed_mutations != 0;
1086    let request_failure = !partial_execution
1087        && outcome.stop.is_none()
1088        && outcome.failures.len() == 1
1089        && outcome.failures[0].path() == &outcome.request_target;
1090    if request_failure {
1091        return direct_failure_response(prefix, &outcome.failures[0]);
1092    }
1093    if !outcome.failures.is_empty() || stop_status.is_some_and(|_| partial_execution) {
1094        let mut failures = outcome.failures.clone();
1095        if let Some(status) = stop_status
1096            && !failures.iter().any(|failure| {
1097                failure.path() == &outcome.request_target
1098                    && failure.status_code() == status.as_u16()
1099            })
1100        {
1101            failures.push(DavMutationFailure::status(
1102                outcome.request_target.clone(),
1103                status.as_u16(),
1104            ));
1105        }
1106        return mutation_multistatus_response_with_limits(prefix, &failures, limits)
1107            .map_err(Into::into);
1108    }
1109    if let Some(status) = stop_status {
1110        return Ok(DavResponse::empty(status));
1111    }
1112    Ok(match outcome.operation {
1113        DavMutationOperation::Delete => delete_success_response(),
1114        DavMutationOperation::Copy | DavMutationOperation::Move => {
1115            mutation_success_response(outcome.destination_existed)
1116        }
1117    })
1118}
1119
1120fn stop_status(stop: DavMutationStop) -> StatusCode {
1121    match stop {
1122        DavMutationStop::Cancelled => StatusCode::SERVICE_UNAVAILABLE,
1123        DavMutationStop::Backend => StatusCode::INTERNAL_SERVER_ERROR,
1124        DavMutationStop::InsufficientStorage => StatusCode::INSUFFICIENT_STORAGE,
1125    }
1126}
1127
1128fn direct_failure_response(
1129    prefix: &str,
1130    failure: &DavMutationFailure,
1131) -> Result<DavResponse, DavMutationComposeError> {
1132    let status =
1133        StatusCode::from_u16(failure.status_code()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
1134    if let Some(lock_path) = failure.lock_path() {
1135        return xml_document_response(
1136            status,
1137            &dav_error_element(&DavErrorCondition::LockTokenSubmitted {
1138                href: crate::href_for_dav_path(prefix, lock_path),
1139            }),
1140        )
1141        .map_err(Into::into);
1142    }
1143    Ok(no_store_empty_response(status))
1144}
1145
1146/// Failure while composing the final recursive mutation response.
1147#[derive(Debug, thiserror::Error)]
1148pub enum DavMutationComposeError {
1149    #[error("failed to compose mutation Multi-Status response")]
1150    MultiStatus(#[from] DavMultiStatusError),
1151    #[error("failed to compose mutation DAV error response")]
1152    Xml(#[from] DavXmlError),
1153}
1154
1155#[cfg(test)]
1156mod tests {
1157    use super::*;
1158
1159    #[test]
1160    fn invalid_private_child_state_stops_transfer_merge() {
1161        let source = Child {
1162            name: bytes::Bytes::from_static(b"invalid/name"),
1163            key: bytes::Bytes::from_static(b"source-key"),
1164            path: DavPath::new("/source/item").unwrap(),
1165            is_collection: false,
1166        };
1167        let destination = Child {
1168            name: bytes::Bytes::from_static(b"invalid/name"),
1169            key: bytes::Bytes::from_static(b"destination-key"),
1170            path: DavPath::new("/destination/item").unwrap(),
1171            is_collection: false,
1172        };
1173        let mut frames = [Frame {
1174            parent: None,
1175            failed: false,
1176        }];
1177        let mut stop = None;
1178
1179        let work = merge_transfer_children_or_stop(
1180            &DavPath::new("/destination/").unwrap(),
1181            &[source],
1182            &[destination],
1183            1,
1184            0,
1185            &mut frames,
1186            &mut stop,
1187        );
1188
1189        assert!(work.is_none());
1190        assert_eq!(stop, Some(DavMutationStop::Backend));
1191    }
1192}