Skip to content

Eliminate deep recursion risk in cross-disk directory copy (cross_copy_single) - #8

Merged
koraytaylan merged 1 commit into
developfrom
fix/3-eliminate-deep-recursion-in-cross-disk-copy
May 19, 2026
Merged

Eliminate deep recursion risk in cross-disk directory copy (cross_copy_single)#8
koraytaylan merged 1 commit into
developfrom
fix/3-eliminate-deep-recursion-in-cross-disk-copy

Conversation

@koraytaylan

Copy link
Copy Markdown
Owner

Summary

This PR fixes the deep recursion risk in cross_copy_single (used by cross-disk copy/move) as described in #3.

The Problem

The original implementation used unbounded recursion (with Box::pin) for directory trees. Each nesting level added a stack frame, risking stack overflow (and app crash) on deep directory structures (e.g. 1000s of levels from backups or generated data).

The Fix

  • Replaced the recursive logic with an explicit VecDeque-based iterative work queue (BFS traversal) inside cross_copy_single.
  • All pending directory/file work items live on the heap; Rust call-stack depth is now constant (O(1) w.r.t. tree depth).
  • Updated docs to explain the safety improvement.
  • Added a regression test that creates + copies a 60-level-deep nested directory tree between two MemoryBackends (would have overflowed before).

Verification

  • All existing tests continue to pass (cross_copy_single_* , run_cross_copy_loop_*, run_cross_move_loop_*).
  • New deep-tree test passes.
  • cargo clippy -- -D warnings clean.
  • No behavior change for top-level callers, progress reporting, or cooperative cancellation (checks remain in the outer loops).

Closes #3

Files changed

  • backend/src/commands/file.rs: core fix + test

This meets all acceptance criteria from the issue.

…s_copy_single) (#3)

- Rewrite cross_copy_single to use explicit VecDeque work queue (BFS) instead of recursion.
  This removes the stack growth per directory level that could cause overflow
  on deep trees (even with Box::pin).
- Update documentation for the function.
- Add regression test 'cross_copy_single_deep_directory_tree' that builds and
  successfully copies a 60-level nested directory tree using MemoryBackend.
- Verified: cargo check, all cross_* tests (incl. run loops), clippy -D warnings pass.
- Behavior for callers, cancellation (outer loop), and progress unchanged.

Closes #3
@koraytaylan
koraytaylan requested a review from Copilot May 19, 2026 21:30
@koraytaylan
koraytaylan merged commit 1c26e01 into develop May 19, 2026
8 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors cross_copy_single (used by cross-backend copy/move) to avoid unbounded recursion when copying directory trees, preventing potential stack overflow on extremely deep directory hierarchies.

Changes:

  • Replaced recursive directory traversal in cross_copy_single with an iterative VecDeque work queue.
  • Updated function-level documentation to explain the recursion-safety motivation and approach.
  • Added a regression test that copies a 60-level deep directory tree between two MemoryBackends.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +212 to 224
while let Some((current_src, current_dst)) = pending.pop_front() {
let stat = src.stat(&current_src).await?;
if stat.is_dir {
dst.create_dir(&current_dst).await?;
let children = src.list(&current_src).await?;
for child in children {
let child_dst = format!("{}/{}", current_dst.trim_end_matches('/'), child.name);
pending.push_back((child.path, child_dst));
}
} else {
let data = src.read(&current_src).await?;
dst.write(&current_dst, &data).await?;
}
Comment on lines +209 to +220
let mut pending: VecDeque<(String, String)> = VecDeque::new();
pending.push_back((src_path.to_string(), dst_path.to_string()));

while let Some((current_src, current_dst)) = pending.pop_front() {
let stat = src.stat(&current_src).await?;
if stat.is_dir {
dst.create_dir(&current_dst).await?;
let children = src.list(&current_src).await?;
for child in children {
let child_dst = format!("{}/{}", current_dst.trim_end_matches('/'), child.name);
pending.push_back((child.path, child_dst));
}
Comment on lines +1109 to +1112
/// Regression test for deep directory trees (prevents stack overflow in cross-copy).
/// Creates a 60-level deep nested directory tree with a file at the bottom
/// and verifies that iterative copy succeeds where recursion would have overflowed.
#[tokio::test]
koraytaylan added a commit that referenced this pull request May 19, 2026
) (#12)

- Pass cancel_flag (as Option<&AtomicBool>) into cross_copy_single and
  check it inside the DFS work loop for finer-grained cooperative
  cancellation during deep/wide cross-disk trees.
- Update run_cross_copy_loop / run_cross_move_loop to pass the flag and
  re-check after the call so mid-tree cancels (incl. last top-level item)
  correctly transition the job to Cancelled instead of Completed/Failed.
- Switch work queue from VecDeque (BFS) to Vec (explicit DFS/LIFO stack)
  to address memory concerns on wide directories; update docs explaining
  the improved peak-mem characteristics (drop full Entry metadata early,
  only retain path strings for pending siblings).
- Reword deep-tree test as regression guard + bump from 60 to 1000 levels
  (now credibly exercises the 2k+ risk range from original #3).
- Update module docs for the improved cancellation granularity.
- All callers/tests updated; no behavior change for normal cases.
- cargo test + clippy -D warnings clean.

Addresses all three points from Copilot review on PR #8 (follow-up to #3).
Closes #11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants