logo SERENITY
Game Systems Foundation
Search Results for

    Show / Hide Table of Contents

    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.

    Inheritance
    object
    AchievementScreenshotKey
    Inherited Members
    object.ToString()
    object.Equals(object)
    object.Equals(object, object)
    object.ReferenceEquals(object, object)
    object.GetHashCode()
    object.GetType()
    object.MemberwiseClone()
    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 Achievements/{player}/{achievement}.png.

    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
    In this article
    © 2026 Serenity. All Rights Reserved