Files, Directories, Paths, and Metadata

Use native filesystem APIs precisely, with explicit path, metadata, error, portability, and lifetime boundaries.

FilesystemPathsMetadata

Three ways to work with the filesystem

Sharp Runtime exposes the same storage through three different shapes. Static helpers are concise for one operation, metadata objects retain a path for a sequence of related operations, and streams expose ordered bytes. Choosing among them is about lifetime and control rather than feature ranking.

ShapeTypesBest useTradeoff
Static helpersFile, Directory, PathOne-shot reads, writes, moves, existence checks, and path manipulationWhole-file/eager operations and a deliberately bounded overload set
Metadata objectsFileSystemInfo, FileInfo, DirectoryInfo, DriveInfoRetain a normalized path and combine metadata with operationsNot a live handle; the filesystem may change between calls
Open I/OFileStream, RandomAccessIncremental bytes, access modes, seeking, resizing, or explicit offsetsResource ownership and close behavior become part of correctness

All three use native facilities. Sharp Runtime does not provide a virtual filesystem abstraction that erases case sensitivity, permissions, sharing, symbolic links, timestamp resolution, or atomic-rename differences. Treat a portable source branch as evidence that the code compiles there, not as proof that every host filesystem answers identically.

File: whole-file and namespace operations

System::IO::File is a static class for the common operations currently implemented:

Operation groupMembersCurrent behavior
ExistenceExistsTrue only for a regular file; empty paths and lookup errors return false
Namespace mutationDelete, Copy, MoveCopy can opt into overwrite; move is non-overwriting; delete rejects a directory
TextReadAllText, WriteAllText, AppendAllTextReads or writes native string bytes through standard streams
LinesReadAllLines, WriteAllLinesEager vector of strings; writes an LF after each supplied line
BinaryReadAllBytes, WriteAllBytesEager byte vectors in binary mode

File::Delete intentionally does not inherit std::filesystem::remove’s ability to remove an empty directory. It first rejects a directory with IOException, matching the separation between file deletion and directory deletion. Deleting a missing file succeeds. FileInfo::Delete routes through the same implementation so the static and object-shaped doors cannot disagree.

Non-overwriting Move rejects an existing destination before calling the native rename operation. This matters on POSIX, where an unchecked rename can replace an existing destination. Copy and Move require the source to be a regular file and translate their main native failures into the IOException family.

Text helpers do not select an encoding

The text methods read and write the bytes carried by std::string. They do not detect a BOM, validate UTF-8, or accept an Encoding argument. ReadAllLines uses the native std::getline LF delimiter, while WriteAllLines writes LF explicitly. Applications that need a defined Unicode or line-ending policy should make that policy explicit with the Text component.

Directory: eager, single-level enumeration

System::IO::Directory implements existence, recursive directory creation, optional recursive deletion, movement, eager file/subdirectory enumeration, and process-current-directory access.

MemberImplemented scopeNot implied
ExistsTrue only when the path resolves to a directoryIt does not distinguish missing, inaccessible, and invalid with exceptions
CreateDirectoryCreates intermediate directoriesNo symbolic-link creation API
Delete(path, recursive)Single directory or full treeRecursive deletion is destructive and follows host filesystem behavior
MoveNon-overwriting native renameNo cross-device copy-and-delete fallback
GetFiles(path)Immediate regular files, returned eagerlyNo streaming enumeration or recursion
GetFiles(path, pattern)*/? pattern over immediate file namesNo SearchOption or EnumerationOptions
GetDirectoriesImmediate subdirectories, returned eagerlyNo pattern overload in this class
Current directoryGetCurrentDirectory, SetCurrentDirectoryThe current directory is process-global state, not thread-local context

The pattern translator treats *.* as *, preserving the legacy .NET/DOS expectation that extensionless names are included. Matching is case-insensitive in the current implementation, including on case-sensitive POSIX filesystems. That is a visible policy choice: it should not be used to infer the filesystem’s actual case rules.

Enumeration uses std::filesystem::directory_iterator. Results are not sorted, snapshots are not atomic, and entries can disappear between enumeration and use. Because the surface is eager, memory consumption grows with the number of matching entries. Permission or concurrent-mutation failures are not normalized uniformly at every iterator step; callers should keep their error handling at the operation boundary.

Path: lexical operations, not filesystem authority

System::IO::Path manipulates path strings. Most members do not access the filesystem, and a syntactically normalized result is not proof that a target exists or remains confined to a directory.

AreaMembersSemantics
CompositionCombine with two or three partsA rooted later part replaces preceding parts
Name partsGetFileName, GetFileNameWithoutExtension, GetExtension, GetDirectoryNameLexical string/path decomposition
NormalizationGetFullPathResolves against the process current directory and collapses duplicate, dot, and dot-dot segments
Temporary pathsGetTempPath, GetTempFileNameUses the host temporary directory; the file-name method creates the file
ExtensionsChangeExtension, HasExtensionWorks on the final path segment
Root testIsPathRootedDelegates absolute-path recognition to std::filesystem::path

GetFullPath rejects an empty string and embedded NUL, obtains the process working directory for relative input, and removes relative segments lexically. It does not resolve symbolic links. Code enforcing a security boundary needs both lexical checks and an appropriate native filesystem strategy; path normalization alone cannot defeat a link swap or another process changing the tree.

The root-length helper is intentionally simplified. Drive-letter and UNC-prefix behavior is not modeled as a complete .NET Windows path grammar. Separator constants and PathSeparator are platform-specific, but a successful build on Windows does not make every managed path edge case equivalent.

FileSystemInfo, FileInfo, and DirectoryInfo

FileSystemInfo retains the original spelling and a resolved absolute path. ToString returns the original spelling; FullName returns the absolute path. An empty constructor path throws ArgumentException, and resolution uses an error-code path so a std::filesystem::filesystem_error does not escape that constructor.

Timestamp model

The base supplies local and UTC creation, last-access, and last-write getters plus local/UTC last-write setters. It does not supply creation-time setters. On POSIX, “creation time” uses st_ctime when portable birth-time data is unavailable; that is inode metadata-change time, not necessarily when the file was created. Timestamp precision, daylight conversion, and access-time updates remain filesystem and mount-policy dependent.

FileInfo

FileInfo exposes name, directory name, existence, length, a simplified owner-write permission test, delete, copy, and move. Length throws when the path is a directory but returns zero when a missing file or another size lookup error prevents a result. The object is not an open handle and does not cache existence or size as a coherent transaction.

CopyTo and MoveTo validate their destination and require the source to exist. A successful move updates the object’s full and original paths. CopyToInfo is a native convenience that returns a new destination object. Full .NET properties such as attributes, Unix mode, link target, and symbolic-link creation/resolution are not implemented on this base/object family.

DirectoryInfo

DirectoryInfo exposes name, parent, existence, create, delete, move, immediate FileInfo enumeration, immediate DirectoryInfo enumeration, and GetFileNames. The last member is a Sharp Runtime convenience; .NET’s DirectoryInfo.GetFiles already returns metadata objects rather than strings.

Missing are CreateSubdirectory, streaming Enumerate* methods, GetFileSystemInfos, Root, and pattern/recursive overloads on the instance. Recursive delete is explicit. The non-recursive override rejects a missing directory and reports non-empty/native failures rather than silently turning them into a no-op.

FileStream modes, access, and large offsets

FileStream supplies the open-handle surface for common file work. FileMode::Open and Truncate require an existing regular file; CreateNew rejects an existing target; create/truncate/append modes require write access; append cannot include read access. The stream tracks read/write capability and consults the native open state for every public operation that needs the file.

The general Stream contract represents length, position, count, and offsets with 32-bit intcs. Code that needs a large-file offset beyond that representable range should use RandomAccess, whose lengths and file offsets are longcs, or a project-specific native abstraction. See Streams, readers, and writers for the complete lifecycle and capability matrix.

Although types such as FileShare, FileOptions, and FileStreamOptions exist in the module, the active FileStream constructor set does not reproduce the complete managed sharing, asynchronous-option, handle, and buffer-size surface. Do not infer file-sharing enforcement merely from the presence of the enum headers.

RandomAccess: positional native descriptor I/O

RandomAccess performs reads and writes at an explicit file offset without changing a shared logical stream position. On POSIX it uses pread, pwrite, lseek, ftruncate, and fsync. On Windows it converts a CRT integer descriptor to a Win32 handle and uses overlapped ReadFile/WriteFile, file-pointer/length operations, and FlushFileBuffers. Every operation throws PlatformNotSupportedException on Emscripten.

MemberContractCaller responsibility
GetLengthReturns a 64-bit length; invalid or non-seekable descriptors throwPass a live descriptor suitable for a file-length query
SetLengthRejects negative length and reports native truncation failureDescriptor must permit resize
ReadReturns a possibly shorter count, zero at endKeep the destination buffer and descriptor valid for the call
WriteRetries short writes until all requested bytes are written; detects zero progressKeep the source buffer stable
FlushToDiskRequests an OS-level durable flushUnderstand that device/filesystem durability remains host-defined

The class borrows the integer descriptor. It never opens, closes, duplicates, locks, or extends its lifetime. Closing the descriptor, letting another thread reuse its number, or changing the underlying file concurrently is outside the class’s ownership boundary.

DriveInfo is a lightweight view

DriveInfo uses std::filesystem::space for available, free, and total bytes. It reports zero when the root is not ready or the query fails. On Windows, GetDrives enumerates the logical-drive bitmask. Elsewhere it returns the root directory /. Drive type currently always reports Fixed, drive format reports Unknown, and volume label returns the configured name. These values are useful as a small storage-capacity surface, not a full mount/volume inventory.

Watching is a separate concurrency model

FileSystemWatcher is not a polling wrapper around Directory. On Linux it owns inotify descriptors and a background dispatch thread. Handler lifetime, reconfiguration, event coalescing, kernel queue limits, and backend availability therefore require separate treatment. Read FileSystemWatcher in depth before using it in teardown-sensitive code.

Filesystem portability checklist

  • Do not assume enumeration order. Sort explicitly when order is part of your output or test.
  • Do not assume case sensitivity from the platform label alone; mounts and the current pattern implementation can differ.
  • Do not treat Exists=false as proof that a path is absent; it can also mean the lookup failed.
  • Keep path normalization separate from symlink-aware confinement.
  • Expect timestamp precision and creation-time meaning to vary.
  • Use the operation’s exception, not a preflight existence check, as the final authority; the tree can change between calls.
  • Keep destructive recursion explicit and narrowly scoped.
  • Do not assume an open file can be renamed, deleted, or shared identically on Windows and POSIX.