vfs: major overhaul — device nodes, stream I/O, mode simplification

- 5-phase VFS architecture overhaul (path resolution, dir cache, watch/notify,
  symlinks, async API)
- Add character/block device node layer with /dev/null and /dev/zero built-ins
- RegisterDevice() API for custom device nodes with read/write/size callbacks
- Merge TOpenMode + TWriteMode into single TOpenMode enum
  (omRead, omWrite, omCreate, omReadWrite, omStream)
- Add stream write support: WriteFile/WriteFileAsync now accept omStream handles
  with auto-advancing offset for both device and volume FDs
- Add PPWriteOffsetHook type and writeOffsetCallback field to TFilesystem
- Add PPReadOffsetHook-based streaming reads for FAT32 and ISO9660
- Update all callers (notepad, edit, inio, wasm runner, filepicker, filedispatch,
  vterminal, stdio) to new 2-param OpenFile API
- 77 unit tests (0 failures) covering path ops, dir cache, symlinks, watch,
  device nodes, and stream read/write
- Add doc/vfs.md — comprehensive public VFS API documentation
This commit is contained in:
Aaron committed 2026-03-08 18:07:44 +00:00
1 parent 63b67f5878
commit fcc9809248
17 files changed
+2774 -417

No files matched your search

@@ -148,6 +148,10 @@ begin
entry := PDirectory_Entry(LL_Add(dirList));
entry^.fileName := stringCopy(Files[i].Name);
entry^.entryType := fileEntry;
entry^.fileSize := Files[i].Size;
entry^.modifiedDate := 0;
entry^.modifiedTime := 0;
entry^.attributes := 0;
end;
end;
if status <> nil then status^ := 0;
@@ -41,11 +41,12 @@ type
Directory : pchar; { directory path within the volume }
FileName : pchar; { filename within that directory }
OpenMode : uint8; { TOpenMode ordinal — avoids VFS type dependency }
WriteMode : uint8; { TWriteMode ordinal }
DataBuffer : puint32; { pre-loaded data for small files, nil otherwise }
DataSize : uint32; { size of pre-loaded data in bytes }
Loaded : boolean; { true if pre-loaded into DataBuffer }
StreamOff : uint32; { current byte offset for streaming / on-demand reads }
DeviceOps : pointer; { PVFSDeviceOps — non-nil for device FDs }
DeviceData : pointer; { opaque data passed to device callbacks }
end;
PFDTable = ^TFDTable;
@@ -113,6 +114,8 @@ begin
entry^.Loaded := false;
entry^.DataSize := 0;
entry^.StreamOff := 0;
entry^.DeviceOps := nil;
entry^.DeviceData := nil;
end;
procedure fd_table_free(table : PFDTable);
@@ -133,9 +133,10 @@ begin
validity := driver.storage.vfs.PathValid(absPath);
if validity <> pvFile then exit;
{ 2. Open the file and read the header bytes }
{ 2. Open the file in stream mode — only reads the bytes we ask for,
avoids pre-loading the entire file just to check a few magic bytes. }
err := eNone;
fh := driver.storage.vfs.OpenFile(absPath, omReadOnly, wmRewrite, @err);
fh := driver.storage.vfs.OpenFile(absPath, omStream, @err);
if fh = 0 then exit;
memset(uint32(@headerBuf[0]), 0, MAX_MAGIC_LEN);
+29 -26
View File
@@ -104,68 +104,71 @@ begin
{ ---- makeAbsolutePathFrom ---- }
{ Absolute path should be returned unchanged }
p := driver.storage.vfs.makeAbsolutePathFrom('/foo/bar', '/base');
Assert(stringEquals(p, '/foo/bar'), 'makeAbsolutePathFrom: abs passthrough');
p := driver.storage.vfs.MakeAbsolutePathFrom('/foo/bar', '/base');
Assert(stringEquals(p, '/foo/bar'), 'MakeAbsolutePathFrom: abs passthrough');
kfree(void(p));
{ Relative path joined to base (base has trailing slash) }
p := driver.storage.vfs.makeAbsolutePathFrom('file.txt', '/home/');
Assert(stringEquals(p, '/home/file.txt'), 'makeAbsolutePathFrom: rel+base trailing /');
p := driver.storage.vfs.MakeAbsolutePathFrom('file.txt', '/home/');
Assert(stringEquals(p, '/home/file.txt'), 'MakeAbsolutePathFrom: rel+base trailing /');
kfree(void(p));
{ Relative path joined to base (base has no trailing slash) }
p := driver.storage.vfs.makeAbsolutePathFrom('file.txt', '/home');
Assert(stringEquals(p, '/home/file.txt'), 'makeAbsolutePathFrom: rel+base no trailing /');
p := driver.storage.vfs.MakeAbsolutePathFrom('file.txt', '/home');
Assert(stringEquals(p, '/home/file.txt'), 'MakeAbsolutePathFrom: rel+base no trailing /');
kfree(void(p));
{ ---- resolvePathFrom ---- }
{ Known virtual dirs resolve to pvDirectory }
res := driver.storage.vfs.resolvePathFrom('disk', '/');
Assert(res = pvDirectory, 'resolvePathFrom: /disk from /');
res := driver.storage.vfs.ResolvePathFrom('disk', '/');
Assert(res = pvDirectory, 'ResolvePathFrom: /disk from /');
res := driver.storage.vfs.resolvePathFrom('dev', '/');
Assert(res = pvDirectory, 'resolvePathFrom: /dev from /');
res := driver.storage.vfs.ResolvePathFrom('dev', '/');
Assert(res = pvDirectory, 'ResolvePathFrom: /dev from /');
res := driver.storage.vfs.resolvePathFrom('mnt', '/');
Assert(res = pvDirectory, 'resolvePathFrom: /mnt from /');
res := driver.storage.vfs.ResolvePathFrom('mnt', '/');
Assert(res = pvDirectory, 'ResolvePathFrom: /mnt from /');
res := driver.storage.vfs.resolvePathFrom('cfg', '/');
Assert(res = pvDirectory, 'resolvePathFrom: /cfg from /');
res := driver.storage.vfs.ResolvePathFrom('cfg', '/');
Assert(res = pvDirectory, 'ResolvePathFrom: /cfg from /');
{ Non-existent names resolve to pvInvalid }
res := driver.storage.vfs.resolvePathFrom('zzznope', '/');
Assert(res = pvInvalid, 'resolvePathFrom: nonexistent from /');
res := driver.storage.vfs.ResolvePathFrom('zzznope', '/');
Assert(res = pvInvalid, 'ResolvePathFrom: nonexistent from /');
{ ---- changeDirectoryFrom ---- }
newDir := nil;
res := driver.storage.vfs.changeDirectoryFrom('disk', '/', newDir);
Assert(res = pvDirectory, 'changeDirectoryFrom /disk: returns dir');
res := driver.storage.vfs.ChangeDirectoryFrom('disk', '/', newDir);
Assert(res = pvDirectory, 'ChangeDirectoryFrom /disk: returns dir');
Assert((newDir <> nil) and stringEquals(newDir, '/disk'),
'changeDirectoryFrom /disk: newDir = /disk');
'ChangeDirectoryFrom /disk: newDir = /disk');
if newDir <> nil then kfree(void(newDir));
newDir := nil;
res := driver.storage.vfs.changeDirectoryFrom('zzznope', '/', newDir);
Assert(res = pvInvalid, 'changeDirectoryFrom nonexistent: returns invalid');
Assert(newDir = nil, 'changeDirectoryFrom nonexistent: newDir nil');
res := driver.storage.vfs.ChangeDirectoryFrom('zzznope', '/', newDir);
Assert(res = pvInvalid, 'ChangeDirectoryFrom nonexistent: returns invalid');
Assert(newDir = nil, 'ChangeDirectoryFrom nonexistent: newDir nil');
{ ---- GetDirectoryListingFrom ---- }
{ Root listing should be non-nil (in-memory VFS always populated) }
map := driver.storage.vfs.GetDirectoryListingFrom('/', '/');
Assert(map <> nil, 'GetDirectoryListingFrom(/) not nil');
driver.storage.vfs.FreeDirectoryListing(map);
{ /disk, /dev, /mnt, /cfg created at init — must appear in root listing }
map := driver.storage.vfs.GetDirectoryListingFrom('/', '/');
Assert(map <> nil, 'GetDirectoryListingFrom(/) not nil (2)');
{ Note: we don't free this map — it's the live VFS in-memory reference,
NOT a caller-owned alloc. Freeing it would corrupt the VFS tree. }
{ All maps are now caller-owned snapshots — always safe to free }
driver.storage.vfs.FreeDirectoryListing(map);
{ Listing a leaf vdir returns non-nil (even if empty) }
map := driver.storage.vfs.GetDirectoryListingFrom('/dev', '/');
Assert(map <> nil, 'GetDirectoryListingFrom(/dev) not nil');
driver.storage.vfs.FreeDirectoryListing(map);
map := driver.storage.vfs.GetDirectoryListingFrom('/cfg', '/');
Assert(map <> nil, 'GetDirectoryListingFrom(/cfg) not nil');
driver.storage.vfs.FreeDirectoryListing(map);
{ Non-existent path returns nil (no crash) }
map := driver.storage.vfs.GetDirectoryListingFrom('/zzznope', '/');
@@ -309,7 +312,7 @@ begin
for i := 0 to 511 do
wbuf[i] := $A5;
err := eNone;
fh := driver.storage.vfs.OpenFile('/disk/dt_vol/TEST.TXT', omWriteOnly, wmNew, @err);
fh := driver.storage.vfs.OpenFile('/disk/dt_vol/TEST.TXT', omCreate, @err);
Assert(err = eNone, 'OpenFile for write: err = eNone');
n := driver.storage.vfs.WriteFile(fh, 0, wbuf, 512);
Assert(n = 512, 'WriteFile 512 bytes');
@@ -319,7 +322,7 @@ begin
rbuf := puint8(kalloc(512));
memset(uint32(rbuf), 0, 512);
err := eNone;
fh := driver.storage.vfs.OpenFile('/disk/dt_vol/TEST.TXT', omReadOnly, wmRewrite, @err);
fh := driver.storage.vfs.OpenFile('/disk/dt_vol/TEST.TXT', omRead, @err);
Assert(err = eNone, 'OpenFile for read: err = eNone');
n := driver.storage.vfs.ReadFile(fh, 0, rbuf, 512);
Assert(n = 512, 'ReadFile 512 bytes');
+17 -2
View File
@@ -90,6 +90,8 @@ type
eTooManyOpenFiles, { per-process FD table full }
eInvalidHandle, { TFileHandle does not refer to an open FD }
eNotStreamMode, { SeekFile called on a non-omStream handle }
eFileNotLoaded, { ReadFile on a pre-load handle whose data is not yet loaded }
eAlreadyExists, { generic: target name already exists (symlink, file, etc.) }
{ Disk / capacity errors }
eDiskFull, { FS reports no free clusters/blocks }
@@ -148,11 +150,17 @@ type
PPDeleteFileHook = procedure(volume : PStorage_Volume; filePath : pchar; status : puint32);
PPDeleteDirHook = procedure(volume : PStorage_Volume; path : pchar; status : puint32);
PPIdentifyHook = function(volume : PStorage_Volume) : boolean;
PPRenameFileHook = procedure(volume : PStorage_Volume; filePath : pchar; newName : pchar; status : puint32);
{ Offset-based read: reads byteCount bytes starting at byte offset into the file.
Returns the number of bytes actually read.
Filesystems that do not implement this leave the field nil and VFS falls back
to the load-all readCallback behaviour. }
PPReadOffsetHook = function(volume : PStorage_Volume; directory : pchar; fileName : pchar; offset : uint32; buffer : puint32; byteCount : uint32) : uint32;
{ Offset-based write: writes byteCount bytes starting at byte offset into the file.
Returns the number of bytes actually written.
Filesystems that do not implement this leave the field nil; VFS returns 0
for stream-mode writes on volume FDs. }
PPWriteOffsetHook = function(volume : PStorage_Volume; directory : pchar; fileName : pchar; offset : uint32; buffer : puint32; byteCount : uint32) : uint32;
{ === Async filesystem hook core.types ===
Each mirrors the corresponding sync hook but receives a TIOCallback + callbackData.
@@ -227,6 +235,8 @@ type
identifyCallback : PPIdentifyHook;
{ Offset-based read for streaming mode — nil if not implemented }
readOffsetCallback : PPReadOffsetHook;
{ Offset-based write for streaming mode — nil if not implemented }
writeOffsetCallback : PPWriteOffsetHook;
{ Async format — nil if FS only supports synchronous create }
createAsyncCallback : PPCreateAsyncHook;
{ Async variants of the main I/O hooks — nil if FS only supports sync.
@@ -237,6 +247,7 @@ type
readDirAsyncCallback : PPReadDirAsyncHook;
deleteFileAsyncCallback : PPDeleteFileAsyncHook;
deleteDirAsyncCallback : PPDeleteDirAsyncHook;
renameFileCallback : PPRenameFileHook;
end;
{ Generic storage volume }
@@ -253,8 +264,12 @@ type
{ Generic directory entry }
TDirectory_Entry = record
fileName : pchar;
entryType : TDirectory_Entry_Type;
fileName : pchar;
entryType : TDirectory_Entry_Type;
fileSize : uint32;
modifiedDate : uint16; { FAT-style: bits 15-9=year-1980, 8-5=month, 4-0=day }
modifiedTime : uint16; { FAT-style: bits 15-11=hour, 10-5=min, 4-0=sec/2 }
attributes : uint8; { FS-specific attribute bits }
end;
TDrive_Error = record
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -827,6 +827,10 @@ begin
dirEntry^.entryType := TDirectory_Entry_Type.directoryEntry
else
dirEntry^.entryType := TDirectory_Entry_Type.fileEntry;
dirEntry^.fileSize := fileEntrys[i].size;
dirEntry^.modifiedDate := 0;
dirEntry^.modifiedTime := 0;
dirEntry^.attributes := fileEntrys[i].attribues;
end;
kfree(void(compString));
@@ -562,6 +562,19 @@ begin
else
dirEntry^.entryType := TDirectory_Entry_Type.fileEntry;
dirEntry^.fileSize := rec^.dataLen_LSB;
dirEntry^.attributes := rec^.fileFlags;
{ Convert ISO 9660 date (year-1900,month,day,hour,min,sec) to FAT-style }
if rec^.recDate[0] >= 80 then
dirEntry^.modifiedDate := (uint16(rec^.recDate[0] - 80) shl 9)
or (uint16(rec^.recDate[1]) shl 5)
or uint16(rec^.recDate[2])
else
dirEntry^.modifiedDate := 0;
dirEntry^.modifiedTime := (uint16(rec^.recDate[3]) shl 11)
or (uint16(rec^.recDate[4]) shl 5)
or (uint16(rec^.recDate[5]) shr 1);
offset := offset + rec^.recLen;
end;
@@ -722,6 +735,7 @@ begin
filesystem.createDirCallback := nil;
filesystem.deleteFileCallback := nil;
filesystem.deleteDirCallback := nil;
filesystem.renameFileCallback := nil;
filesystem.readAsyncCallback := nil;
filesystem.readDirAsyncCallback := nil;