Files
Asuro/doc/planning/vfs.md
t3hn3rd 6005ee8576
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
fix: vfs.md incorrect link to document
../src/driver/storage/driver.storage.types.pas -> ../src/driver/storage/driver.storage.types.md
2026-03-08 21:24:14 +00:00

588 lines
18 KiB
Markdown

# 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
```pascal
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
```pascal
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
```pascal
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](#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`
```pascal
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`
```pascal
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`
```pascal
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`
```pascal
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`
```pascal
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`
```pascal
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`
```pascal
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`
```pascal
function DeleteFile(Path : pchar; Error : PError) : TError;
```
Deletes a file at the given path. Returns `eNone` on success.
### `CreateDirectory`
```pascal
function CreateDirectory(Path : pchar; Error : PError) : TError;
```
Creates a directory on the underlying filesystem.
### `DeleteDirectory`
```pascal
function DeleteDirectory(Path : pchar; Error : PError) : TError;
```
Deletes an empty directory. Returns `eDirectoryNotEmpty` if the directory has entries.
### `RenameFile`
```pascal
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`
```pascal
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`
```pascal
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`
```pascal
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`
```pascal
procedure DeleteFileAsync(
Path : pchar;
Error : PError;
Callback : TIOCallback;
CallbackData : pointer
);
```
### `DeleteDirectoryAsync`
```pascal
procedure DeleteDirectoryAsync(
Path : pchar;
Error : PError;
Callback : TIOCallback;
CallbackData : pointer
);
```
### `GetDirectoryListingAsync`
```pascal
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`
```pascal
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`
```pascal
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`
```pascal
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`
```pascal
function ResolvePathFrom(Path : pchar; BaseDir : pchar) : TIsPathValid;
```
Resolves a relative path against `BaseDir` and returns its validity.
### `ChangeDirectory`
```pascal
function ChangeDirectory(Path : pchar) : TIsPathValid;
```
Sets the per-process current working directory. Returns the validity of the new path.
### `ChangeDirectoryFrom`
```pascal
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`
```pascal
function GetWorkingDirectory : pchar;
```
Returns a heap-allocated copy of the per-process CWD. Caller must `kfree()`.
---
## Directory Listings
### `GetDirectoryListingFrom`
```pascal
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`
```pascal
procedure FreeDirectoryListing(map : PHashMap);
```
Frees a directory listing snapshot and all its deep-copied entries. Safe to pass `nil`.
---
## Virtual Directories
### `newVirtualDirectory`
```pascal
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`
```pascal
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`
```pascal
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.
---
## Symlinks
### `CreateSymlink`
```pascal
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
```pascal
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`
```pascal
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
```pascal
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:
```pascal
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`
```pascal
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`
```pascal
procedure UnwatchDirectory(WatchID : uint32);
```
Removes a watch by ID. Safe to call with `0` or invalid IDs.
### Callback Signature
```pascal
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](../src/driver/storage/driver.storage.types.md) 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.