Class AchievementScreenshotKey
The one place the layout of a stored achievement screenshot is written down —
Achievements/{player}/{achievement}.png, plus the index that lists what a player has —
and the only thing allowed to decide whether a key read back from data is one of ours.
Inherited Members
Namespace: Serenity.Achievements.Infrastructure.Stores
Assembly: Serenity.Achievements.Infrastructure.dll
Syntax
public static class AchievementScreenshotKey
Remarks
Every key that arrives from stored data is untrusted. It reaches this module out of an unlock record, which lives in whatever store the project configured — a save file a player can edit, a row a server wrote, a document another build produced. A key is turned straight into a file path, so accepting one that escapes the player's folder would let stored data name any file on disk to read or delete. Nothing here trusts a key: it is parsed against the exact shape below or refused, and BelongsTo(PlayerId, string) compares parsed segments rather than string prefixes.
The segment rules are an explicit allowlist rather than NamingSanitizer.FileNameSafe
because these keys are persisted and have to mean the same thing on every machine that reads
them back; that helper filters through Path.GetInvalidFileNameChars, whose contents
differ between Windows and Unix. The allowlist also rules out the traversal segments
(., ..) a denylist of illegal characters happily preserves.
Escaping is injective, not lossy: every character outside [A-Za-z0-9-] becomes
_ followed by the lowercase hex of each of its UTF-8 bytes, and a literal underscore
escapes to _5f by the same rule. Folding disallowed characters to a bare _ —
what the first cut did — collides: a/b, a_b and a.b all became
a_b, so three players would have shared one screenshot folder and a reset for one
would have wiped the others.
Fields
FileExtension
The extension the stored bytes carry, matching the PNG the capturer encodes.
Declaration
public const string FileExtension = ".png"
Field Value
| Type | Description |
|---|---|
| string |
IndexFileName
The file listing the keys stored for one player.
Declaration
public const string IndexFileName = "index.txt"
Field Value
| Type | Description |
|---|---|
| string |
MaximumSegmentLength
The longest a single escaped segment may be. Generous for any real identifier, and short enough that the resolved path stays inside the platform's path limit.
Declaration
public const int MaximumSegmentLength = 200
Field Value
| Type | Description |
|---|---|
| int |
RootSegment
The folder every achievement screenshot lives under.
Declaration
public const string RootSegment = "Achievements"
Field Value
| Type | Description |
|---|---|
| string |
Methods
BelongsTo(PlayerId, string)
Whether a stored key names one of this player's screenshots.
Declaration
public static bool BelongsTo(PlayerId player, string storageKey)
Parameters
| Type | Name | Description |
|---|---|---|
| PlayerId | player | The player the key is tested against. |
| string | storageKey | The key handed back by a save. |
Returns
| Type | Description |
|---|---|
| bool |
Remarks
Segment-exact rather than a prefix test: StartsWith would accept
Achievements/player-10/x.png as belonging to player-1, so a bulk delete
for one player would take another player's screenshots with it.
ForUnlock(PlayerId, AchievementId)
Builds the key one unlock's screenshot is stored under.
Declaration
public static string ForUnlock(PlayerId player, AchievementId achievement)
Parameters
| Type | Name | Description |
|---|---|---|
| PlayerId | player | The player the unlock belongs to. |
| AchievementId | achievement | The achievement that was unlocked. |
Returns
| Type | Description |
|---|---|
| string |
IndexFor(PlayerId)
Builds the key of the index listing every screenshot stored for a player.
Declaration
public static string IndexFor(PlayerId player)
Parameters
| Type | Name | Description |
|---|---|---|
| PlayerId | player | The player whose index is addressed. |
Returns
| Type | Description |
|---|---|
| string |
IsRecognized(string)
Whether a stored key is a screenshot or index key this module wrote.
Declaration
public static bool IsRecognized(string storageKey)
Parameters
| Type | Name | Description |
|---|---|---|
| string | storageKey | The key to test. |
Returns
| Type | Description |
|---|---|
| bool |
IsWithinRoot(string, string)
Whether a resolved blob key is still confined to the anchor it was built from — the check every blob call is gated on, so a key that slipped past parsing still cannot address anything outside the screenshots folder.
Declaration
public static bool IsWithinRoot(string rootDirectory, string resolvedKey)
Parameters
| Type | Name | Description |
|---|---|---|
| string | rootDirectory | The directory the key was anchored under. Blank means unanchored. |
| string | resolvedKey | The key about to be handed to the blob store. |
Returns
| Type | Description |
|---|---|
| bool |
Resolve(string, string)
Turns a stored key into the key the blob store is addressed by, anchoring it under the directory the backend writes to.
Declaration
public static string Resolve(string rootDirectory, string storageKey)
Parameters
| Type | Name | Description |
|---|---|---|
| string | rootDirectory | The directory the backend stores under. Blank anchors nothing. |
| string | storageKey | The relative key an unlock record carries. |
Returns
| Type | Description |
|---|---|
| string |
Remarks
The two are kept apart deliberately: the record persists the relative key, which stays valid when the player's save directory moves — between machines, between a development build and a shipped one, or between two operating systems — while the blob store wants the absolute path the file backend resolves files against.
TryParse(string, out PlayerId, out AchievementId)
Reads a stored key back into the player and achievement it names, accepting only the exact shape this type writes.
Declaration
public static bool TryParse(string storageKey, out PlayerId player, out AchievementId achievement)
Parameters
| Type | Name | Description |
|---|---|---|
| string | storageKey | The key an unlock record carries. |
| PlayerId | player | The player the key names. |
| AchievementId | achievement | The achievement the key names. |
Returns
| Type | Description |
|---|---|
| bool | True when the key is exactly |
Remarks
This is the trust boundary. A key that is rooted, carries a traversal segment, uses a separator inside a segment, has the wrong number of segments, the wrong extension, an unescapable segment or an overlong one is not one of ours, and the caller must refuse it rather than hand it to a blob store.
TryParseIndex(string, out PlayerId)
Whether a stored key is the index of the player it names.
Declaration
public static bool TryParseIndex(string storageKey, out PlayerId player)
Parameters
| Type | Name | Description |
|---|---|---|
| string | storageKey | The key to test. |
| PlayerId | player | The player the index belongs to. |
Returns
| Type | Description |
|---|---|
| bool |