Skip to content

fix(file_ops): safety hardening — TOCTOU docs, error propagation, dedup, tests#77

Merged
leszek3737 merged 2 commits into
mainfrom
fab1
Jun 13, 2026
Merged

fix(file_ops): safety hardening — TOCTOU docs, error propagation, dedup, tests#77
leszek3737 merged 2 commits into
mainfrom
fab1

Conversation

@leszek3737

@leszek3737 leszek3737 commented Jun 13, 2026

Copy link
Copy Markdown
Owner
  • Coordination: centralize MSG_DEST_EXISTS, MSG_CRITICAL_DIR, MSG_SYMLINK_CHMOD into common.rs; add is_dir_reparse() helper; gate chmod on #[cfg(unix)]
  • WS-A: reject_same_file canonicalize fallback fixed (and_then + is_ok_and instead of ok().zip().is_some_and); strip_prefix debug_log on invariant violation; remove_any non-adversarial contract doc; collapse check_canceled/check_optional_canceled; error source chain doc
  • WS-B: TOCTOU race in publish_temp_dir (overwrite=false) documented; validate_copy_targets extracted; preserve_timestamps on top-level dir after publish_temp_dir; TEMP_DIR_COUNTER — AtomicU64, atomic rename sequence documented in 3 functions; reservation patterns doc
  • WS-C: .map(|_bytes| ()) comment; create_dir_all rationale doc; MSG_SYMLINK_CHMOD dedup; rename_entry source/dest inode guard
  • WS-D: is_canceled single post-loop check; magic const rationales in chunk_copy.rs; #[cfg(unix)] on chmod re-export
  • Code review: root guard comment corrected (Unix only); /tmp added to CRITICAL_DIRS; same-inode symlink error clarified
  • Tests: 11 new — overwrite:true copy, canceled+errors format_summary, move cancel, error kind assertions, normalize_suffix (5 subtests), root directory rejection, overwrite.rs conflict tests

Summary by Sourcery

Harden file operations and batch processing against edge cases and TOCTOU concerns while improving error messaging and cancellation handling.

Bug Fixes:

  • Fix same-file detection on non-Unix/Windows platforms to avoid false positives when destinations are missing and ensure metadata errors are propagated.
  • Prevent publish_temp_dir from racing with concurrent destination creation when overwrite is false by re-checking destination existence just before rename.
  • Ensure batch move/copy and delete operations classify filesystem entries consistently, including symlinks, so that the correct copy/delete paths are taken.
  • Make batch cancellation robust by adding a final cancel check after the main processing loop and ensuring canceled moves report canceled status even with errors.
  • Propagate receiver disconnection in chunked copy as an Interrupted error instead of silently continuing, and tighten error messages for cross-device move rollback failures.
  • Reject deletes of root and critical system directories more reliably across platforms, including /tmp on Unix, and clarify root guard behavior on Windows drives.

Enhancements:

  • Centralize shared error messages and directory/symlink helpers for file operations, including directory reparse detection and critical-directory messages.
  • Document TOCTOU constraints, crash-safety behavior, and error-source limitations around path canonicalization, temp publishing, and reservation patterns.
  • Refine rename_entry validation and error reporting, including explicit same-inode and dest-exists handling while keeping TOCTOU notes documented.
  • Ensure directory copy preserves top-level timestamps after publishing temp directories and add rationale comments for buffer sizes and progress throttling.
  • Simplify cancellation utilities by separating required vs optional cancel checks and tightening batch progress handling.
  • Clean up test-only temp dirs and improve robustness of tests around critical directory deletion behavior.

Tests:

  • Add batch tests for overwrite:true copy behavior, cancellation summaries, move cancellation, and expected error kinds for missing sources.
  • Add normalize_suffix unit tests covering parent traversal, absolute paths, over-root errors, and no-op components.
  • Extend overwrite conflict dialog tests to cover no-pending-action, overwrite-true non-conflicts, duplicate source conflicts, and nonexistent destination handling.
  • Adjust existing delete_dir_recursive tests to assert root and critical-prefix protections rather than specific system paths.

…up, tests

- Coordination: centralize MSG_DEST_EXISTS, MSG_CRITICAL_DIR, MSG_SYMLINK_CHMOD
  into common.rs; add is_dir_reparse() helper; gate chmod on #[cfg(unix)]
- WS-A: reject_same_file canonicalize fallback fixed (and_then + is_ok_and
  instead of ok().zip().is_some_and); strip_prefix debug_log on invariant
  violation; remove_any non-adversarial contract doc; collapse
  check_canceled/check_optional_canceled; error source chain doc
- WS-B: TOCTOU race in publish_temp_dir (overwrite=false) documented;
  validate_copy_targets extracted; preserve_timestamps on top-level dir after
  publish_temp_dir; TEMP_DIR_COUNTER — AtomicU64, atomic rename sequence
  documented in 3 functions; reservation patterns doc
- WS-C: .map(|_bytes| ()) comment; create_dir_all rationale doc;
  MSG_SYMLINK_CHMOD dedup; rename_entry source/dest inode guard
- WS-D: is_canceled single post-loop check; magic const rationales in
  chunk_copy.rs; #[cfg(unix)] on chmod re-export
- Code review: root guard comment corrected (Unix only); /tmp added to
  CRITICAL_DIRS; same-inode symlink error clarified
- Tests: 11 new — overwrite:true copy, canceled+errors format_summary,
  move cancel, error kind assertions, normalize_suffix (5 subtests),
  root directory rejection, overwrite.rs conflict tests
@sourcery-ai

sourcery-ai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Reviewer's Guide

Hardens file/batch operations against TOCTOU and error-handling issues by centralizing shared messages/helpers, refining delete/move/copy semantics (including symlink and critical-dir handling), tightening rename/chmod behavior, and expanding regression tests for overwrite, cancellation, and error reporting.

Flow diagram for classify_entry-based copy/delete dispatch

flowchart LR
    SrcPath[Path] --> Classify[classify_entry]
    Classify --> KindSymlink{EntryKind::Symlink}
    Classify --> KindDir{EntryKind::Dir}
    Classify --> KindFile{EntryKind::File}

    KindSymlink --> CopySymlink[copy_entry: file_ops::copy_symlink]
    KindDir --> CopyDir[copy_entry: copy_dir_recursive_with_progress]
    KindFile --> CopyFile[copy_entry: copy_file_with_progress]

    KindSymlink --> DelSymlink[batch_delete: file_ops::delete_file]
    KindDir --> DelDir[batch_delete: delete_dir_recursive _cancelable]
    KindFile --> DelFile[batch_delete: file_ops::delete_file]
Loading

File-Level Changes

Change Details Files
Centralized common file-op helpers and messages, tightened cancel handling, and clarified error semantics.
  • Added is_dir_meta()/is_dir_reparse() helpers for platform-aware directory detection and future reparse-point handling.
  • Split check_canceled into mandatory/optional variants and simplified cancel checks using AtomicBool directly.
  • Introduced MSG_DEST_EXISTS, MSG_CRITICAL_DIR, and MSG_SYMLINK_CHMOD constants and wired them into ensure_destination_absent, rename_entry, delete validation, and chmod.
  • Hardened non-unix reject_same_file by using symlink_metadata, explicit not-found handling, and canonicalize() via is_ok_and.
  • Documented error-source loss in path_contains and added canonicalize_existing_path as a named hook for future validation.
  • Improved canonicalize_with_nearest_existing_parent with a debug_log on strip_prefix invariant violation and refactored normalize_suffix plus tests for various path edge cases.
  • Reimplemented remove_any using symlink_metadata, is_dir_meta, and symlink-aware dispatch, with explicit non-adversarial TOCTOU contract documentation.
src/ops/file_ops/common.rs
Refined delete behavior with platform-specific critical-directory lists, symlink handling, and shared error messages.
  • On Windows, delete symlinks using is_dir_meta-based dispatch; on other platforms keep existing behavior.
  • Replaced macro-based CRITICAL_DIRS/CRITICAL_DIR_PREFIXES with explicit arrays, adding /tmp and documenting Unix vs Windows root handling.
  • Switched delete critical-dir errors to use MSG_CRITICAL_DIR and clarified comments around canonical root guard.
src/ops/file_ops/delete.rs
src/ops/file_ops/common.rs
src/ops/file_ops/mod.rs
Improved batch copy/move/delete control flow, entry classification, cancellation semantics, and error reporting, with new tests.
  • Introduced EntryKind and classify_entry() for unified symlink/dir/file classification, reusing it in copy_entry and batch_delete.
  • Simplified copy_entry and batch_delete match logic around classify_entry results, reducing duplicated symlink/dir/file detection.
  • Adjusted is_canceled usage to perform a single post-loop cancel check to catch cancellation signaled during the final iteration.
  • Enhanced batch_delete loop to rely on the shared canceled flag rather than inline cancel checks.
  • Standardized duplicate-destination test strings with EXPECTED_DUP_MSG constant and simplified nonexistent path construction in tests.
  • Added tests covering overwrite:true copy behavior, move cancellation reporting, format_summary semantics when canceled with/without errors, and error kind expectations for missing sources.
src/ops/batch.rs
src/ops/batch/tests.rs
Strengthened temp-dir publishing, move operations, and directory copy behavior for better TOCTOU resilience and rollback behavior.
  • Documented two-phase publish_temp_dir atomicity/crash behavior and rechecked dest existence via symlink_metadata when overwrite=false, returning AlreadyExists on new races.
  • On publish_temp_dir failure with backup, log backup-restore failures via debug_log instead of wrapping them into a new error.
  • Adjusted swap_temp_to_dest to use fs::rename consistently and updated reserve_temp_file_for to use TEMP_NAME_MAX_ATTEMPTS with a generic exhausted-attempts message.
  • Refactored MoveKind::copy_entry to discard byte counts explicitly and MoveKind::remove_src to use remove_any for symlink-safe removal.
  • In move_ops copy_then_remove_src, changed rollback failure handling to return a composed error describing both cancel and rollback failures instead of only logging.
  • Documented copy_file_with_progress validation strategy and updated copy_dir_recursive_with_progress to preserve timestamps on the top-level destination directory and explain redundant metadata calls.
src/ops/file_ops/temp.rs
src/ops/file_ops/move_ops.rs
src/ops/file_ops/copy.rs
src/ops/file_ops/common.rs
Tightened entry-ops behavior (create_directory, rename, chmod), progress-copy internals, exports, and overwrite-dialog behavior, plus safety-focused tests.
  • Documented create_directory rationale (create_dir_all with traversal rejection) and split rename_entry into validate_entry_name + core logic, adding same-inode/try_exists commentary and using MSG_DEST_EXISTS.
  • Refined chmod to use MSG_SYMLINK_CHMOD for symlink errors and changed macOS EFTYPE mapping to a more generic "unsupported file type for chmod" message; gated public chmod export behind cfg(unix).
  • Documented chunk_copy constants (BUFFER_SIZE, PROGRESS_THROTTLE, PROGRESS_CHECK_BYTES) and simplified progress receiver handling to treat disconnection as an Interrupted error immediately.
  • Relaxed a test helper to use unwrap_or_default() on SystemTime::now - UNIX_EPOCH, and retargeted delete-dir tests from /etc to root and prefixed dir paths to avoid permission heuristics.
  • Added overwrite.rs tests for overwrite conflict detection: pending_action=None, overwrite=true with no conflicts, duplicate sources in different dirs producing conflicts, and nonexistent dest handling without panic.
src/ops/file_ops/entry_ops.rs
src/ops/chunk_copy.rs
src/ops/file_ops/mod.rs
src/lib.rs
src/tests/overwrite.rs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request introduces several refactorings, platform-specific compatibility improvements, and robust error handling across file operations. Key changes include classifying file entries using a new EntryKind enum, refining cancellation checks in batch operations, standardizing error messages, and adding extensive unit tests for overwrite conflicts and suffix normalization. Additionally, Windows-specific directory metadata checks and macOS-specific chmod error mappings were improved. Feedback on these changes highlights that treating timestamp preservation failures as fatal during directory copies could cause operations to fail on unsupported filesystems (e.g., FAT32/exFAT); it is recommended to log a warning instead of returning an error.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/ops/file_ops/copy.rs Outdated
));
}
publish_temp_dir(guard.path(), dest, overwrite, src_perms)?;
preserve_timestamps(dest, &fs::metadata(src)?)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Treating preserve_timestamps as a fatal error can cause copy operations to fail completely on filesystems that do not support setting timestamps (such as some FAT32/exFAT USB drives or network mounts), even though the directory and all its files were successfully copied and published. Making timestamp preservation failures non-fatal (by logging a warning instead of returning an error) makes the copy operation much more robust.

Suggested change
preserve_timestamps(dest, &fs::metadata(src)?)?;
if let Err(e) = preserve_timestamps(dest, &fs::metadata(src)?) {
crate::debug_log!("warning: failed to preserve timestamps for {}: {}", dest.display(), e);
}

@sourcery-ai sourcery-ai Bot 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.

Hey - I've left some high level feedback:

  • Now that classify_entry and remove_any both encapsulate similar file-type dispatch logic (symlink/dir/file), consider reusing one from the other (or sharing a single helper) to avoid subtle divergence in behavior over time.
  • The MSG_DEST_EXISTS usage is inconsistent across call sites (e.g., copy_file includes the destination path while rename_entry does not); for easier debugging, consider standardizing the error format to always include the destination path.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Now that `classify_entry` and `remove_any` both encapsulate similar file-type dispatch logic (symlink/dir/file), consider reusing one from the other (or sharing a single helper) to avoid subtle divergence in behavior over time.
- The `MSG_DEST_EXISTS` usage is inconsistent across call sites (e.g., `copy_file` includes the destination path while `rename_entry` does not); for easier debugging, consider standardizing the error format to always include the destination path.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@greptile-apps

greptile-apps Bot commented Jun 13, 2026

Copy link
Copy Markdown

Greptile Summary

This PR hardens the file-operations layer across four workstreams: TOCTOU documentation and narrow-window re-checks in copy/temp, deduplication of shared string constants into common.rs, cancellation and error-propagation cleanups in batch and move ops, and 11 new unit/integration tests.

  • batch_delete had its end-of-loop is_canceled call removed without the compensating post-loop check added to execute_batch_generic; a cancel set during the progress callback of the last deleted item will not be reflected in BatchReport.canceled.
  • publish_temp_dir uses a hardcoded \"destination already exists: ...\" string instead of MSG_DEST_EXISTS, diverging from every other callsite.
  • The doc comment describing rename_entry was accidentally placed on the extracted private helper validate_entry_name.

Confidence Score: 4/5

Safe to merge with one fix: batch_delete can silently return canceled: false when the user cancels during the progress callback of the last item in a delete batch.

The rest of the PR — TOCTOU docs, constant deduplication, preserve_timestamps, classify_entry refactor, and the 11 new tests — all look correct. The asymmetry between batch_delete and execute_batch_generic cancel handling is a real gap in cancel reporting for delete operations.

src/ops/batch.rs — the batch_delete function, lines 790–793, is missing the post-loop cancel check.

Important Files Changed

Filename Overview
src/ops/batch.rs Refactors entry classification into classify_entry, consolidates cancel checks — but removes the end-of-loop is_canceled call from batch_delete without adding the compensating post-loop check added to execute_batch_generic, leaving a cancel-detection gap for the last deleted item.
src/ops/file_ops/common.rs Adds is_dir_meta/is_dir_reparse helpers, inlines check_canceled, centralises MSG_DEST_EXISTS/MSG_CRITICAL_DIR/MSG_SYMLINK_CHMOD constants, rewrites remove_any to stat-dispatch, and adds normalize_suffix unit tests. Changes look correct.
src/ops/file_ops/temp.rs Adds a pre-rename dest-existence re-check for overwrite=false in publish_temp_dir (TOCTOU hardening), switches error restore to a debug_log path, and upgrades TEMP_DIR_COUNTER to AtomicU64. One callsite uses a hardcoded string instead of MSG_DEST_EXISTS.
src/ops/file_ops/move_ops.rs Switches src.is_dir() to src_meta.file_type().is_dir() (avoids following symlinks), upgrades rollback-failure path to return a rich combined error, and simplifies remove_src for symlinks via remove_any. Changes are correct.
src/ops/file_ops/entry_ops.rs Extracts validate_entry_name, deduplicates MSG_SYMLINK_CHMOD/MSG_DEST_EXISTS usage, and corrects EFTYPE error message. Doc comment describing rename_entry was placed on the extracted private helper instead.
src/ops/file_ops/delete.rs Expands macro-generated CRITICAL_DIRS/CRITICAL_DIR_PREFIXES into explicit arrays, adds /tmp to Linux lists, fixes Windows remove_symlink to avoid a double-stat, and deduplicates the critical-dir error message via MSG_CRITICAL_DIR.
src/ops/chunk_copy.rs Moves receiver_alive guard inline to immediately return on send error (correct), adds rationale comments for the three magic constants. No logic issues.
src/ops/file_ops/copy.rs Adds preserve_timestamps call on the top-level dest directory after publish_temp_dir, uses MSG_DEST_EXISTS, and adds an explanatory comment for the redundant metadata stat. Correct.
src/ops/batch/tests.rs Adds 7 new test cases (overwrite:true copy, cancel detection, format_summary variants, error-kind assertions) and simplifies nonexistent-path construction. All look correct.
src/tests/overwrite.rs Adds 4 integration tests for check_overwrite_conflict covering None action, overwrite:true no-conflict, duplicate sources conflict, and nonexistent dest. Tests are correct.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[execute_batch_generic] --> B{top-of-loop\nis_canceled?}
    B -- yes --> C[state.canceled = true\nbreak]
    B -- no --> D[process_batch_entry]
    D --> E{Interrupted +\nis_canceled?}
    E -- yes --> F[state.canceled = true]
    E -- no --> G[record result]
    F --> H{state.canceled?}
    G --> H
    H -- yes --> I[break]
    H -- no --> B
    I --> J[post-loop final check]
    J -- is_canceled? --> K[state.canceled = true]
    J --> L[return BatchReport]
    K --> L

    M[batch_delete] --> N{top-of-loop\nis_canceled?}
    N -- yes --> O[canceled = true / break]
    N -- no --> P[classify + delete]
    P --> Q{Interrupted +\nis_canceled?}
    Q -- yes --> R[canceled = true]
    Q -- no --> S[record result]
    R --> T{canceled?}
    S --> T
    T -- yes --> U[break]
    T -- no --> N
    U --> V[return BatchReport]
    N -. last item .-> V
    style J fill:#90EE90
    style K fill:#90EE90
    style V fill:#FFB6C1
Loading

Comments Outside Diff (1)

  1. src/ops/batch.rs, line 790-793 (link)

    P1 Missing post-loop cancel check in batch_delete

    The end-of-loop is_canceled(cancel) call was removed from batch_delete without adding the compensating post-loop check that was correctly added to execute_batch_generic. If the cancel flag is set during the report_progress callback that fires after the last item is processed (lines 777–789), neither the removed end-of-loop check nor the top-of-loop check (which won't fire again) will see it. The loop exits naturally and BatchReport { canceled: false } is returned even though the user canceled. execute_batch_generic now has the pattern after its loop for exactly this reason — the same fix is needed here.

Reviews (1): Last reviewed commit: "fix(file_ops): safety hardening — TOCTOU..." | Re-trigger Greptile

Comment thread src/ops/file_ops/temp.rs
Comment on lines +132 to +137
Ok(_) => {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("destination already exists: {}", dest.display()),
));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 MSG_DEST_EXISTS constant not used here, unlike every other callsite in the module. If the constant text changes, this message will silently diverge.

Suggested change
Ok(_) => {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("destination already exists: {}", dest.display()),
));
}
Ok(_) => {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("{MSG_DEST_EXISTS}: {}", dest.display()),
));
}

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment thread src/ops/file_ops/entry_ops.rs Outdated
Comment on lines +29 to +36
/// Renames a filesystem entry within its parent directory.
///
/// Validates that `new_name` is a single filename component (no separators, no `..`).
/// Detects same-inode renames (e.g., case-only rename on case-insensitive FS).
/// On POSIX, the `try_exists` guard is best-effort — `fs::rename` atomically
/// replaces the destination; true atomic no-replace requires `RENAME_NOREPLACE`
/// or `renamex_np`, which are out of stdlib.
fn validate_entry_name(new_name: &str) -> io::Result<()> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 The doc comment beginning "Renames a filesystem entry within its parent directory" describes rename_entry, not the private validate_entry_name helper extracted from it. The comment should sit on the public function.

Suggested change
/// Renames a filesystem entry within its parent directory.
///
/// Validates that `new_name` is a single filename component (no separators, no `..`).
/// Detects same-inode renames (e.g., case-only rename on case-insensitive FS).
/// On POSIX, the `try_exists` guard is best-effort — `fs::rename` atomically
/// replaces the destination; true atomic no-replace requires `RENAME_NOREPLACE`
/// or `renamex_np`, which are out of stdlib.
fn validate_entry_name(new_name: &str) -> io::Result<()> {
fn validate_entry_name(new_name: &str) -> io::Result<()> {

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@opencode-agent

Copy link
Copy Markdown

All tests pass, clippy is clean, and formatting is compliant. Here's my comprehensive review:


PR Review: Safety Hardening for File Operations

Overall: This is a well-intentioned hardening PR with good documentation, useful refactoring (classify_entry), and legitimate fixes (reject_same_file, /tmp guard, final cancel check). Build, clippy, and all 273 tests pass. However, there are a few issues worth addressing.


Bugs / Behavioral Concerns

1. publish_temp_dir swallows restore error — regression in data-loss visibility (src/ops/file_ops/temp.rs:160-166)

When the backup restore fails in the error path, the error is only debug_log!-ed and not propagated. The caller receives the original rename error with no indication that user data is now trapped in an orphaned .lc-dir-backup-*.tmp/dest path. The previous code propagated this error. Worse, the subsequent fs::remove_dir(&backup.container) (line 167) will also silently fail because the container still holds the un-restored entry — a cascade of silent failures in a data-loss scenario.

Consider propagating the restore error (wrapped with context) so the caller can surface it, or at minimum document this behavior in the doc comment.

2. batch_delete missing final post-loop cancel check (src/ops/batch.rs:790-793)

The PR correctly added a post-loop is_canceled check to execute_batch_generic (lines 504-508) to catch cancel signals set during the last item's progress callback. However, batch_delete had its in-loop is_canceled check removed (diff lines 128-130) but received no equivalent post-loop check. If cancel is set after the last delete completes, batch_delete won't report canceled: true:

// batch_delete — current code after the loop (line 793):
// (no final is_canceled check)

// execute_batch_generic — current code after the loop (line 504-508):
if !state.canceled && is_canceled(cancel) {
    state.canceled = true;
}

This should be made consistent.


Code Quality / Convention Issues

3. is_dir_reparse is dead code (src/ops/file_ops/common.rs:46-49)

This function is defined with #[allow(dead_code)] but is never referenced anywhere in the codebase. This violates both YAGNI and the project's AGENTS.md convention: "No #[allow(...)] to suppress lints except: unwrap_used/expect_used/panic on tests". It should be removed or actually used.

4. Missed message deduplication in temp.rs (src/ops/file_ops/temp.rs:135)

The PR's stated goal is to "Centralize MSG_DEST_EXISTS... into common.rs", but publish_temp_dir still uses a raw string literal:

format!("destination already exists: {}", dest.display()),

instead of the centralized MSG_DEST_EXISTS constant. This should use format!("{MSG_DEST_EXISTS}: {}", dest.display()) for consistency with common.rs:79 and copy.rs:55.

5. Dead branch in remove_any on non-Windows (src/ops/file_ops/common.rs:251)

if meta.is_symlink() && is_dir_meta(&meta) {

On non-Windows, is_dir_meta(meta) is meta.is_dir(), which is always false for symlinks (from symlink_metadata). This branch is only reachable on Windows. Functionally correct, but misleading — a reader might think directory symlinks are handled differently on Unix. Consider adding a #[cfg(windows)] guard or a clarifying comment.

6. Overly broad visibility on constants (src/ops/file_ops/common.rs:73,223,224)

MSG_DEST_EXISTS, MSG_CRITICAL_DIR, and MSG_SYMLINK_CHMOD are declared pub in a pub(super) module. Since the module common is pub(super), these should be pub(super) for consistency with the rest of the module's API surface.


Minor Issues

7. Test name doesn't match behavior (src/ops/batch/tests.rs)

batch_copy_error_has_expected_kind (line ~276) is named as if it checks io::ErrorKind, but it actually checks error message substrings ("entity not found", "No such file", "Not Found"). This is because BatchReport.errors stores Vec<String>, not structured errors. Consider renaming to batch_copy_error_message_indicates_not_found.

8. batch_move_cancel_reports_canceled doesn't exercise the final-check fix (src/ops/batch/tests.rs)

The test's progress callback sets cancel=true via report_file_active before the action runs, causing the action to fail with Interrupted, which sets state.canceled via the error path (line 614) — not via the new final post-loop check (line 506). The specific edge case the fix targets (cancel set after the last item succeeds) isn't actually tested. To test it, you'd need a scenario where the last action succeeds, then cancel is set.

9. Error kind change in copy_then_remove_src (src/ops/file_ops/move_ops.rs:174-185)

When both cancel and rollback fail, the error kind changes from Interrupted to Other. The new message is more informative, and it's currently safe since batch processing checks is_canceled directly rather than inspecting error kinds. Worth noting in case future code adds ErrorKind::Interrupted matching on move results.


Positive Aspects

  • classify_entry centralization (batch.rs) — eliminates triplicated type-dispatch logic across copy_entry and batch_delete
  • Thorough TOCTOU documentation — the remove_any, publish_temp_dir, and rename_entry doc comments are excellent
  • reject_same_file fix for non-Unix/Windows (common.rs:127-147) — now propagates metadata errors instead of silently treating them as "not same file"
  • src.is_dir()src_meta.file_type().is_dir() (move_ops.rs:135) — avoids a redundant syscall and is more correct (doesn't follow symlinks)
  • /tmp addition to CRITICAL_DIRS — good safety improvement; is_under_temp correctly exempts legitimate temp-dir usage
  • chunk_copy.rs simplification — removing the receiver_alive flag for immediate return is cleaner and equivalent
  • Magic constant rationales (chunk_copy.rs) — excellent inline documentation of BUFFER_SIZE, PROGRESS_THROTTLE, PROGRESS_CHECK_BYTES

New%20session%20-%202026-06-13T11%3A18%3A13.383Z
opencode session  |  github run

…cel check

- copy.rs: preserve_timestamps failure now non-fatal (warn+continue) for
  FAT32/exFAT compatibility (gemini-code-assist)
- temp.rs: use MSG_DEST_EXISTS constant instead of raw string (greptile)
- entry_ops.rs: move rename_entry doc comment to correct function (greptile)
- common.rs: remove dead is_dir_reparse() (greptile — YAGNI + lint policy)
- common.rs: clarify Windows-only branch in remove_any (greptile)
- batch.rs: add missing post-loop cancel check to batch_delete (greptile)
@opencode-agent

Copy link
Copy Markdown

I've completed my thorough review. Here's my analysis:


PR Review: Safety Hardening for File Operations

CI Status: All green — cargo fmt clean, cargo clippy -D warnings clean, 273 tests pass, release build succeeds.

Important Context

This PR has two commits. The second commit (f558816) addresses feedback from the first. Several automated reviewers (Greptile, opencode-agent) reviewed only commit 1 (4615fc3) and flagged issues that are already fixed in commit 2:

Flagged issue Status
batch_delete missing post-loop cancel check Fixedbatch.rs:796 now mirrors execute_batch_generic:506
publish_temp_dir hardcoded string instead of MSG_DEST_EXISTS Fixedtemp.rs:135 now uses the constant
entry_ops.rs doc comment misplaced on validate_entry_name Fixed — doc is correctly on rename_entry:58
is_dir_reparse dead code with #[allow(dead_code)] Removed — no longer present
Fatal preserve_timestamps error on FAT32/exFAT Fixed — now non-fatal debug_log! warning (copy.rs:131-135)

Remaining Concerns

1. publish_temp_dir restore-error regression (temp.rs:160-166) — Medium

The base branch propagated the restore error with a descriptive message:

fs::rename(&backup.entry, dest).map_err(|restore_err| {
    io::Error::other(format!("failed to restore destination {} ...", ...))
})?;

This PR changed it to a bare debug_log!, returning only the original rename error. In a data-loss-adjacent scenario (rename fails + restore also fails), the user's data is orphaned in a .lc-dir-backup-*.tmp container with no signal in the returned error.

Notably, copy_then_remove_src in the same PR does the opposite — it composes a rich combined error (move_ops.rs:176-183). These two functions handle the same class of problem (primary + rollback both fail) inconsistently. Consider aligning publish_temp_dir with the move_ops.rs pattern.

2. Test doesn't exercise the final-check fix (batch/tests.rs:757-778) — Low

batch_move_cancel_reports_canceled sets cancel=true in the progress callback, but the action then fails with Interrupted before completing — so state.canceled is set via the error path (process_batch_entry), not via the new post-loop check at batch.rs:506. The edge case the fix targets (cancel set after the last item succeeds) isn't actually tested.

3. MSG_DEST_EXISTS message inconsistency (entry_ops.rs:86) — Low

ensure_destination_absent, copy_file, and publish_temp_dir all format as "{MSG_DEST_EXISTS}: {path}", but rename_entry uses the bare constant without the destination path. For debuggability, include the path here too.

Verified Correct

  • classify_entry — clean refactoring, eliminates triplicated type-dispatch; symlink_metadata correctly classifies symlinks vs dirs vs files
  • reject_same_file (non-unix/windows) — now propagates metadata errors instead of silently treating them as "not same file"; the and_then/is_ok_and chain is logically equivalent to the old ok().zip() but doesn't discard errors
  • remove_any rewrite — stat-first dispatch is correct across platforms; the Windows-only branch (line 246) is clearly documented as unreachable on Unix
  • CRITICAL_DIRS expansion/tmp correctly added to Linux lists; macos/non-macos lists verified equivalent to the old macro output
  • move_ops.rssrc.is_dir()src_meta.file_type().is_dir() avoids following symlinks and eliminates a redundant syscall; remove_src for symlinks now delegates to remove_any
  • chunk_copy.rs — removing the receiver_alive flag in favor of immediate return on send error is cleaner and equivalent; magic constant rationale comments are excellent
  • #[cfg(test)] use MSG_DEST_EXISTS in copy.rs — correct, because copy_file itself is #[cfg(test)] (line 39)
  • #[cfg(unix)] on chmod re-export in lib.rs — correct, prevents compilation failure on non-Unix targets
  • normalize_suffix tests — all 5 subtests correctly cover parent traversal, above-root error, absolute path, and . components

Verdict

Solid PR. The refactoring, documentation, and safety improvements are well-executed. The one concern worth addressing before merge is the publish_temp_dir restore-error regression (concern #1) — either propagate a composed error like move_ops.rs does, or explicitly document why swallowing is acceptable here.

New%20session%20-%202026-06-13T12%3A09%3A23.340Z
opencode session  |  github run

@leszek3737
leszek3737 merged commit 28dce7e into main Jun 13, 2026
6 checks passed
@leszek3737
leszek3737 deleted the fab1 branch June 13, 2026 12:26
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.

1 participant