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:
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);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in new issue
Block a user