Skip to content

Save notes whose body is empty - #33

Merged
dangduc merged 1 commit into
masterfrom
fix/empty-note-write-paramerr
Sep 12, 2026
Merged

dangduc merged 1 commit into
masterfrom
fix/empty-note-write-paramerr

Conversation

@dangduc

@dangduc dangduc commented Sep 12, 2026

Copy link
Copy Markdown
Owner

Problem

Saving a note with an empty body failed with a modal alert:

Changed notes could not be saved because a parameter was invalid.

That text is OSStatus -50 (paramErr), raised by -noteDidNotWrite:errorCode:. Each failure also orphaned a zero-length temporary file in the notes directory.

Cause

A note with an empty body is stored as a zero-length file, and Foundation returns a NULL buffer pointer for zero-length NSData:

read 0-byte file       len=0 bytes=0x0
[data copy] (baseline) len=0 bytes=0x0
dataUsingEncoding      len=0 bytes=0x600000f1c000

Once such a note was read back from disk, -rememberSourceBaselineData:encoding: cached that NULL-pointer data. On the next save, the unchanged-source fast path in -sourceDataReturningError: returned it verbatim instead of re-encoding, and FSRefWriteData rejected it:

if (!buffer || !fsRef) { return paramErr; }

Traced directly:

begin name=vô lễ.txt nameLength=9 dataLength=0 dataBytes=0x0 destRef=.../vô lễ.txt
CreateTemporaryFile ok temp=.../.017801465-810886610-13
FSRefWriteData failed: -50 (dataLength=0 dataBytes=0x0 blockSize=1048576)
error writing to temporary file: -50
Unable to save note file vô lễ.txt

The temporary file is created before the write, so a failure leaves it behind — which is the signature these failures leave in a notes directory.

Two conditions hid this. A note created in the current session has no cached source baseline, so it re-encodes and saves fine; the failure needs a relaunch first. And it affects every empty-body note at once during any pass that rewrites all notes, such as a storage-format change, rather than a single deliberate save.

The earlier write path always built data with dataUsingEncoding:, which returns a non-NULL pointer for an empty string, so this arrived with the source-editing work in 60e0333.

Fix

  • Sources/Utilities/BufferUtils.c — a zero-length write is valid, so only a missing buffer with bytes to write is an error. This also covers the export path, which passes [formattedData bytes] and had the same latent failure.
  • Sources/Model/NoteObject.m — skip the unchanged-source fast path when the cached data is empty, so a NULL pointer never reaches a writer.

Testing

New suite Tests/Regression/empty-source-write/, registered in Tests/run-regression-tests.py (18 checks):

  • zero-length data exposes a NULL buffer pointer, and a zero-length file reads back that way
  • FSRefWriteData accepts a zero-length write with a NULL buffer, and still rejects a NULL buffer with bytes to write, and a missing FSRef
  • an empty-body note writes on creation and rewrites after its source baseline is cached
  • no orphaned temporary files remain afterwards

Verified against a copy of a real library with five empty-body notes: 5 failures and 5 orphaned temporary files before, 0 and 0 after. Reverting either guard and rebuilding fails the suite (FAIL: zero-length write with a NULL buffer succeeds), so the check cannot pass vacuously.

Ran on macOS 13.7.8, Intel Development build.

https://claude.ai/code/session_01W5dVNP811hKcLKUmJHD1FG

A note with an empty body is stored as a zero-length file. Foundation returns
a NULL buffer pointer for zero-length NSData, so once such a note was read back
from disk and its source baseline cached, -sourceDataReturningError: returned
that data unchanged and FSRefWriteData rejected the NULL pointer with paramErr.
The note failed to save with "Changed notes could not be saved because a
parameter was invalid", and the temporary file created for the atomic swap was
left behind in the notes directory.

The failure appeared only after a relaunch, because a note created in the
current session has no cached source baseline, and it affected several notes at
once during any pass that rewrites every note, such as a storage-format change.

Treat a zero-length write as valid in FSRefWriteData, which also covers the
export path, and skip the unchanged-source fast path when the cached data is
empty so a NULL pointer never reaches a writer.

Claude-Session: https://claude.ai/code/session_01W5dVNP811hKcLKUmJHD1FG
@dangduc
dangduc merged commit a011d56 into master Sep 12, 2026
3 checks passed
@dangduc
dangduc deleted the fix/empty-note-write-paramerr branch September 12, 2026 06:57
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