Skip to content

Refuse a second app data type that resolves to another type's file [patch] - #384

Open
matt-edmondson wants to merge 1 commit into
mainfrom
fix/same-file-types-336
Open

matt-edmondson wants to merge 1 commit into
mainfrom
fix/same-file-types-336

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #336

What was wrong

The default file name is the simple type name in snake case. PluginA.Settings and PluginB.Settings therefore both used settings.json, and so did Gen<int> and Gen<string>. Each type held its own lock, loaded the other's file without error, and saved over it, so one type's data was silently wiped.

Change

This follows the triage comment: the default name stays as it is, since renaming it would move existing users' data, and a collision is detected instead.

  • New AppData.ClaimFile<T>(path) records the first type that loads or saves each path. A different type that resolves to the same path then throws InvalidOperationException. The message names both types, with namespace and generic arguments, and points to the LoadOrCreate(fileName) and LoadOrCreate(subdirectory) overloads.
  • LoadOrCreate calls it before reading the file, and Save() calls it before writing, so an instance created directly with new() is covered too.
  • The registry is a ConditionalWeakTable keyed by IFileSystem. In production there is one default file system. In tests each thread has its own MockFileSystem, where the same path is a different file, so parallel tests that reuse a type name in different classes don't trip it.
  • Paths are compared case-insensitively on Windows and macOS.

Behaviour change: an app that already has two colliding types gets an exception on the second one's first load, where before it silently lost data. Loading the same type repeatedly is unaffected.

Tests

New FileCollisionTests:

  • SameNamedTypesInDifferentNamespacesAreRefused: the issue's repro. The second type throws, both names are in the message, and the first type's data survives.
  • ClosedGenericsOfOneClassAreRefused
  • SaveIsRefusedForASecondTypeToo
  • Guards that pass either way: TheSameTypeCanLoadAndSaveRepeatedly, SameNamedTypesWithTheirOwnFileNamesCoexist, and TheSamePathOnAnotherFileSystemIsAnotherFile.

With AppData.cs reverted, 3 of the 6 new cases fail, and the 3 guards pass either way. Full suite on Linux / .NET 10: 151/151 passed. A Release build of the library succeeds for all six target frameworks.


Generated by Claude Code

…atch]

The default file name is the simple type name, so two Settings classes in
different namespaces, or two closed generics of one class, used one file
under different locks, and each type's save wiped the other's data. The
first type to load or save a path now owns it on that file system, and a
different type throws InvalidOperationException naming both types and
the overloads that give it its own file.

Fixes #336

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017vfJW44rALLJaHHtXa8D2t
@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

1 participant