-
Notifications
You must be signed in to change notification settings - Fork 0
Virtual File System
This page explains how SimpleXisoDrive turns an Xbox image into a file-manager-visible, read-only
volume. It covers format selection, path resolution, caching, the Dokan operation layer, and the
read-only guarantees. XDVDFS images (.iso, .xiso, .cso, .chd) and ZArchive (.zar) trees are
supported.
The same virtual file system feeds both mount backends: on Windows XboxIsoVfsDokan maps Dokan
callbacks to it, and on Linux/macOS FuseFileSystem maps the FUSE 3 callbacks (getattr, open,
read, statfs, readdir, init) to the same volume contract. The operation matrices below
describe the Windows/Dokan mapping in detail; the FUSE callbacks apply the equivalent read-only
semantics (for example, open rejects non-read-only flags and macOS setattr returns EROFS).
VfsContainer is the facade between the Dokan operations layer and the image data. It is created once
per mount and disposed on unmount. VfsContainer itself implements IVfsVolume, so the Dokan layer
consumes the volume contract rather than the concrete facade (which is also how volume failures are
injected in tests). The actual storage is an IVfsVolume implementation selected by
VfsVolumeFactory:
| Input | Detection | Volume |
|---|---|---|
.iso, .xiso, extensionless |
XDVDFS volume descriptor validates; a renamed CHD or ZArchive falls back to the matching format | XisoVfsVolume |
.chd |
CHDSharp opens the container (CD/GD-ROM rejected); the decompressed image must validate as XDVDFS. A file whose content is not a CHD is detected by content instead |
XisoVfsVolume over a ChdImageStream
|
.zar |
ZArchive footer validates. A file whose content is not an archive is detected by content instead |
ZarVfsVolume, unless the archive root holds exactly one file that validates as an XDVDFS image — then that embedded image mounts through XisoVfsVolume over ZArchiveReader.OpenRead
|
| Any other extension | Content detection: XISO probe first; a renamed CHD or ZArchive falls back to the matching format | as above |
XisoVfsVolume:
- A keep-open
XisoExploreropens the ISO file withFileShare.ReadWrite(shared access lets antivirus and indexing tools open the file too) and eagerly probes the volume descriptor. - XISOSharp probes the rebuilt sector-0 layout and the standard sector-32 layouts (plain, GLOBAL, XGD3, XGD2 Hybrid, XGD1) and records the winning disc offset.
- Invalid images surface as
XisoFormatException, which the volume wraps inInvalidImageException; missing files still throwFileNotFoundException. - The volume size and creation time come from the probed
VolumeInfo; the synthetic root entry (\) is cached.
For an embedded XISO, the same class uses the XisoReader stream APIs instead: GetVolumeInfo
probes the stream, and lookups/listings go through GetEntryInfo/ListDirectory under a stream
lock.
For a CHD, ChdImageSource opens the container with ChdFile.Open, rejects CD/GD-ROM images, and
hands the XisoVfsVolume a ChdImageStream over the decompressed bytes. The same stream APIs then
probe the decompressed image; CHDSharp decompresses hunks on demand and caches the most recent hunk
per handle. A CHD whose decompressed image is not XDVDFS fails with
"<path>" is not an Xbox ISO CHD (XDVDFS filesystem not found).
ZarVfsVolume:
-
ZArchiveReader.TryOpenvalidates the archive footer and loads the offset records, name table, and file tree. Failure throwsInvalidImageException. - The uncompressed volume size is computed by summing every file in the tree; the timestamp
is the
.zarfile's last-write time (the format stores none, andDateTime.MinValueis reported when it is unavailable). - The root ZArchive node is cached.
For an embedded XISO, the factory opens a seekable stream over the single archive file with
ZArchiveReader.OpenRead and lets XisoVfsVolume validate and mount it exactly like a standalone
ISO; ReaderOwningVfsVolume keeps the archive reader alive and closes it with the mount.
| Member | Behavior |
|---|---|
VolumeSize |
ISO length in bytes (CHD: the decompressed image size), or the summed uncompressed size of a ZArchive tree. |
VolumeCreationTime |
Timestamp from the volume descriptor (ISO/CHD); for ZAR, the archive file's last-write time (the format stores none). DateTime.MinValue when unavailable or invalid. |
GetEntry(path) |
Resolves a virtual path to an IVfsEntry, or null. Never throws. |
GetFolderList(path) |
Lazily enumerates the children of a directory. Returns nothing when the path is not a valid directory. |
ReadFile(entry, buffer, offset) |
Reads file data (decompressing ZAR blocks or CHD hunks as needed); returns the number of bytes read, or 0 on failure. |
Dispose() |
Closes the underlying stream or archive. |
Virtual paths use backslashes. Normalization (identical in both volumes):
| Input | Normalized |
|---|---|
/Games/Halo |
\Games\Halo |
\Games\Halo\ |
\Games\Halo |
"" |
\ |
\ |
\ |
Entry lookup is case-insensitive. The root path always resolves to the synthetic root entry created during construction.
GetEntry resolves a path as follows:
- If the path is
\, return the cached root entry. - If the path is already in the entry cache, return it.
- Otherwise ask XISOSharp for the entry:
XisoExplorer.GetNodefor path-based images orXisoReader.GetEntryInfofor stream-based images (both case-insensitive,/-separated library paths). - Cache and return the result.
Every step is wrapped so that a failure logs an error and returns null instead of propagating to
Dokan. ZArchive lookups preserve the archive's original name casing in the returned entry even when
the requested path uses different case.
Windows can send special relative segments. XboxIsoVfsDokan.NormalizePath handles them:
| Input path | Normalized |
|---|---|
\ |
\ |
\. or \..
|
\ |
\Games\. |
\Games |
\Games\Halo\.. |
\Games |
/ separators |
Converted to \
|
GetFolderList enumerates the entries of a directory table:
- If the listing is cached, it is replayed from the cache.
- Otherwise the directory entry is resolved (it must be a directory) and XISOSharp's
XisoExplorer.ListChildren/XisoReader.ListDirectorywalks the TOC. - Every named entry is cached by full path and added to the returned list.
- The complete list is stored in the children cache.
The library's TOC walk is hardened:
- visited offsets are tracked, preventing cycles;
- each directory table is capped at a fixed number of entries;
- separator-bearing file names abort the walk instead of escaping the mount root.
GetFolderList reads children directly from the archive's flat file tree:
- If the listing is cached, it is replayed from the cache.
- Otherwise each child is read with
TryGetDirEntry, which returns the name, type, size, and node handle in one call; entries are cached by full path and yielded. - The complete list is stored in the children cache.
Safety: directory counts are capped at 100,000 entries and the underlying reader validates node counts against the file-tree bounds, so a crafted archive cannot make the enumeration run away.
Both volumes cache resolved path entries and directory listings. The caches are bounded: at most 4,096 path entries and 512 directory listings are kept per volume, and once a budget is exhausted new entries are not stored (existing ones can still be updated), so a large tree cannot grow memory without limit for the mount lifetime. Cached listings are returned as the same list instance, so callers must treat the returned sequence as read-only.
XboxIsoVfsDokan.FindFiles augments directory listings with Windows-style virtual entries:
| Entry | When added |
|---|---|
. |
Always |
.. |
Every directory except the root |
Both virtual entries are reported as read-only directories and carry the volume creation time.
The table lists every operation implemented by XboxIsoVfsDokan, its behavior, and the status codes
it can return.
| Operation | Behavior | Status codes |
|---|---|---|
CreateFile |
Opens a handle; resolves the path and stores the IVfsEntry in info.Context. Denies write access, creation, and truncation. Directory/file type mismatches are reported precisely. |
Success, FileNotFound, AccessDenied, AlreadyExists, PathNotFound, NotADirectory, Error
|
ReadFile |
Reads up to the buffer size, clamped to the remaining file size. Returns InvalidHandle for directories and InvalidParameter for negative offsets. A read at or beyond the file size returns Success with 0 bytes. |
Success, InvalidParameter, InvalidHandle, Error
|
GetFileInformation |
Returns name, attributes, size, and timestamps. Directories report length 0. The synthetic root (empty name) reports the volume label. |
Success, FileNotFound, Error
|
FindFiles |
Lists ., .., and all real children. Requires a directory. |
Success, NotADirectory, Error
|
FindFilesWithPattern |
Lists children matching a wildcard translated to a case-insensitive regex with a 1-second match timeout. * and *.* match every file, including names without an extension; . and .. are always included. |
Success, NotADirectory, Error
|
GetFileSecurity |
Builds a FileSecurity or DirectorySecurity granting Everyone read and execute access. Missing entries report FileNotFound. |
Success, FileNotFound, Error
|
GetVolumeInformation |
Returns the volume label and file system name for the active volume: XBOX_ISO/XDVDFS for images, XBOX_ZAR/ZARCHIVE for ZArchive trees (an embedded XISO keeps the image values). Maximum component length 255, features `ReadOnlyVolume |
CasePreservedNames |
GetDiskFreeSpace |
Reports the ISO size as total capacity and 0 free bytes. |
Success, Error
|
FindStreams |
Alternate data streams are not supported. | NotImplemented |
LockFile / UnlockFile / FlushFileBuffers
|
No-op, reported as successful (Windows flushes on handle close; a read-only volume accepts it). | Success |
| Operation | Behavior |
|---|---|
Cleanup, CloseFile
|
Empty: no per-handle resources exist. |
Mounted, Unmounted
|
Return Success; exceptions are logged. |
| Operation | Status |
|---|---|
WriteFile |
AccessDenied (bytes written = 0) |
SetFileAttributes |
AccessDenied |
SetFileTime |
AccessDenied |
DeleteFile |
AccessDenied |
DeleteDirectory |
AccessDenied |
MoveFile |
AccessDenied |
SetEndOfFile |
AccessDenied |
SetAllocationSize |
AccessDenied |
SetFileSecurity |
AccessDenied |
The read-only volume option is already requested from Dokan, so most write attempts are rejected by the driver; the explicit denials are a second line of defense.
| Property | Value |
|---|---|
| Creation time | Volume creation time |
| Last access time | Volume creation time |
| Last write time | Volume creation time |
| Directory length | 0 |
| File length |
Size from the entry |
| Attributes | Always include ReadOnly; ISO entries also map XDVDFS flags, ZArchive entries are ReadOnly plus Normal/Directory (the format stores no attributes) |
| Security |
Everyone: ReadAndExecute allowed |
Windows may cache metadata, so all files appear to have the disc's creation timestamp.
All public operations except the empty lifecycle methods run through
ExecuteWithReporting(operation, fileName, action):
- Execute the action.
- On success, return its
NtStatus. - On exception, log
Dokan operation {Operation} failed for '{FileName}'at Error level, then returnDokanResult.Error.
Because the Serilog pipeline includes BugReportSink at Warning level, such failures are also
written to error.log and can be forwarded to the bug report API. See
Services and Privacy and Networking.
- A keep-open
XisoExplorerserializes its metadata operations on an internal lock; the stream-backedXisoVfsVolumelocks its held image stream around every seek/read.ZArchiveReaderis likewise internally locked and swaps whole decompressed 64 KiB blocks in and out of a bounded LRU cache.ChdImageStreamis not thread-safe, so the volume's stream lock serializes CHD reads; with--image-isothe syntheticimage.isouses a second, independent CHD reader. - Both volume implementations use concurrent dictionaries for their entry and children caches.
-
GetFolderListreturns the cached list, built on first request while the underlying stream or archive lock is taken per read.
User guide
Technical reference
Development
Project