Skip to content

VFS Public Interface

The Virtual File System (driver.storage.vfs) provides a unified API for file I/O, directory management, path resolution, symlinks, device nodes, and directory-change notifications. All paths are UNIX-style (/-separated). The VFS namespace is a tree of virtual directories, mount points, symlinks, and device nodes.


Types

Enumerations

Type Values Description
TOpenMode omRead, omWrite, omCreate, omReadWrite, omStream How a file handle is opened. See OpenFile for details.
TIsPathValid pvInvalid, pvFile, pvDirectory Result of path validation.
TObjectType otVDIRECTORY, otDRIVE, otDEVICE, otVFILE, otMOUNT, otDIRECTORY, otFILE, otSYMLINK Node types in the VFS tree.
TVFSWatchEvent weCreated, weDeleted, weRenamed, weModified Events that fire on watched directories.

Handle Type

TFileHandle = uint32;

Returned by OpenFile / OpenFileAsync. Value 0 means invalid/not open. Handles are 1-based indices into the per-process file descriptor table (max 64 open files per process).

Records

TVFSObject

TVFSObject = record
    Parent     : PVFSObject;
    ObjectName : pchar;
    ObjectType : TObjectType;
    Reference  : void;
    FileSize   : uint32;
end;

Internal VFS tree node. Exposed in directory listing snapshots returned by GetDirectoryListingFrom.

TVFSDeviceOps

TVFSDeviceOps = record
    Read  : TVFSDevReadFunc;
    Write : TVFSDevWriteFunc;
    Size  : TVFSDevSizeFunc;
end;

Function-pointer table for character/block device I/O. Any field may be nil if the device does not support that operation. See Device Nodes for callback signatures.


Initialisation

procedure init()

Initialises the VFS subsystem. Creates the root node and default virtual directories (/dev, /disk, /mnt, /cfg, /boot), registers built-in device nodes (/dev/null, /dev/zero), initialises the watch table and directory cache, and registers shell commands (LS, CD, PUSHD, POPD, MKDIR, RM, RMDIR, MOUNT, UMOUNT).

Called once during kernel boot. Must not be called again.


File I/O — Synchronous

OpenFile

function OpenFile(
    Filename  : pchar;
    OpenMode  : TOpenMode;
    Error     : PError
) : TFileHandle;

Opens a file or device node. Returns a non-zero handle on success, 0 on failure. Error^ is set to the specific TError code.

  • Device nodes (e.g. /dev/null) are detected before volume resolution. The FD is set up with device callbacks; no filesystem driver is involved.
  • omRead: Open existing file for reading. The entire file is pre-loaded into the FD's data buffer. Subsequent ReadFile calls read from this buffer by position.
  • omWrite: Open existing file for writing (overwrite).
  • omCreate: Create a new file for writing. Fails with eFileDoesNotExist if the file already exists.
  • omReadWrite: Open existing file for both reading and writing. Pre-loads data like omRead, but also allows WriteFile.
  • omStream: Streaming sequential I/O. No pre-load. Reads and writes use the filesystem's offset-based callbacks on demand, with an internal stream offset that auto-advances. Use SeekFile to reposition.

ReadFile

function ReadFile(
    FileHandle : TFileHandle;
    Position   : uint32;
    Buffer     : puint8;
    Length     : uint32
) : uint32;

Reads up to Length bytes into Buffer. Returns the number of bytes actually read.

  • Device FDs: Dispatches to DeviceOps^.Read. In stream mode, the internal stream offset is used and auto-advanced.
  • Stream mode (omStream): Position is ignored; reads from the current stream offset.
  • Buffered mode (omRead / omReadWrite): Reads from the pre-loaded buffer at Position.

WriteFile

function WriteFile(
    FileHandle : TFileHandle;
    Position   : uint32;
    Buffer     : puint8;
    Length     : uint32
) : uint32;

Writes Length bytes from Buffer. Returns bytes written (Length on success, 0 on failure).

  • Handle must have been opened with omWrite, omCreate, omReadWrite, or omStream.
  • Device FDs: Dispatches to DeviceOps^.Write. In stream mode, the internal stream offset is used and auto-advanced.
  • Stream mode (omStream): Position is ignored; writes at the current stream offset via the filesystem's writeOffsetCallback. Returns 0 if the FS does not implement writeOffsetCallback.
  • Volume FDs (non-stream): Delegates to the filesystem's write callback. The buffer is padded to 4096 bytes internally (drivers may read full sectors).

CloseFile

function CloseFile(FileHandle : TFileHandle) : TError;

Closes the file handle. Frees all associated buffers and strings. Returns eNone on success, eInvalidHandle if the handle is not open.

FileSize

function FileSize(Filename : pchar; error : PError) : uint32;

Opens the file read-only, reads its size, closes it, and returns the size in bytes. Convenience wrapper — for already-open files, prefer FileSizeFromHandle.

FileSizeFromHandle

function FileSizeFromHandle(Handle : TFileHandle) : uint32;

Returns the data size of an open file handle. For device FDs, calls DeviceOps^.Size if available. Returns 0 for invalid handles.

SeekFile

function SeekFile(Handle : TFileHandle; Offset : uint32) : TError;

Sets the stream offset for a handle opened with omStream. Returns eNotStreamMode if the handle was not opened in stream mode.

DeleteFile

function DeleteFile(Path : pchar; Error : PError) : TError;

Deletes a file at the given path. Returns eNone on success.

CreateDirectory

function CreateDirectory(Path : pchar; Error : PError) : TError;

Creates a directory on the underlying filesystem.

DeleteDirectory

function DeleteDirectory(Path : pchar; Error : PError) : TError;

Deletes an empty directory. Returns eDirectoryNotEmpty if the directory has entries.

RenameFile

function RenameFile(OldPath : pchar; NewName : pchar; Error : PError) : TError;

Renames a file. NewName is just the new filename, not a full path.


File I/O — Asynchronous

All async variants return immediately. The callback fires when the operation completes. Callers must keep all buffers alive until the callback fires.

OpenFileAsync

procedure OpenFileAsync(
    Filename     : pchar;
    OpenMode     : TOpenMode;
    var OutHandle : TFileHandle;
    Error        : PError;
    Callback     : TIOCallback;
    CallbackData : pointer
);

Async version of OpenFile. OutHandle and Error^ are set before the callback fires.

ReadFileAsync

procedure ReadFileAsync(
    FileHandle   : TFileHandle;
    Position     : uint32;
    Buffer       : puint8;
    Length       : uint32;
    BytesRead    : puint32;
    Callback     : TIOCallback;
    CallbackData : pointer
);

Async read. BytesRead^ is set before the callback fires. Device FDs are handled synchronously with an immediate callback.

WriteFileAsync

procedure WriteFileAsync(
    FileHandle   : TFileHandle;
    Position     : uint32;
    Buffer       : puint8;
    Length       : uint32;
    Callback     : TIOCallback;
    CallbackData : pointer
);

Async write. Prefers the filesystem's async write hook; falls back to sync + immediate callback.

  • Handle must have been opened with omWrite, omCreate, omReadWrite, or omStream.
  • Device FDs: Dispatches to DeviceOps^.Write synchronously, then fires callback. In stream mode, stream offset is auto-advanced.
  • Stream mode (omStream) for volume FDs: Uses writeOffsetCallback; fires eNotSupported if the FS does not implement it.
  • Non-stream volume FDs: Standard async/sync write dispatch with 4096-byte padding.

DeleteFileAsync

procedure DeleteFileAsync(
    Path         : pchar;
    Error        : PError;
    Callback     : TIOCallback;
    CallbackData : pointer
);

DeleteDirectoryAsync

procedure DeleteDirectoryAsync(
    Path         : pchar;
    Error        : PError;
    Callback     : TIOCallback;
    CallbackData : pointer
);

GetDirectoryListingAsync

procedure GetDirectoryListingAsync(
    Path         : pchar;
    ResultMap    : PPHashMap;
    Callback     : TIOCallback;
    CallbackData : pointer
);

Async directory listing. ResultMap^ is set to a caller-owned PHashMap snapshot before the callback fires. Caller must FreeDirectoryListing() the result.


Path Resolution

PathValid

function PathValid(Path : pchar) : TIsPathValid;

Tests whether a path resolves to a file, directory, or is invalid. Follows symlinks (up to 8 hops). Device nodes return pvFile.

MakeAbsolutePath

function MakeAbsolutePath(Path : pchar) : pchar;

Converts a path to absolute form using the current working directory. Returns a heap-allocated string — caller must kfree().

MakeAbsolutePathFrom

function MakeAbsolutePathFrom(Path : pchar; BaseDir : pchar) : pchar;

Same as MakeAbsolutePath but uses BaseDir instead of the process CWD. Returns a heap-allocated string — caller must kfree().

ResolvePathFrom

function ResolvePathFrom(Path : pchar; BaseDir : pchar) : TIsPathValid;

Resolves a relative path against BaseDir and returns its validity.

ChangeDirectory

function ChangeDirectory(Path : pchar) : TIsPathValid;

Sets the per-process current working directory. Returns the validity of the new path.

ChangeDirectoryFrom

function ChangeDirectoryFrom(
    Path    : pchar;
    BaseDir : pchar;
    var NewDir : pchar
) : TIsPathValid;

Changes directory relative to BaseDir. NewDir receives the new absolute path (heap-allocated, caller must kfree()).

GetWorkingDirectory

function GetWorkingDirectory : pchar;

Returns a heap-allocated copy of the per-process CWD. Caller must kfree().


Directory Listings

GetDirectoryListingFrom

function GetDirectoryListingFrom(Path : pchar; BaseDir : pchar) : PHashMap;

Returns a deep-copied snapshot PHashMap of the directory contents. Keys are entry names (pchar), values are PVFSObject. Returns nil if the path is invalid.

Multiple calls return independent snapshots — modifying or freeing one does not affect others.

Important: Caller must call FreeDirectoryListing() when done.

FreeDirectoryListing

procedure FreeDirectoryListing(map : PHashMap);

Frees a directory listing snapshot and all its deep-copied entries. Safe to pass nil.


Virtual Directories

newVirtualDirectory

function newVirtualDirectory(Path : pchar) : TError;

Creates a virtual directory in the VFS namespace tree. Parent directories must already exist. Returns eAlreadyExists if the name is taken.

Virtual directories exist only in the VFS tree — they are not backed by any filesystem and are lost on reboot.


Mount Points

mountVolume

function mountVolume(mountPath : pchar; volume : PStorage_Volume) : TRegError;

Mounts a storage volume at the given VFS path. The path must be an existing virtual directory. Returns pvRegistered on success.

auto_mount_volumes

procedure auto_mount_volumes();

Automatically mounts all detected volumes under /disk/ with auto-generated names (/disk/vol0, /disk/vol1, ...).

After mounting all volumes, reads MOUNT.ASR from the root of the boot volume (ISO). Each line in the file is a command:

mnt {device}/boot /boot
mnt {device}/sys  /sys
  • mnt <source> <target> — creates a symlink from <target> to <source>.
  • {device} is replaced with the boot volume's mount path (e.g. /disk/vol2).

This replaces the old asr.mnt persistent-mount mechanism.


function CreateSymlink(LinkPath : pchar; TargetPath : pchar) : TError;

Creates a symbolic link at LinkPath pointing to TargetPath. The parent directory of LinkPath must exist and be a virtual directory. The target path is stored as-is and resolved at access time. Symlink resolution is capped at 8 hops.

Returns: - eNone — success - eInvalidArgument — nil argument - eInvalidPath — parent directory does not exist - eAlreadyExists — name already taken - eNotADirectory — parent is not a directory


Device Nodes

Device nodes allow non-storage I/O to be accessed through the standard VFS file API. A device is registered at a VFS path with a table of read/write/size callbacks.

Callback Signatures

TVFSDevReadFunc  = function(devData : pointer; offset : uint32; buffer : puint8; length : uint32) : uint32;
TVFSDevWriteFunc = function(devData : pointer; offset : uint32; buffer : puint8; length : uint32) : uint32;
TVFSDevSizeFunc  = function(devData : pointer) : uint32;
  • devData — opaque pointer passed at registration time
  • offset — byte offset (for positional reads) or current stream offset
  • buffer — caller's data buffer
  • length — number of bytes requested
  • Return value — number of bytes actually read/written

Any callback may be nil if the device does not support that operation.

RegisterDevice

function RegisterDevice(
    Path    : pchar;
    Ops     : PVFSDeviceOps;
    DevData : pointer
) : TError;

Registers a device node at Path. The TVFSDeviceOps record is deep-copied so the caller's record can be stack-allocated. DevData is stored as-is (not copied).

The parent directory must exist and be a virtual directory.

Returns: - eNone — success - eInvalidArgument — nil path or ops - eDirectoryDoesNotExist — parent not found - eNotADirectory — parent is not a virtual directory - eAlreadyExists — name already taken

Built-in Devices

Path Read Write Size
/dev/null Returns 0 bytes (EOF) Accepts and discards all data 0
/dev/zero Fills buffer with zero bytes Accepts and discards all data 0

Example: Registering a Custom Device

function MyDev_Read(devData : pointer; offset : uint32; buffer : puint8; length : uint32) : uint32;
begin
    { Fill buffer with 'A' characters }
    memset(uint32(buffer), ord('A'), length);
    MyDev_Read := length;
end;

var
    ops : TVFSDeviceOps;
begin
    ops.Read  := @MyDev_Read;
    ops.Write := nil;
    ops.Size  := nil;
    RegisterDevice('/dev/mydevice', @ops, nil);
end;

The device can then be used with the standard file API:

var
    h   : TFileHandle;
    err : TError;
    buf : array[0..15] of uint8;
begin
    h := OpenFile('/dev/mydevice', omRead, @err);
    ReadFile(h, 0, @buf[0], 16);  { buf now contains 16 'A' bytes }
    CloseFile(h);
end;

Watch / Notify

Directory watches fire a callback whenever the watched directory is mutated (child created, deleted, renamed, or modified).

WatchDirectory

function WatchDirectory(
    Path     : pchar;
    Callback : TVFSWatchCallback;
    UserData : pointer
) : uint32;

Registers a watch on a directory path. Returns a non-zero watch ID on success, 0 on failure (nil args, path doesn't exist, or table full). Maximum 32 concurrent watches.

UnwatchDirectory

procedure UnwatchDirectory(WatchID : uint32);

Removes a watch by ID. Safe to call with 0 or invalid IDs.

Callback Signature

TVFSWatchCallback = procedure(
    event    : TVFSWatchEvent;
    path     : pchar;
    userdata : pointer
);
  • event — what kind of mutation occurred
  • path — the absolute path of the affected entry
  • userdata — the opaque pointer passed at registration time

Error Codes

All functions that return TError use the shared error codes defined in driver.storage.types. The most commonly returned codes from VFS operations are:

Code Meaning
eNone Success
eInvalidArgument Nil pointer or invalid parameter
eInvalidPath Malformed or unresolvable path
eInvalidHandle Handle is not open
eInvalidFileName Resolved filename is empty or invalid
eFileDoesNotExist Path resolves but no file found
eDirectoryDoesNotExist Parent directory not found
eNotADirectory Path component is not a directory
eAlreadyExists Target name already exists
eTooManyOpenFiles Per-process FD table full (max 64)
eNotStreamMode SeekFile on non-stream handle
eFileNotLoaded Read on handle whose data hasn't loaded
eReadOnly Write on read-only handle

See driver.storage.types.pas for the full TError enumeration.


Ownership & Lifetime Rules

  1. File handles — Must be closed with CloseFile when no longer needed. All handles are per-process and automatically closed on process exit.
  2. Directory listings — GetDirectoryListingFrom / GetDirectoryListingAsync return caller-owned snapshots. Always call FreeDirectoryListing().
  3. Heap strings — MakeAbsolutePath, MakeAbsolutePathFrom, GetWorkingDirectory, and ChangeDirectoryFrom (via NewDir) return heap-allocated strings. Caller must kfree().
  4. Async buffers — All buffers passed to async functions must remain valid until the completion callback fires.
  5. Device ops — The TVFSDeviceOps record is deep-copied by RegisterDevice. The caller's record can be stack-local. However, DevData is stored by pointer — its lifetime must exceed the device registration.