镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Escape an unpaired surrogate in a persistence key's file name [patch] - #206

Open
matt-edmondson wants to merge 2 commits into
mainfrom
fix/lone-surrogate-keys-202
Open

matt-edmondson wants to merge 2 commits into
mainfrom
fix/lone-surrogate-keys-202

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #202

What was wrong

PersistenceProviderUtilities.GetSafeFileName passed lone surrogates through unchanged. On Unix, .NET writes a path as UTF-8 and replaces an unpaired surrogate with U+FFFD. So "user\uD800", "user\uDBFF" and a literal "user�" all named the same file and overwrote each other, and GetAllKeysAsync returned none of the original keys. This happens in practice when a key is user text cut through the middle of an emoji.

Change

  • An unpaired surrogate is written as %uXXXX. The existing %XX form can't hold 0xD800, and u isn't a hex digit, so the two forms can't be confused. A literal % is still %25.
  • GetKeyFromFileName decodes %uXXXX. It returns null for a malformed form or for one that doesn't name a surrogate, the same way it handles a bad %XX.
  • Truncate's "never split an escape" guard covers the six-character form as well.
  • Paired surrogates (emoji) are copied as before, so no existing file name changes. Before this change a lone surrogate was already stored as U+FFFD, so no existing file could be named with the new escape.
  • CLAUDE.md's PersistenceProviderUtilities entry describes both escape forms.

This doesn't touch #77 (keys that differ only in case). The %u form works with either fix proposed there.

Tests

  • SafeFileName_Escapes_Unpaired_Surrogates_And_Keeps_Pairs: lone high, lone low, a reversed pair and U+FFFD keys all get distinct names that survive UTF-8 and decode back to the original key. An emoji key is unchanged.
  • SafeFileName_Never_Truncates_Inside_A_Surrogate_Escape: across 25 paddings, a truncated name never ends partway through a %uXXXX.
  • Keys_Differing_Only_In_An_Unpaired_Surrogate_Keep_Separate_Records: on NativeFileSystemProvider, "a\uD800", "a\uDBFF" and "a�" keep separate values, and GetAllKeysAsync returns all three.
  • With the utilities change reverted, all three new tests fail. With only the truncation guard disabled, the truncation test fails.
  • Full suite on Linux / .NET 10: 1416 passed, 1 skipped, 0 failed.

Generated by Claude Code

GetSafeFileName passed lone surrogates through, and on Unix .NET writes a
path as UTF-8, replacing them with U+FFFD. "user\uD800", "user\uDBFF" and
"user�" all named one file, overwrote each other, and GetAllKeysAsync
listed none of them.

An unpaired surrogate is now written as %uXXXX, which GetKeyFromFileName
decodes, and truncation never cuts inside one. Paired surrogates (emoji)
keep their names.

Fixes #202

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017vfJW1od4g4sgqv5pyHNsc
Addresses SonarCloud S3776 (GetSafeFileName's cognitive complexity) and S127
(loop index advanced inside the for body) raised on the %uXXXX change. Both
loops are now while loops over a helper that returns how many characters it
consumed; the encoding and its bounds checks are unchanged.

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

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants