Skip to content
The CNA BibleCNA 0.1.0-alpha.1 Edition

Chapter 50 Storage and Persistence Boundaries

Microsoft::Xna::Framework::Storage maps XNA’s device-selection API onto one host filesystem root. It provides usable save-file operations, but the abstraction is not a security sandbox: two path inputs can escape their intended roots at the pinned revision.

50.1 StorageDevice contract

StorageDevice reports filesystem FreeSpace, TotalSpace, and IsConnected. Its Begin*/End* pairs have XNA’s asynchronous shape but complete synchronously. Selector overloads accept player, free-space, and directory-count constraints; CNA ignores the constraints because it has no removable-device choice. DeviceChanged is never raised.

SetAppNameEXT(appName) selects the per-application root and invalidates any cached resolution. Call it before storage or GamerServices access: achievements and leaderboards also live below this root. GetStorageRootEXT() exposes the resulting path.

Storage result ownership is explicit. EndShowSelector returns std::unique_ptr<StorageDevice>; EndOpenContainer returns std::unique_ptr<StorageContainer>.

1 StorageDevice::SetAppNameEXT("MyGame");
2
3 auto select = StorageDevice::BeginShowSelector(nullptr, nullptr);
4 std::unique_ptr<StorageDevice> device =
5 StorageDevice::EndShowSelector(select.get());
6
7 auto opening = device->BeginOpenContainer(
8 "MyGame Save Data", nullptr, nullptr);
9 std::unique_ptr<StorageContainer> container =
10 device->EndOpenContainer(opening.get());

50.2 Storage root resolution

The root is resolved lazily, then cached until SetAppNameEXT changes the app name:

  1. 1.

    SDL_GetPrefPath(nullptr, app);

  2. 2.

    XDG_DATA_HOME/<app> when SDL fails;

  3. 3.

    HOME/.local/share/<app>;

  4. 4.

    the current working directory when the environment supplies no home.

The final fallback is process-location-dependent and should not be treated as a stable save location. If the root does not yet exist, space queries return LLONG_MAX. For IsConnected, CNA walks upward to the first existing ancestor; true therefore does not prove that the per-app directory has been created.

50.3 StorageContainer contract

StorageContainer provides create, delete, existence, enumeration, and glob-pattern operations for files and directories. Its OpenFile overloads return System::IO::Stream. Paths are intended to be relative to the container.

1 {
2 std::unique_ptr<System::IO::Stream> out =
3 container->CreateFile("savegame.dat");
4 std::vector<SharpRuntime::bytecs> bytes = SerializeGameState();
5 out->Write(bytes.data(), 0, static_cast<int>(bytes.size()));
6 }
7
8 if (container->FileExists("savegame.dat")) {
9 std::unique_ptr<System::IO::Stream> in = container->OpenFile(
10 "savegame.dat", System::IO::FileMode::Open);
11 std::vector<SharpRuntime::bytecs> bytes(in->getLengthProperty());
12 int offset = 0;
13 while (offset < static_cast<int>(bytes.size())) {
14 int n = in->Read(bytes.data(), offset,
15 static_cast<int>(bytes.size()) - offset);
16 if (n == 0) break;
17 offset += n;
18 }
19 if (offset != static_cast<int>(bytes.size())) {
20 /* reject a truncated save */
21 }
22 }

Dispose() is idempotent, sets IsDisposed, and raises Disposing; it does not guard later operations. The container retains a non-owning const StorageDevice*, so the device must outlive it. Treat a disposed container as unusable even though the implementation still permits path operations.

50.4 Open path-containment defects

Limitation. Container name can escape the storage root BeginOpenContainer(displayName) retains the name verbatim. Construction joins root/displayName/playerFolder and rejects only an empty name. An absolute name discards the root under std::filesystem::path::operator/; .. can climb above it. DeleteContainer uses ResolveContainedPath and rejects these cases, so opening and deleting do not share one boundary.

Limitation. File and directory names can escape a container The shared ResolvePath(relative) helper only evaluates storagePath/relative. It rejects an empty string but neither absolute paths nor ... For example, CreateFile("../../outside.txt") can write above the container. Validate every externally influenced filename before passing it to this namespace.

The focused storage tests cover containment for DeleteContainer; they do not cover StorageContainer, opening a container, enumeration, or the hand-written */? glob matcher. The untested boundary is therefore the same boundary where the defects remain.

50.5 File sharing is not enforced

The fullest OpenFile overload accepts System::IO::FileShare, but comments the parameter name out and constructs FileStream with path, mode, and access only. The pinned FileStream has no share-mode constructor, and its std::fstream implementation supplies no cross-process locking policy. Consequently, None, Read, and ReadWrite produce the same CNA behavior. Do not use FileShare::None as protection against concurrent save writers.

50.6 Exception boundary and evidence

StorageDeviceNotConnectedException is thrown when space queries translate a filesystem exception. Container and file operations do not first check IsConnected or uniformly translate host errors into this type.

The public headers and implementations establish ownership, root selection, ignored parameters, and the two traversal paths. Tests establish the protected DeleteContainer cases and a normal deletion. Container read/write behavior is source-proven here; the pinned storage test set does not supply an end-to-end save round-trip or hostile container-path oracle. The example uses fixed names and therefore does not demonstrate containment of untrusted input.

Type at least three characters. Results are ranked by how often and where the words occur.