Skip to content

Bound persistence file names in UTF-8 bytes as well as characters [patch] - #101

Merged
matt-edmondson merged 1 commit into
mainfrom
fix/persistence-key-byte-budget
Oct 7, 2026
Merged

matt-edmondson merged 1 commit into
mainfrom
fix/persistence-key-byte-budget

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #48

Problem

PersistenceProviderUtilities.GetSafeFileName truncated a name only when it exceeded 100 UTF-16 characters. Linux and macOS limit a file-name component to 255 bytes. A key of 83 or more CJK characters was left untruncated. Once FileSystemPersistenceProvider appended the extension and .tmp, StoreAsync threw PathTooLongException. Afterwards the key read back as missing. DataHome, ConfigHome and Temp delegate to the same provider, so they failed the same way.

Change

  • Names are now truncated past 246 UTF-8 bytes as well as past 100 characters. That is 255 minus a five-byte extension and .tmp, and every built-in serializer's extension (.json, .yaml, .toml) is five bytes.
    • I chose 246 rather than the ~200 the issue floated so that any name which could be written before still encodes to the same file name. A lower budget would have moved keys of 201–246 bytes to new hashed names, and the data already stored under those keys would stop being found.
  • Truncate takes whole code points within both budgets, so it no longer splits a surrogate pair. The old fixed 83-character cut left a lone high surrogate in an emoji key. The percent-escape back-off is unchanged.
  • IsTruncatedName also recognises names cut at the byte budget: the marker plus hash, with a UTF-8 length within 5 bytes of it. Names truncated under the old rule, at 98–100 characters, are still recognised.

Tests (PersistenceNamingTests)

  • SafeFileName_Bounds_Utf8_Bytes_Without_Splitting_A_Code_Point runs five CJK, emoji and mixed keys. For each it checks that the name is ≤ 246 bytes, has no split surrogate, is reported as unrecoverable, and stays distinct.
  • Long_Multibyte_Keys_Store_And_Retrieve_On_The_Native_File_System stores and retrieves the same five keys on the real file system.
  • SafeFileName_Keeps_Names_That_Fit_The_Byte_Budget_Verbatim checks that an 82-CJK name (246 bytes) is unchanged.
  • Proven both ways. Against the original PersistenceProviderUtilities.cs, 5 cases fail: both CJK keys in the bounds test, the 60-emoji key's split surrogate, and both CJK store/retrieve cases with PathTooLongException. All pass with the fix.
  • Full suite on Linux: 962 passed, 0 failed. Release build of Essentials is clean on all targets, netstandard2.1 included.

This PR is independent of #99 and #100 and merges cleanly with both.

🤖 Generated with Claude Code

https://claude.ai/code/session_01D7wytU6jsZ1cH3f6grCTz5


Generated by Claude Code

…tch]

GetSafeFileName truncated only past 100 UTF-16 characters, but Linux and
macOS limit a file-name component to 255 bytes. A key of 83+ CJK
characters stayed untruncated, and StoreAsync threw PathTooLongException
once the extension and ".tmp" were appended.

Names are now also truncated past 246 UTF-8 bytes, which leaves room for
a five-byte extension and ".tmp". Every name that could be written before
stays verbatim, so existing files are still found. Truncation takes whole
code points, so a surrogate pair is no longer split, and IsTruncatedName
recognises names cut at the byte budget.

Fixes #48

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D7wytU6jsZ1cH3f6grCTz5
@sonarqubecloud

sonarqubecloud Bot commented Oct 7, 2026

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit b6d10a0 into main Oct 7, 2026
14 checks passed
@matt-edmondson
matt-edmondson deleted the fix/persistence-key-byte-budget branch October 7, 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.

FileSystem/DataHome/ConfigHome/Temp persistence can't store non-ASCII keys of ~83+ chars on Linux/macOS: StoreAsync throws PathTooLongException

2 participants