I/O, Streams & HTTP · Lesson 18

File paths are portable data, not filesystem permission

Explore path composition without pretending a browser lab touched the user’s disk.

A path string is not a file capability

System.IO.Path works with path representations. Combining components, finding a filename or examining an extension can be useful without opening a file. This lesson deliberately stays at that representation layer so it runs predictably in SharpRune and does not claim access to the user's ordinary disk. A browser-hosted runtime can have a virtual filesystem and a native app has its own storage rules; neither should be confused with arbitrary access to a desktop path. Treat storage access as a separately configured adapter whose permissions and persistence must be verified in its actual environment.

Trace the portable observations in the sample

Path.Combine receives the relative directory data and the filename rune.json. GetFileName prints rune.json, GetExtension prints .json, and IsPathRooted prints False for the combined relative path. The full combined path is intentionally not printed as a portable expected string because separator conventions vary by platform. No File or Directory operation occurs, so the run neither creates the data directory nor reads a saved JSON document. These three observations establish lexical path behavior for the supplied components, not the existence, content or readability of a file.

Keep path syntax distinct from storage ownership

The path string is immutable application data; the program owns no open stream, file descriptor or directory enumeration. Once a real adapter opens a file, that adapter must own the stream and dispose it at a suitable boundary. The caller should distinguish the domain operation it wants, such as loading a settings document, from an unconstrained path supplied by another layer. Configuration can select an application-owned location, while the adapter handles concrete open, read, write and persistence behavior. This separation also allows tests of parsing logic to remain independent of storage.

Combining a base does not create a sandbox

Do not assume that passing a preferred directory as the first Combine operand forces every result to remain underneath it. A later rooted component can change the result, and relative components can include parent traversal. Path.GetFullPath performs path normalization relative to a specified context; it does not prove existence, authorization or a race-free containment boundary. Symlinks and platform-specific path rules further separate lexical inspection from filesystem behavior. This lesson is not an implementation of secure extraction or arbitrary user-path handling, so its simple output must not be used as evidence for those stronger requirements.

Practice: test rooted and relative operands

Add a relative filename, a nested relative path and a platform-appropriate rooted operand to a table. Observe IsPathRooted and the resulting filename for each without asserting one platform's separator spelling on every platform. Then introduce a parent-directory component and explain why the final filename can remain rune.json while the location changes. The hint is that GetFileName is a projection that discards location information. Two paths with the same last component need not refer to the same file or share the same access policy.

External console exercise: an owned UTF-8 file fixture

Sketch a small settings-store interface whose input is a named application setting rather than an arbitrary path. Keep JSON parsing in a pure function and use a separate native or desktop adapter to select an application-owned directory. For an actual integration exercise, use a temporary directory in that supported environment, write a known document, close its stream, read it back and clean up only that owned fixture. Do not report the console's lexical Path exercise as having performed that integration. Predict how a missing file should differ from malformed saved content.

Run the following complete snippet only in an external .NET console environment with temporary-directory access. It creates its own unique fixture, writes and reads explicit UTF-8 asynchronously, prints True for the round trip, and removes only that fixture in finally. This is separate from the portable Path-only program opened by SharpRune. A temporary round trip does not establish application persistence across restarts or permission to modify another directory.

using System;
using System.IO;
using System.Text;
using System.Threading;

var fixture = Directory.CreateTempSubdirectory("SharpRune_IO_");
try
{
    string path = Path.Combine(fixture.FullName, "rune.txt");
    var utf8 = new UTF8Encoding(false, true);
    await File.WriteAllTextAsync(path, "rune\n", utf8, CancellationToken.None);
    string restored = await File.ReadAllTextAsync(path, utf8, CancellationToken.None);
    Console.WriteLine(restored == "rune\n");
}
finally
{
    fixture.Delete(recursive: true);
}

Review the environment boundary honestly

The lesson's strength is its deliberately limited claim. It teaches portable observations that can be made from path data and identifies where real storage behavior begins. Maintain separate checks for path construction, document parsing and the storage adapter's read/write contract. In the target application, verify persistence across restarts, expected access failures and ownership of temporary files using the platform's supported mechanisms. A correct extension string does not establish a JSON schema, and a relative path does not by itself establish either a safe storage location or a granted filesystem permission.

  • The sample performs lexical path operations and no filesystem I/O.
  • Test real persistence through an explicitly owned target-environment adapter.

A working example

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.IO;
using System.IO.Compression;
using System.Buffers.Binary;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;

string path = Path.Combine("data", "rune.json"); Console.WriteLine(Path.GetFileName(path)); Console.WriteLine(Path.GetExtension(path)); Console.WriteLine(Path.IsPathRooted(path));

Expected output

rune.json
.json
False
Try this
This lab only manipulates path strings; actual file access is a separate external setup and permission boundary.

Further reading