Skip to content

fix(ADFA-2584): Allow file rename for case-only changes#1359

Open
dara-abijo-adfa wants to merge 1 commit into
stagefrom
ADFA-2584-allow-case-only-file-rename
Open

fix(ADFA-2584): Allow file rename for case-only changes#1359
dara-abijo-adfa wants to merge 1 commit into
stagefrom
ADFA-2584-allow-case-only-file-rename

Conversation

@dara-abijo-adfa
Copy link
Copy Markdown
Contributor

Allow file renaming for case-only changes, e.g "numberFragment" to "NumberFragment"

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 2, 2026

Review Change Stack

📝 Walkthrough

Release Notes

New Feature

  • Case-only file rename support: Files can now be renamed with case-only changes (e.g., "numberFragment" → "NumberFragment") using a temporary file approach

Implementation Details

  • When target filename matches current name (case-insensitive), the file is renamed through an intermediate .tmp file before reaching the destination
  • Standard file renaming is used for all other cases via FileUtils.rename()
  • File name validation (1-40 characters) is enforced before notifying observers

⚠️ Risks and Concerns

  • Incomplete cleanup risk: If the first renameTo() call succeeds but the second fails, the file is left as {name}.tmp and becomes inaccessible to users
  • Race condition vulnerability: On multi-threaded systems, concurrent renames to the same temporary filename could cause conflicts
  • Validation timing issue: The success/error emission is based on both the rename operation AND the name length check. Semantic mismatch: onResult?.invoke(renamed) is called with the raw renamed value, but the UI receives the validation-gated success/error message, creating a disconnect between caller feedback and actual validation
  • Limited error context: No distinction in error reporting between rename failure and validation failure
  • Potential file loss: If a file named {name}.tmp already exists, calling file.renameTo(tempFile) will overwrite it

Best Practice Violations

  • Error handling is implicit and lacks granularity (no specific exception types or detailed error codes)
  • File I/O operations lack proper rollback mechanism
  • No logging for debugging rename failures on case-only changes
  • Hard-coded file extension .tmp lacks configurable isolation pattern

Walkthrough

The renameFile method in FileManagerViewModel is refactored to handle case-insensitive file renames. When the target name matches the current file name only in case, it performs a rename through a temporary *.tmp file; otherwise it uses direct rename. Name length validation is deferred to the final success condition.

Changes

File Manager Rename Logic

Layer / File(s) Summary
Rename logic with case-insensitive support
app/src/main/java/com/itsaky/androidide/viewmodel/FileManagerViewModel.kt
The rename operation creates a destination File and branches: if the desired name matches the current name case-insensitively, it renames through a temporary *.tmp intermediate file; otherwise it directly renames via FileUtils.rename. Name length validation (1..40 characters) is moved to the final success condition.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • appdevforall/CodeOnTheGo#1264: Both PRs modify FileManagerViewModel.renameFile, with this PR refining the rename and validation logic while the related PR changed method signature and event handling.

Suggested reviewers

  • itsaky-adfa
  • Daniel-ADFA
  • jomen-adfa

Poem

🐰 A file seeks to change its case with grace,
Through temp files dancing in rename's embrace,
No length too long for our hop and sprint,
Just validate when the deed's complete—that's the hint!
File Manager hops on, bug fixed with a bound! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main change: allowing file rename operations when only the case differs.
Description check ✅ Passed The description is directly related to the changeset, providing a clear example of the use case being addressed.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ADFA-2584-allow-case-only-file-rename

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
app/src/main/java/com/itsaky/androidide/viewmodel/FileManagerViewModel.kt (2)

33-38: 💤 Low value

Optional: Optimize for exact name match case.

When file.name equals newName exactly (not just case-insensitively), this is a no-op rename. The current implementation still performs the two-step rename through a temporary file, which is unnecessary. Consider returning early with success when the names match exactly.

⚡ Proposed optimization
+if (file.name == newName) {
+    return@withContext true
+}
 if (file.name.equals(newName, ignoreCase = true)) {
     val tempFile = File(file.parentFile, "$newName.tmp")
     file.renameTo(tempFile) && tempFile.renameTo(destFile)
 } else {
     FileUtils.rename(file, newName)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/itsaky/androidide/viewmodel/FileManagerViewModel.kt`
around lines 33 - 38, Detect the exact-match no-op before doing the temporary
rename: if file.name == newName (case-sensitive) simply return success instead
of performing the two-step temp rename; update the logic in the
FileManagerViewModel rename flow (the block that currently checks
file.name.equals(newName, ignoreCase = true) and uses tempFile/destFile) to
first check for exact equality and short-circuit, otherwise fall back to the
existing temp-file sequence or FileUtils.rename call.

43-43: 💤 Low value

Align FileRenameEvent construction with rename destination (no file.parent NPE risk)

File(file.parent, newName) won’t fail due to file.parent being nullable—java.io.File(String, String) treats a null parent as new File(child). Still, for consistency with the actual rename path (destFile = File(file.parentFile, newName)), build the event using destFile (or File(file.parentFile, newName)) instead of File(file.parent, newName) at FileManagerViewModel.kt:43.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/itsaky/androidide/viewmodel/FileManagerViewModel.kt` at
line 43, The FileRenameEvent is constructed with File(file.parent, newName)
which is inconsistent with the actual rename target (destFile) and risks
mismatch; update the creation of renameEvent in FileManagerViewModel (the
variable renameEvent) to use the same destFile used for renaming (or construct
with File(file.parentFile, newName)) so the event accurately reflects the
destination path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/main/java/com/itsaky/androidide/viewmodel/FileManagerViewModel.kt`:
- Around line 34-35: The rename sequence uses a fixed temp filename
("$newName.tmp") which can collide; update the logic in FileManagerViewModel
around variables tempFile, file.renameTo(tempFile) and destFile to first ensure
a unique temporary path (e.g., use File.createTempFile or loop to append an
increasing suffix until a non-existent temp is found), fail fast if you cannot
reserve a unique temp, perform file.renameTo(tempFile) then
tempFile.renameTo(destFile), and ensure you clean up the tempFile on any failure
to avoid leaving or overwriting files.
- Around line 34-35: The two-step rename in FileManagerViewModel (where it
creates tempFile and calls file.renameTo(tempFile) &&
tempFile.renameTo(destFile)) lacks rollback: if the first rename succeeds but
the second fails the temp file remains. Change the logic in the renaming method
in FileManagerViewModel to perform the second rename in an if block so you can
detect failure; on failure attempt to move tempFile back to the original file
name (rollback), catch/log any exceptions (use whatever logger is available in
this class), and return false; ensure the method returns true only if both
renames succeed and that all filesystem operations are guarded and error-handled
to avoid leaving a .tmp file behind.
- Line 41: The rename validation is happening after the file is already renamed,
causing filesystem changes when the new name is invalid; in FileManagerViewModel
(the method handling rename—e.g., renameFile or the function where "renamed" and
"newName" are used) move the check newName.length in 1..40 to before any
file.renameTo / rename operation, return early or set an error/result when the
name is invalid, and only perform the actual rename when the name passes
validation (then update the existing condition that currently reads "if
(newName.length in 1..40 && renamed)" so it only evaluates renamed after a
successful pre-check).

---

Nitpick comments:
In `@app/src/main/java/com/itsaky/androidide/viewmodel/FileManagerViewModel.kt`:
- Around line 33-38: Detect the exact-match no-op before doing the temporary
rename: if file.name == newName (case-sensitive) simply return success instead
of performing the two-step temp rename; update the logic in the
FileManagerViewModel rename flow (the block that currently checks
file.name.equals(newName, ignoreCase = true) and uses tempFile/destFile) to
first check for exact equality and short-circuit, otherwise fall back to the
existing temp-file sequence or FileUtils.rename call.
- Line 43: The FileRenameEvent is constructed with File(file.parent, newName)
which is inconsistent with the actual rename target (destFile) and risks
mismatch; update the creation of renameEvent in FileManagerViewModel (the
variable renameEvent) to use the same destFile used for renaming (or construct
with File(file.parentFile, newName)) so the event accurately reflects the
destination path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5d1e6559-5286-456a-86f8-cb92064e4fb2

📥 Commits

Reviewing files that changed from the base of the PR and between 1793332 and 77a8b03.

📒 Files selected for processing (1)
  • app/src/main/java/com/itsaky/androidide/viewmodel/FileManagerViewModel.kt

Comment on lines +34 to +35
val tempFile = File(file.parentFile, "$newName.tmp")
file.renameTo(tempFile) && tempFile.renameTo(destFile)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: Check for temporary file collision before rename.

The temporary file "$newName.tmp" could already exist in the directory. If it does, renameTo will either fail (leaving the original file unchanged) or potentially overwrite existing data on some file systems, causing data loss.

🛡️ Proposed fix to check for temp file existence
 if (file.name.equals(newName, ignoreCase = true)) {
     val tempFile = File(file.parentFile, "$newName.tmp")
+    if (tempFile.exists()) {
+        return@withContext false
+    }
     file.renameTo(tempFile) && tempFile.renameTo(destFile)
 } else {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
val tempFile = File(file.parentFile, "$newName.tmp")
file.renameTo(tempFile) && tempFile.renameTo(destFile)
val tempFile = File(file.parentFile, "$newName.tmp")
if (tempFile.exists()) {
return@withContext false
}
file.renameTo(tempFile) && tempFile.renameTo(destFile)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/itsaky/androidide/viewmodel/FileManagerViewModel.kt`
around lines 34 - 35, The rename sequence uses a fixed temp filename
("$newName.tmp") which can collide; update the logic in FileManagerViewModel
around variables tempFile, file.renameTo(tempFile) and destFile to first ensure
a unique temporary path (e.g., use File.createTempFile or loop to append an
increasing suffix until a non-existent temp is found), fail fast if you cannot
reserve a unique temp, perform file.renameTo(tempFile) then
tempFile.renameTo(destFile), and ensure you clean up the tempFile on any failure
to avoid leaving or overwriting files.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Major: Add rollback mechanism for failed two-step rename.

If file.renameTo(tempFile) succeeds but tempFile.renameTo(destFile) fails, the file will be left with the .tmp extension. There is no cleanup or rollback, leaving the file system in an inconsistent state.

🔄 Proposed fix to add rollback logic
 if (file.name.equals(newName, ignoreCase = true)) {
     val tempFile = File(file.parentFile, "$newName.tmp")
-    file.renameTo(tempFile) && tempFile.renameTo(destFile)
+    val firstRename = file.renameTo(tempFile)
+    if (firstRename) {
+        val secondRename = tempFile.renameTo(destFile)
+        if (!secondRename) {
+            // Rollback: restore original name
+            tempFile.renameTo(file)
+        }
+        secondRename
+    } else {
+        false
+    }
 } else {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/itsaky/androidide/viewmodel/FileManagerViewModel.kt`
around lines 34 - 35, The two-step rename in FileManagerViewModel (where it
creates tempFile and calls file.renameTo(tempFile) &&
tempFile.renameTo(destFile)) lacks rollback: if the first rename succeeds but
the second fails the temp file remains. Change the logic in the renaming method
in FileManagerViewModel to perform the second rename in an if block so you can
detect failure; on failure attempt to move tempFile back to the original file
name (rollback), catch/log any exceptions (use whatever logger is available in
this class), and return false; ensure the method returns true only if both
renames succeed and that all filesystem operations are guarded and error-handled
to avoid leaving a .tmp file behind.

}

if (renamed) {
if (newName.length in 1..40 && renamed) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: Validate name before performing rename operation.

The validation of newName.length in 1..40 now occurs AFTER the rename operation completes. This means if the name length is invalid (empty, or >40 characters), the file will still be renamed, but the operation will be reported as failed. This creates an inconsistent state where the file system changes but the operation is reported as an error.

Validation must occur before attempting the rename to prevent this inconsistency.

🛡️ Proposed fix to validate before rename
 fun renameFile(file: File, newName: String, context: Context? = null, onResult: ((Boolean) -> Unit)? = null) {
     viewModelScope.launch {
+        if (newName.length !in 1..40) {
+            _operationResult.emit(FileOpResult.Error(R.string.rename_failed))
+            onResult?.invoke(false)
+            return@launch
+        }
         val destFile = File(file.parentFile, newName)
         val renamed = withContext(Dispatchers.IO) {
             if (file.name.equals(newName, ignoreCase = true)) {
                 val tempFile = File(file.parentFile, "$newName.tmp")
                 file.renameTo(tempFile) && tempFile.renameTo(destFile)
             } else {
                 FileUtils.rename(file, newName)
             }
         }

-        if (newName.length in 1..40 && renamed) {
+        if (renamed) {
             // Notify system of the rename
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/itsaky/androidide/viewmodel/FileManagerViewModel.kt` at
line 41, The rename validation is happening after the file is already renamed,
causing filesystem changes when the new name is invalid; in FileManagerViewModel
(the method handling rename—e.g., renameFile or the function where "renamed" and
"newName" are used) move the check newName.length in 1..40 to before any
file.renameTo / rename operation, return early or set an error/result when the
name is invalid, and only perform the actual rename when the name passes
validation (then update the existing condition that currently reads "if
(newName.length in 1..40 && renamed)" so it only evaluates renamed after a
successful pre-check).

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.

3 participants