feat: Implement fileMenu.saveAs/saveCopy and restore fldatabasesaveas guard#394
Conversation
… guard (#392) Three related fixes: 1. Restore !fldatabasesaveas guard in menupack.c that was lost during PR #391 refactoring. Both mesavemenustructure_legacy() and _v7() now skip the in-memory adroutline update during Save As, preserving the source database address pointers. 2. Implement fileMenu.saveAs(path) / fileMenu.saveCopy(path) using a save-then-copy approach: flush the source database to disk, then do an OS-level file copy. This avoids the dbstartsaveas/dbendsaveas machinery which modifies in-memory addresses during save. Works for both system root and guest databases. 3. Fix cancoonglobals handling: odb_guard_exit no longer auto-restores cancoonglobals, since most callers (filemenu_open, filemenu_close, filemenu_save) call functions that legitimately change it. Callers that need it restored (filemenu_saveas, filemenu_save_guestdb) now do so explicitly via guard.saved_cancoonglobals. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Code Review - PR #394SummaryThis PR implements ✅ Strengths1. Excellent Architecture DecisionThe save-then-copy approach is superior to using the
2. Comprehensive Test Coverage7 new integration tests cover:
All 28 filemenu tests passing is excellent validation. 3. Proper Global State HandlingThe change to
4. Code Quality
🔍 Issues & RecommendationsCRITICAL: Resource Leak on Read Error
|
Code Review - PR #394Overall Assessment✅ APPROVED - This is a well-implemented fix with excellent test coverage. The PR successfully addresses issue #392 by restoring the Strengths
Issues & Recommendations🔴 Critical: File Copy Error HandlingLocation: The file copy loop doesn't check for while ((n = fread(buf, 1, sizeof(buf), fin)) > 0) {
if (fwrite(buf, 1, n, fout) != n) {
// ... error handling
}
}
// Missing: if (ferror(fin)) { handle read error }Fix needed: while ((n = fread(buf, 1, sizeof(buf), fin)) > 0) {
if (fwrite(buf, 1, n, fout) != n) {
fclose(fin);
fclose(fout);
log_error(LOG_COMP_DB, "filemenu_saveas: write error");
langerrormessage(BIGSTRING("\x1c" "Can't save: file write error"));
return false;
}
}
if (ferror(fin)) {
fclose(fin);
fclose(fout);
log_error(LOG_COMP_DB, "filemenu_saveas: read error");
langerrormessage(BIGSTRING("\x1d" "Can't save: file read error"));
return false;
}🟡 Minor: Resource Cleanup on Copy FailureLocation: When the file copy fails mid-write, the partial destination file is left on disk. Consider adding cleanup: if (fwrite(buf, 1, n, fout) != n) {
fclose(fin);
fclose(fout);
remove(stringbaseaddress(bsdest)); // Clean up partial file
log_error(LOG_COMP_DB, "filemenu_saveas: write error");
langerrormessage(BIGSTRING("\x1c" "Can't save: file write error"));
return false;
}🟡 Minor: Pascal String Mutation SafetyLocation: The Design Questions1.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: aae0ed7299
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| /* NOTE: cancoonglobals is intentionally NOT restored here. Most callers | ||
| * (filemenu_open, filemenu_close, filemenu_save) call functions like | ||
| * odbOpenFile/odbCloseFile/odbSaveFile that legitimately set cancoonglobals | ||
| * as a side effect. Auto-restoring it would undo these valid changes. | ||
| * Callers that need cancoonglobals restored (e.g., filemenu_saveas) should |
There was a problem hiding this comment.
Restore cancoonglobals for callers that expect full guard rollback
With odb_guard_exit no longer restoring cancoonglobals, any caller that relied on the guard to fully restore database globals will now return with cancoonglobals still pointing at the nested DB (or nil). For example, migrate_internal() in Common/source/db_format.c explicitly uses odb_guard_enter/exit to “restore caller’s database globals” after nested dbopenfile(); after this change the caller’s cancoon isn’t restored, so subsequent operations can run against the migrated DB’s globals or hit cancoonglobals-dependent asserts. This makes migration/other guarded flows unstable unless they now manually restore cancoonglobals.
Useful? React with 👍 / 👎.
…error check, fix string lengths Review feedback from both bots: 1. Restore cancoonglobals auto-restore in odb_guard_exit. The odbengine.c setcancoonglobals operates on a static (file-local) variable, not the extern one that odb_guard saves/restores. Removing auto-restore was unnecessary and could break migrate_internal() (Codex feedback). 2. Add ferror(fin) check after file copy loop to detect read errors that cause fread to return 0 (both bots flagged this). 3. Remove partial destination file on copy failure via remove(). 4. Fix 4 incorrect Pascal string length prefixes (bot feedback). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Final Code Review - PR #394Overall Assessment✅ APPROVED - All previously identified issues have been addressed. This is an excellent implementation that properly solves issue #392 with comprehensive test coverage and sound architectural decisions. ✅ Verified Fixes1. Critical Issue Resolved: File Copy Error HandlingThe ferror(fin) check has been added (line 681-688), preventing silent failures on read errors:
2. Proper Cleanup on Write ErrorsLine 674 now includes remove() call to clean up partial files on write failures. 3. Pascal String Lengths VerifiedAll BIGSTRING length prefixes are correct (0x33=51, 0x1d=29, 0x22=34, 0x1c=28, 0x1b=27) 🎯 Implementation StrengthsArchitecture:
Code Quality:
Test Coverage (28 total filemenu tests, all passing):
Critical Bug Fix: 🔍 cancoonglobals Handling ChangeThe removal of automatic cancoonglobals restoration from odb_guard_exit() is well-justified:
This is a subtle but correct design improvement. 📋 Standards Compliance
🚀 Performance & 🔒 SecurityPerformance: 8KB buffer size is standard and appropriate. Save-then-copy adds overhead but is acceptable tradeoff for correctness. Security: ✅ No concerns - proper path handling, error checking, no buffer overflows, proper cleanup of partial files. 📊 Test Results
✅ Final Verdict: APPROVED - Ready to MergeThis PR successfully:
Excellent work! The architectural decision to use save-then-copy is sound, test coverage is exemplary, and all code quality issues have been properly addressed. |
Summary
Closes #392.
!fldatabasesaveasguard inmenupack.c— During PR feat: Implement fileMenu verbs with v7 save format and db corruption fixes #391 refactoring, the address update was moved insidemesavemenustructure_legacy()and_v7()without preserving the guard that prevents in-memoryadroutlinefrom being updated during Save As operationsfileMenu.saveAs(path)/fileMenu.saveCopy(path)— Uses a save-then-copy approach: flush the source database to disk, then do an OS-level byte-for-byte file copy. This avoids thedbstartsaveas/dbendsaveasmachinery which modifies in-memory address pointers during save. Works for both system root and guest databases (viatarget.set)cancoonglobalshandling inodb_guard_exit— Stop auto-restoringcancoonglobalssince most callers (filemenu_open,filemenu_close,filemenu_save) invoke functions that legitimately change it as a side effect. Callers that need it restored (filemenu_saveas,filemenu_save_guestdb) now do so explicitlyFiles Changed
Common/source/menupack.c!fldatabasesaveasguard in both_legacyand_v7Common/source/db_format.ccancoonglobalsauto-restore fromodb_guard_exittests/headless_filemenu_verbs.cfilemenu_saveas(), wirefilv_saveas+filv_savecopy, addcancoonglobalsrestore tofilemenu_save_guestdbtests/integration/test_cases/filemenu_verbs.yamlTest plan
🤖 Generated with Claude Code
Note
Medium Risk
Touches database save/copy paths and Save As state, where subtle pointer/state bugs can corrupt open databases; coverage is improved via new integration tests but file-copy and target-selection logic may vary across environments.
Overview
Implements headless
fileMenu.saveAs/fileMenu.saveCopyby adding a newfilemenu_saveas()handler that saves the current target DB (system root or targeted guest DB) and then performs an OS-level byte-for-byte file copy to the destination path, with strict 1-parameter validation and improved error handling.Fixes Save As correctness in menu persistence by restoring the
!fldatabasesaveasguard inmesavemenustructure_legacy()andmesavemenustructure_v7()soadroutlinein-memory isn’t mutated during Save As, preventing source DB corruption. Integration tests are updated/expanded to cover the new verbs and behaviors (28 filemenu tests total), and generated test reports are refreshed.Written by Cursor Bugbot for commit c126268. This will update automatically on new commits. Configure here.