Every file access in ZEngine currently uses raw std::filesystem::path directly at the call site — ProjectViewUIComponent, AssetManager, ShaderReader, etc. This means:
- Paths are OS-native (backslash on Windows, slash on POSIX) with no enforcement
- No cross-platform normalization — a path that works on macOS silently breaks on Windows
Location in the tree
ZEngine/ZEngine/Core/VFS/
VFSPath.h ← path value type
VFSPath.cpp
VFSError.h ← error enum + VFSResult
IVFSFile.h ← file handle interface
IVFSBackend.h ← backend interface (disk, zip, memory — implemented next ticket)
IVFSContext.h ← the VFS itself — the object callers hold
- VFSError.h
Write this first — everything else depends on VFSResult.
// ZEngine/Core/VFS/VFSError.h
#pragma once
#include <ZEngineDef.h>
#include <cstdint>
namespace ZEngine::Core::VFS
{
enum class VFSError : uint32_t
{
OK = 0,
NotFound = 1,
PermissionDenied= 2,
AlreadyExists = 3,
NotADirectory = 4,
NotAFile = 5,
InvalidPath = 6,
Unsupported = 7,
IOError = 8,
OutOfMemory = 9,
Corrupted = 10,
Cancelled = 11,
};
// Lightweight result type — no exceptions, no heap allocation.
// Pattern already used in the engine: explicit success/error, caller checks.
//
// Usage:
// VFSResult<uint64_t> r = file->Size();
// if (r.Succeeded()) { use r.Value(); }
// else { log r.Error(); }
template <typename T>
struct VFSResult
{
// Construct a success result
static VFSResult Ok(T value)
{
VFSResult r;
r.m_value = std::move(value);
r.m_error = VFSError::OK;
return r;
}
// Construct an error result
static VFSResult Fail(VFSError error)
{
ZENGINE_VALIDATE_ASSERT(error != VFSError::OK, "Use Ok() to construct success")
VFSResult r;
r.m_error = error;
return r;
}
bool Succeeded() const { return m_error == VFSError::OK; }
bool Failed() const { return m_error != VFSError::OK; }
VFSError Error() const { return m_error; }
// Only call Value() after checking Succeeded()
T& Value() { ZENGINE_VALIDATE_ASSERT(Succeeded(), "Accessing value of a failed VFSResult"); return m_value; }
const T& Value() const { ZENGINE_VALIDATE_ASSERT(Succeeded(), "Accessing value of a failed VFSResult"); return m_value; }
private:
T m_value = {};
VFSError m_error = VFSError::OK;
};
// Void specialisation for operations that succeed or fail with no return value
template <>
struct VFSResult<void>
{
static VFSResult Ok() { VFSResult r; r.m_error = VFSError::OK; return r; }
static VFSResult Fail(VFSError e) { VFSResult r; r.m_error = e; return r; }
bool Succeeded() const { return m_error == VFSError::OK; }
bool Failed() const { return m_error != VFSError::OK; }
VFSError Error() const { return m_error; }
private:
VFSError m_error = VFSError::OK;
};
} // namespace ZEngine::Core::VFS
- VFSPath.h / VFSPath.cpp
This is the core of the ticket. The VFSPath type is immutable after construction and always stores a normalized UTF-8 string with forward slashes.
// ZEngine/Core/VFS/VFSPath.h
#pragma once
#include <Core/Memory/Allocator.h>
#include <Core/VFS/VFSError.h>
#include <ZEngineDef.h>
#include <cstdint>
namespace ZEngine::Core::VFS
{
// Maximum length of any VFS path string (including null terminator).
// Matches MAX_FILE_PATH_COUNT already defined in ZEngineDef.h.
static constexpr size_t VFS_MAX_PATH = MAX_FILE_PATH_COUNT; // 256
// Maximum number of path components (segments between '/').
// e.g. "/a/b/c/d" has 4 components.
static constexpr size_t VFS_MAX_COMPONENTS = 32;
// A view into one segment of a VFSPath ("textures", "rock", "png").
// Points into the owning VFSPath's internal buffer — valid only while
// the owning VFSPath is alive and unmodified.
struct VFSPathComponent
{
const char* Data = nullptr;
size_t Length = 0;
bool Empty() const { return Length == 0 || Data == nullptr; }
};
// -----------------------------------------------------------------------
// VFSPath
//
// An immutable, normalized, absolute VFS path.
// Always begins with '/'. Never ends with '/' (except root "/").
// Forward-slash separator only. UTF-8 encoded.
// No heap allocation — stored in a fixed-size char buffer.
//
// Two construction paths:
// VFSPath::Parse(raw_string) — from any string (validates + normalizes)
// VFSPath::FromNative(os_path) — from a std::filesystem::path or OS string
//
// All other construction is disabled. Copying is cheap (256-byte stack copy).
// -----------------------------------------------------------------------
struct VFSPath
{
// Normalize and validate `raw`. Returns InvalidPath if the input
// cannot be made into a valid VFS path.
static VFSResult<VFSPath> Parse(cstring raw);
static VFSResult<VFSPath> Parse(const char* raw, size_t length);
// Convert an OS-native path to a VFSPath.
// Windows: strips drive letter, converts backslash to forward slash.
// POSIX: accepted as-is after normalization.
static VFSResult<VFSPath> FromNative(cstring native);
// The root path "/".
static VFSPath Root();
// Default-constructed path is invalid (empty buffer). Always use Parse/Root.
VFSPath() = default;
// Full normalized path string, e.g. "/textures/rock.png"
cstring CStr() const { return m_buffer; }
size_t Length() const { return m_length; }
bool IsValid() const { return m_length > 0; }
bool IsRoot() const { return m_length == 1 && m_buffer[0] == '/'; }
// Last component: "/textures/rock.png" → "rock.png"
VFSPathComponent Filename() const;
// Filename without extension: "rock.png" → "rock"
VFSPathComponent Stem() const;
// Extension including dot: "rock.png" → ".png" | "rock" → ""
VFSPathComponent Extension() const;
// Parent: "/textures/rock.png" → "/textures" | "/textures" → "/"
VFSPath Parent() const;
// All components as an array of views into m_buffer.
// Count is stored in m_component_count.
// "/a/b/c" → ["a", "b", "c"]
uint32_t ComponentCount() const { return m_component_count; }
VFSPathComponent ComponentAt(uint32_t index) const;
// Append a relative segment: path / "subdir" → "/textures/subdir"
// Returns InvalidPath if the result would exceed VFS_MAX_PATH.
VFSResult<VFSPath> Append(cstring segment) const;
VFSResult<VFSPath> Append(const VFSPath& other) const;
// Operator form of Append — asserts on failure (use in contexts where
// the path is known to be valid, e.g. compiled-in asset paths).
VFSPath operator/(cstring segment) const;
// Always byte-exact on the normalized string.
// Case sensitivity is a backend concern, not a path concern.
bool operator==(const VFSPath& other) const;
bool operator!=(const VFSPath& other) const;
bool operator< (const VFSPath& other) const; // for use in sorted containers
// Returns true if `this` is a path prefix of `other`.
// "/textures".IsPrefixOf("/textures/rock.png") == true
// "/tex".IsPrefixOf("/textures/rock.png") == false (component boundary)
bool IsPrefixOf(const VFSPath& other) const;
// Returns a std::filesystem::path with the OS-native separator.
// Requires <filesystem> — only call from platform/OS-facing code,
// never from hot paths or asset browser rendering.
// (Defined in VFSPath.cpp with #include <filesystem>)
void ToNative(char* out_buffer, size_t out_size) const;
// FNV-1a hash of the normalized string. Fast and stable.
uint64_t Hash() const { return m_hash; }
private:
// Stores the normalized path. Fixed-size — no heap allocation.
char m_buffer[VFS_MAX_PATH] = {};
size_t m_length = 0;
// Offsets (into m_buffer) of the start of each component.
// Component i runs from m_component_offsets[i] for m_component_lengths[i] chars.
uint16_t m_component_offsets[VFS_MAX_COMPONENTS] = {};
uint16_t m_component_lengths[VFS_MAX_COMPONENTS] = {};
uint32_t m_component_count = 0;
// Pre-computed FNV-1a hash for O(1) hash-map use.
uint64_t m_hash = 0;
// Internal: called by Parse/FromNative after the buffer is written.
// Walks m_buffer and fills m_component_offsets, m_component_lengths,
// m_component_count, m_hash.
void BuildComponents();
uint64_t ComputeHash() const;
};
} // namespace ZEngine::Core::VFS
Implementation notes for VFSPath.cpp:
Parse(raw) algorithm (in-place, no allocation):
1. Copy `raw` into a temp char[VFS_MAX_PATH] buffer
2. Replace all '\\' with '/'
3. If Windows: strip drive letter prefix (e.g. "C:") if present
4. Collapse all double slashes ("//") into single "/"
5. Resolve "." and ".." components:
- Walk components left to right
- "." → skip
- ".." → pop last component; if stack empty → return InvalidPath (root escape)
- Anything else → push
6. Reconstruct the normalized string with a leading "/"
7. Strip trailing "/" unless result is "/"
8. Validate result length ≤ VFS_MAX_PATH - 1
9. Call BuildComponents()
BuildComponents() algorithm:
Walk m_buffer char by char.
On each '/' (excluding the leading one), a new component starts.
Record offset of start, count chars until next '/' or null.
Store in m_component_offsets / m_component_lengths.
Compute m_hash via FNV-1a over m_buffer[0..m_length].
- IVFSFile.h
The file handle interface. Backends return a pointer to a concrete implementation of this. Callers never see the concrete type.
// ZEngine/Core/VFS/IVFSFile.h
#pragma once
#include <Core/VFS/VFSError.h>
#include <Core/VFS/VFSPath.h>
#include <cstdint>
#include <span>
namespace ZEngine::Core::VFS
{
enum class VFSOpenFlags : uint32_t
{
None = 0,
Read = 1 << 0,
Write = 1 << 1,
Append = 1 << 2,
Create = 1 << 3, // create if not exists (requires Write)
Truncate = 1 << 4, // truncate on open (requires Write)
};
inline VFSOpenFlags operator|(VFSOpenFlags a, VFSOpenFlags b)
{ return static_cast<VFSOpenFlags>(static_cast<uint32_t>(a) | static_cast<uint32_t>(b)); }
inline bool operator&(VFSOpenFlags a, VFSOpenFlags b)
{ return (static_cast<uint32_t>(a) & static_cast<uint32_t>(b)) != 0; }
// Returned by Stat()
struct VFSFileStat
{
uint64_t SizeBytes = 0;
int64_t MTimeNs = 0; // last modified, nanoseconds since epoch
bool IsDirectory = false;
bool IsReadOnly = false;
};
// -----------------------------------------------------------------------
// IVFSFile
//
// A handle to an open file. Returned by IVFSBackend::Open().
// Closed automatically on destruction (RAII).
//
// All reads and writes use explicit byte offsets (pread/pwrite semantics).
// There is no internal seek cursor — this allows multiple async workers to
// read the same file handle concurrently without locking.
// -----------------------------------------------------------------------
struct IVFSFile
{
virtual ~IVFSFile() = default;
// Read `buffer.size()` bytes starting at `offset` into `buffer`.
// Returns the number of bytes actually read (may be less than requested
// at end-of-file). Returns IOError on failure.
virtual VFSResult<size_t> Read(std::span<uint8_t> buffer, uint64_t offset) = 0;
// Write `buffer.size()` bytes from `buffer` at `offset`.
// Returns the number of bytes written. Returns Unsupported on read-only files.
virtual VFSResult<size_t> Write(std::span<const uint8_t> buffer, uint64_t offset) = 0;
// Total file size in bytes.
virtual VFSResult<uint64_t> Size() const = 0;
// File metadata.
virtual VFSResult<VFSFileStat> Stat() const = 0;
// Flush pending writes to the backend. No-op for read-only files.
virtual VFSResult<void> Flush() = 0;
// The VFS path this file was opened at.
virtual const VFSPath& Path() const = 0;
// Optional: zero-copy read.
// Returns a span directly into the backend's memory (e.g. memory-mapped file,
// or a MemoryBackend's internal buffer).
// Returns Unsupported if not available — caller falls back to Read().
// The returned span is valid until the file is closed.
virtual VFSResult<std::span<const uint8_t>> MemoryMap() { return VFSResult<std::span<const uint8_t>>::Fail(VFSError::Unsupported); }
// Convenience: read entire file into a caller-provided buffer.
// Allocates nothing — caller owns the buffer.
// Returns total bytes read.
VFSResult<size_t> ReadAll(std::span<uint8_t> out_buffer);
};
} // namespace ZEngine::Core::VFS
- IVFSBackend.h
A backend is the thing that actually knows how to open files — a disk directory, a ZIP archive, or an in-memory store. This interface is implemented in the next ticket. It is declared here so IVFSContext can reference it.
// ZEngine/Core/VFS/IVFSBackend.h
#pragma once
#include <Core/VFS/IVFSFile.h>
#include <Core/VFS/VFSPath.h>
#include <Core/Containers/Array.h>
#include <cstdint>
#include <memory>
namespace ZEngine::Core::VFS
{
// One entry returned by IVFSBackend::List()
struct VFSDirEntry
{
VFSPath Path = {};
VFSFileStat Stat = {};
bool IsDirectory = false;
};
enum class VFSBackendCaps : uint32_t
{
None = 0,
Read = 1 << 0,
Write = 1 << 1,
List = 1 << 2,
MemoryMap = 1 << 3,
Watch = 1 << 4,
};
inline VFSBackendCaps operator|(VFSBackendCaps a, VFSBackendCaps b)
{ return static_cast<VFSBackendCaps>(static_cast<uint32_t>(a) | static_cast<uint32_t>(b)); }
inline bool operator&(VFSBackendCaps a, VFSBackendCaps b)
{ return (static_cast<uint32_t>(a) & static_cast<uint32_t>(b)) != 0; }
// -----------------------------------------------------------------------
// IVFSBackend
//
// Abstract storage backend. Concrete implementations:
// VFSDiskBackend — wraps std::filesystem (next ticket)
// VFSZipBackend — reads from a ZIP/PAK archive (later)
// VFSMemoryBackend — in-memory store for tests and scratch data (later)
//
// An IVFSBackend is associated with one logical root in the mount table.
// All paths passed to backend methods are RELATIVE to that root.
// The IVFSContext strips the mount prefix before calling the backend.
// -----------------------------------------------------------------------
struct IVFSBackend
{
virtual ~IVFSBackend() = default;
// Open a file. `path` is relative to this backend's root.
virtual VFSResult<IVFSFile*> Open(const VFSPath& path, VFSOpenFlags flags) = 0;
// Release a file previously returned by Open().
// After Close(), the IVFSFile* is invalid.
virtual void Close(IVFSFile* file) = 0;
// Stat a path without opening it.
virtual VFSResult<VFSFileStat> Stat(const VFSPath& path) const = 0;
// Check existence without stat.
virtual bool Exists(const VFSPath& path) const = 0;
// List the contents of a directory.
// Returns entries in unspecified order.
// `arena` is used to allocate the returned Array — lifetime tied to arena.
virtual VFSResult<Core::Containers::Array<VFSDirEntry>>
List(Core::Memory::ArenaAllocator* arena,
const VFSPath& dir) const = 0;
// Write operations — return Unsupported if backend is read-only.
virtual VFSResult<void> CreateDir(const VFSPath& path) { return VFSResult<void>::Fail(VFSError::Unsupported); }
virtual VFSResult<void> Remove(const VFSPath& path) { return VFSResult<void>::Fail(VFSError::Unsupported); }
virtual VFSResult<void> Rename(const VFSPath& from,
const VFSPath& to) { return VFSResult<void>::Fail(VFSError::Unsupported); }
// Backend identity and capabilities.
virtual cstring BackendType() const = 0; // "disk", "zip", "memory"
virtual VFSBackendCaps Capabilities() const = 0;
};
} // namespace ZEngine::Core::VFS
- IVFSContext.h
The object all engine code holds. One instance per engine, owned by the engine startup sequence. In this ticket it has no mount table — that is the next ticket. Here it exposes a minimal interface so existing call sites can be migrated now.
// ZEngine/Core/VFS/IVFSContext.h
#pragma once
#include <Core/VFS/IVFSBackend.h>
#include <Core/VFS/IVFSFile.h>
#include <Core/VFS/VFSPath.h>
namespace ZEngine::Core::VFS
{
// -----------------------------------------------------------------------
// IVFSContext
//
// The top-level VFS object. Owns the mount table (next ticket) and
// routes all Open/Stat/List calls to the appropriate backend.
//
// This ticket: declare the interface + a passthrough DiskContext that
// forwards directly to std::filesystem (zero behaviour change, allows
// call sites to migrate to the new API before the mount table exists).
// -----------------------------------------------------------------------
struct IVFSContext
{
virtual ~IVFSContext() = default;
// Open a file by absolute VFS path.
virtual VFSResult<IVFSFile*> Open(const VFSPath& path, VFSOpenFlags flags) = 0;
// Close a file returned by Open().
virtual void Close(IVFSFile* file) = 0;
// Stat without opening.
virtual VFSResult<VFSFileStat> Stat(const VFSPath& path) const = 0;
// Check existence.
virtual bool Exists(const VFSPath& path) const = 0;
// List a directory.
virtual VFSResult<Core::Containers::Array<VFSDirEntry>>
List(Core::Memory::ArenaAllocator* arena,
const VFSPath& dir) const = 0;
// ---- The mount table API (stubbed in this ticket, implemented next) ----
// Mount a backend at a logical VFS root.
// `logical_root` e.g. VFSPath::Parse("/game")
// `priority` — higher wins on collision (for overlay, next ticket)
virtual VFSResult<void> Mount(IVFSBackend* backend,
const VFSPath& logical_root,
int priority = 0) = 0;
// Unmount by logical root.
virtual VFSResult<void> Unmount(const VFSPath& logical_root) = 0;
};
// -----------------------------------------------------------------------
// VFSDiskContext
//
// A concrete IVFSContext that wraps a single disk directory.
// This is the ONLY concrete implementation delivered in this ticket.
// It is a direct passthrough to std::filesystem — behaviour is identical
// to what exists today, but through the new typed interface.
//
// Constructed with a native root path (the working space path from
// EditorConfiguration / AssetManager::CurrentWorkingSpacePath).
// All VFS paths are resolved relative to that root.
//
// Mount() / Unmount() are stubbed — they log a warning and return OK.
// The full mount table replaces this in the next ticket.
// -----------------------------------------------------------------------
struct VFSDiskContext final : public IVFSContext
{
// `native_root` must be an absolute OS path (e.g. the project's asset dir).
explicit VFSDiskContext(cstring native_root);
~VFSDiskContext() override;
VFSResult<IVFSFile*> Open(const VFSPath& path, VFSOpenFlags flags) override;
void Close(IVFSFile* file) override;
VFSResult<VFSFileStat> Stat(const VFSPath& path) const override;
bool Exists(const VFSPath& path) const override;
VFSResult<Core::Containers::Array<VFSDirEntry>>
List(Core::Memory::ArenaAllocator* arena,
const VFSPath& dir) const override;
VFSResult<void> Mount(IVFSBackend*, const VFSPath&, int) override;
VFSResult<void> Unmount(const VFSPath&) override;
private:
// Translate a VFSPath to an absolute OS path under m_native_root.
// Validates the result stays under m_native_root (no "../" escape).
void ToNativePath(const VFSPath& vfs_path,
char* out_buffer, size_t out_size) const;
char m_native_root[VFS_MAX_PATH] = {};
size_t m_native_root_len = 0;
};
} // namespace ZEngine::Core::VFS
- Migration: how existing call sites change
The migrates should concern two existing call sites as proof of concept. The rest follow in a later cleanup pass.
AssetManager::LoadTextureFileAsAsset — currently takes cstring file, bool absolute:
// Before
asset_mgr->LoadTextureFileAsAsset(directory_icon_path.c_str(), true);
// After (VFSPath constructed from the absolute OS path)
auto icon_vfs_path = VFSPath::FromNative(directory_icon_path.c_str());
if (icon_vfs_path.Succeeded())
{
asset_mgr->LoadTextureFileAsAsset(icon_vfs_path.Value());
}
ProjectViewUIComponent::Initialize — sets m_assets_directory:
// Before
m_assets_directory = ParentLayer->CurrentApp->WorkingSpacePath; // std::filesystem::path
// After
auto result = VFSPath::FromNative(ParentLayer->CurrentApp->WorkingSpacePath.string().c_str());
ZENGINE_VALIDATE_ASSERT(result.Succeeded(), "WorkingSpacePath is not a valid VFS path")
m_assets_directory_vfs = result.Value();
Deliverables checklist
[ ] ZEngine/Core/VFS/VFSError.h — VFSError enum + VFSResult
[ ] ZEngine/Core/VFS/VFSPath.h — VFSPath declaration
[ ] ZEngine/Core/VFS/VFSPath.cpp — Parse, FromNative, BuildComponents,
Append, IsPrefixOf, ToNative, operators
[ ] ZEngine/Core/VFS/IVFSFile.h — IVFSFile interface + VFSOpenFlags + VFSFileStat
Append, IsPrefixOf, ToNative, operators
[ ] ZEngine/Core/VFS/IVFSFile.h — IVFSFile interface + VFSOpenFlags + VFSFileStat
[ ] ZEngine/Core/VFS/IVFSBackend.h — IVFSBackend interface + VFSDirEntry + VFSBackendCaps
[ ] ZEngine/Core/VFS/IVFSContext.h — IVFSContext interface + VFSDiskContext
[ ] ZEngine/Core/VFS/VFSDiskContext.cpp — VFSDiskContext implementation
[ ] ZEngine/tests/ test_vfspath.cpp — unit tests (see below)
[ ] Migrate AssetManager::LoadTextureFileAsAsset to accept VFSPath
[ ] Migrate ProjectViewUIComponent::m_assets_directory to VFSPath
[ ] Update ZEngine CMakeLists.txt to include Core/VFS/ sources
Test cases for test_vfspath.cpp
Parse("/textures/rock.png") → OK, CStr() == "/textures/rock.png"
Parse("textures/rock.png") → OK, CStr() == "/textures/rock.png"
Parse("textures/../rock.png") → OK, CStr() == "/rock.png"
Parse("./textures/rock.png") → OK, CStr() == "/textures/rock.png"
Deliverables checklist for the engineer
[ ] ZEngine/Core/VFS/VFSError.h — VFSError enum + VFSResult
[ ] ZEngine/Core/VFS/VFSPath.h — VFSPath declaration
[ ] ZEngine/Core/VFS/VFSPath.cpp — Parse, FromNative, BuildComponents,
Append, IsPrefixOf, ToNative, operators
[ ] ZEngine/Core/VFS/IVFSFile.h — IVFSFile interface + VFSOpenFlags + VFSFileStat
[ ] ZEngine/Core/VFS/IVFSBackend.h — IVFSBackend interface + VFSDirEntry + VFSBackendCaps
[ ] ZEngine/Core/VFS/IVFSFile.h — IVFSFile interface + VFSOpenFlags + VFSFileStat
[ ] ZEngine/Core/VFS/IVFSBackend.h — IVFSBackend interface + VFSDirEntry + VFSBackendCaps
[ ] ZEngine/Core/VFS/IVFSFile.h — IVFSFile interface + VFSOpenFlags + VFSFileStat
[ ] ZEngine/Core/VFS/IVFSBackend.h — IVFSBackend interface + VFSDirEntry + VFSBackendCaps
[ ] ZEngine/Core/VFS/IVFSContext.h — IVFSContext interface + VFSDiskContext
[ ] ZEngine/Core/VFS/VFSDiskContext.cpp — VFSDiskContext implementation
Append, IsPrefixOf, ToNative, operators
[ ] ZEngine/Core/VFS/IVFSFile.h — IVFSFile interface + VFSOpenFlags + VFSFileStat
[ ] ZEngine/Core/VFS/IVFSBackend.h — IVFSBackend interface + VFSDirEntry + VFSBackendCaps
[ ] ZEngine/Core/VFS/IVFSContext.h — IVFSContext interface + VFSDiskContext
[ ] ZEngine/Core/VFS/VFSDiskContext.cpp — VFSDiskContext implementation
[ ] ZEngine/tests/ test_vfspath.cpp — unit tests (see below)
[ ] ZEngine/Core/VFS/VFSDiskContext.cpp — VFSDiskContext implementation
[ ] ZEngine/tests/ test_vfspath.cpp — unit tests (see below)
[ ] ZEngine/Core/VFS/VFSDiskContext.cpp — VFSDiskContext implementation
[ ] ZEngine/tests/ test_vfspath.cpp — unit tests (see below)
[ ] ZEngine/Core/VFS/VFSDiskContext.cpp — VFSDiskContext implementation
[ ] ZEngine/tests/ test_vfspath.cpp — unit tests (see below)
[ ] Migrate AssetManager::LoadTextureFileAsAsset to accept VFSPath
[ ] Migrate ProjectViewUIComponent::m_assets_directory to VFSPath
[ ] Update ZEngine CMakeLists.txt to include Core/VFS/ sources
Test cases for test_vfspath.cpp
Parse("/textures/rock.png") → OK, CStr() == "/textures/rock.png"
Parse("textures/rock.png") → OK, CStr() == "/textures/rock.png"
Parse("textures/../rock.png") → OK, CStr() == "/rock.png"
Parse("./textures/rock.png") → OK, CStr() == "/textures/rock.png"
Parse("//textures///rock.png") → OK, CStr() == "/textures/rock.png"
Parse("/textures/rock.png/") → OK, CStr() == "/textures/rock.png"
Parse("../../escape") → Fail(InvalidPath)
Parse("") → Fail(InvalidPath)
FromNative("C:\Assets\rock") → OK, CStr() == "/Assets/rock" [Windows only]
VFSPath::Root() → CStr() == "/"
path.Filename() → "rock.png"
path.Stem() → "rock"
path.Extension() → ".png"
path.Parent() → "/textures"
path.ComponentCount() → 2
[ ] ZEngine/Core/VFS/VFSDiskContext.cpp — VFSDiskContext implementation
[ ] ZEngine/tests/ test_vfspath.cpp — unit tests (see below)
[ ] ZEngine/Core/VFS/VFSDiskContext.cpp — VFSDiskContext implementation
[ ] ZEngine/tests/ test_vfspath.cpp — unit tests (see below)
[ ] Migrate AssetManager::LoadTextureFileAsAsset to accept VFSPath
[ ] Migrate ProjectViewUIComponent::m_assets_directory to VFSPath
[ ] Update ZEngine CMakeLists.txt to include Core/VFS/ sources
Test cases for test_vfspath.cpp
Parse("/textures/rock.png") → OK, CStr() == "/textures/rock.png"
Parse("textures/rock.png") → OK, CStr() == "/textures/rock.png"
Parse("textures/../rock.png") → OK, CStr() == "/rock.png"
Parse("./textures/rock.png") → OK, CStr() == "/textures/rock.png"
Parse("//textures///rock.png") → OK, CStr() == "/textures/rock.png"
Parse("/textures/rock.png/") → OK, CStr() == "/textures/rock.png"
Parse("../../escape") → Fail(InvalidPath)
Parse("") → Fail(InvalidPath)
FromNative("C:\Assets\rock") → OK, CStr() == "/Assets/rock" [Windows only]
VFSPath::Root() → CStr() == "/"
path.Filename() → "rock.png"
path.Stem() → "rock"
path.Extension() → ".png"
path.Parent() → "/textures"
path.ComponentCount() → 2
path / "sub" → "/textures/rock.png/sub" [asserts on failure]
"/tex".IsPrefixOf("/textures") → false (not a component boundary)
"/textures".IsPrefixOf("/textures/rock.png") → true
Two VFSPaths from same input have equal Hash()
Two VFSPaths from different inputs have different Hash()
Every file access in ZEngine currently uses raw std::filesystem::path directly at the call site — ProjectViewUIComponent, AssetManager, ShaderReader, etc. This means:
Location in the tree
ZEngine/ZEngine/Core/VFS/
VFSPath.h ← path value type
VFSPath.cpp
VFSError.h ← error enum + VFSResult
IVFSFile.h ← file handle interface
IVFSBackend.h ← backend interface (disk, zip, memory — implemented next ticket)
IVFSContext.h ← the VFS itself — the object callers hold
Write this first — everything else depends on VFSResult.
This is the core of the ticket. The VFSPath type is immutable after construction and always stores a normalized UTF-8 string with forward slashes.
Implementation notes for VFSPath.cpp:
The file handle interface. Backends return a pointer to a concrete implementation of this. Callers never see the concrete type.
A backend is the thing that actually knows how to open files — a disk directory, a ZIP archive, or an in-memory store. This interface is implemented in the next ticket. It is declared here so IVFSContext can reference it.
The object all engine code holds. One instance per engine, owned by the engine startup sequence. In this ticket it has no mount table — that is the next ticket. Here it exposes a minimal interface so existing call sites can be migrated now.
The migrates should concern two existing call sites as proof of concept. The rest follow in a later cleanup pass.
AssetManager::LoadTextureFileAsAsset — currently takes cstring file, bool absolute:
ProjectViewUIComponent::Initialize — sets m_assets_directory:
Deliverables checklist
[ ] ZEngine/Core/VFS/VFSError.h — VFSError enum + VFSResult
[ ] ZEngine/Core/VFS/VFSPath.h — VFSPath declaration
[ ] ZEngine/Core/VFS/VFSPath.cpp — Parse, FromNative, BuildComponents,
Append, IsPrefixOf, ToNative, operators
[ ] ZEngine/Core/VFS/IVFSFile.h — IVFSFile interface + VFSOpenFlags + VFSFileStat
Append, IsPrefixOf, ToNative, operators
[ ] ZEngine/Core/VFS/IVFSFile.h — IVFSFile interface + VFSOpenFlags + VFSFileStat
[ ] ZEngine/Core/VFS/IVFSBackend.h — IVFSBackend interface + VFSDirEntry + VFSBackendCaps
[ ] ZEngine/Core/VFS/IVFSContext.h — IVFSContext interface + VFSDiskContext
[ ] ZEngine/Core/VFS/VFSDiskContext.cpp — VFSDiskContext implementation
[ ] ZEngine/tests/ test_vfspath.cpp — unit tests (see below)
[ ] Migrate AssetManager::LoadTextureFileAsAsset to accept VFSPath
[ ] Migrate ProjectViewUIComponent::m_assets_directory to VFSPath
[ ] Update ZEngine CMakeLists.txt to include Core/VFS/ sources
Test cases for test_vfspath.cpp
Parse("/textures/rock.png") → OK, CStr() == "/textures/rock.png"
Parse("textures/rock.png") → OK, CStr() == "/textures/rock.png"
Parse("textures/../rock.png") → OK, CStr() == "/rock.png"
Parse("./textures/rock.png") → OK, CStr() == "/textures/rock.png"
Deliverables checklist for the engineer
[ ] ZEngine/Core/VFS/VFSError.h — VFSError enum + VFSResult
[ ] ZEngine/Core/VFS/VFSPath.h — VFSPath declaration
[ ] ZEngine/Core/VFS/VFSPath.cpp — Parse, FromNative, BuildComponents,
Append, IsPrefixOf, ToNative, operators
[ ] ZEngine/Core/VFS/IVFSFile.h — IVFSFile interface + VFSOpenFlags + VFSFileStat
[ ] ZEngine/Core/VFS/IVFSBackend.h — IVFSBackend interface + VFSDirEntry + VFSBackendCaps
[ ] ZEngine/Core/VFS/IVFSFile.h — IVFSFile interface + VFSOpenFlags + VFSFileStat
[ ] ZEngine/Core/VFS/IVFSBackend.h — IVFSBackend interface + VFSDirEntry + VFSBackendCaps
[ ] ZEngine/Core/VFS/IVFSFile.h — IVFSFile interface + VFSOpenFlags + VFSFileStat
[ ] ZEngine/Core/VFS/IVFSBackend.h — IVFSBackend interface + VFSDirEntry + VFSBackendCaps
[ ] ZEngine/Core/VFS/IVFSContext.h — IVFSContext interface + VFSDiskContext
[ ] ZEngine/Core/VFS/VFSDiskContext.cpp — VFSDiskContext implementation
Append, IsPrefixOf, ToNative, operators
[ ] ZEngine/Core/VFS/IVFSFile.h — IVFSFile interface + VFSOpenFlags + VFSFileStat
[ ] ZEngine/Core/VFS/IVFSBackend.h — IVFSBackend interface + VFSDirEntry + VFSBackendCaps
[ ] ZEngine/Core/VFS/IVFSContext.h — IVFSContext interface + VFSDiskContext
[ ] ZEngine/Core/VFS/VFSDiskContext.cpp — VFSDiskContext implementation
[ ] ZEngine/tests/ test_vfspath.cpp — unit tests (see below)
[ ] ZEngine/Core/VFS/VFSDiskContext.cpp — VFSDiskContext implementation
[ ] ZEngine/tests/ test_vfspath.cpp — unit tests (see below)
[ ] ZEngine/Core/VFS/VFSDiskContext.cpp — VFSDiskContext implementation
[ ] ZEngine/tests/ test_vfspath.cpp — unit tests (see below)
[ ] ZEngine/Core/VFS/VFSDiskContext.cpp — VFSDiskContext implementation
[ ] ZEngine/tests/ test_vfspath.cpp — unit tests (see below)
[ ] Migrate AssetManager::LoadTextureFileAsAsset to accept VFSPath
[ ] Migrate ProjectViewUIComponent::m_assets_directory to VFSPath
[ ] Update ZEngine CMakeLists.txt to include Core/VFS/ sources
Test cases for test_vfspath.cpp
Parse("/textures/rock.png") → OK, CStr() == "/textures/rock.png"
Parse("textures/rock.png") → OK, CStr() == "/textures/rock.png"
Parse("textures/../rock.png") → OK, CStr() == "/rock.png"
Parse("./textures/rock.png") → OK, CStr() == "/textures/rock.png"
Parse("//textures///rock.png") → OK, CStr() == "/textures/rock.png"
Parse("/textures/rock.png/") → OK, CStr() == "/textures/rock.png"
Parse("../../escape") → Fail(InvalidPath)
Parse("") → Fail(InvalidPath)
FromNative("C:\Assets\rock") → OK, CStr() == "/Assets/rock" [Windows only]
VFSPath::Root() → CStr() == "/"
path.Filename() → "rock.png"
path.Stem() → "rock"
path.Extension() → ".png"
path.Parent() → "/textures"
path.ComponentCount() → 2
[ ] ZEngine/Core/VFS/VFSDiskContext.cpp — VFSDiskContext implementation
[ ] ZEngine/tests/ test_vfspath.cpp — unit tests (see below)
[ ] ZEngine/Core/VFS/VFSDiskContext.cpp — VFSDiskContext implementation
[ ] ZEngine/tests/ test_vfspath.cpp — unit tests (see below)
[ ] Migrate AssetManager::LoadTextureFileAsAsset to accept VFSPath
[ ] Migrate ProjectViewUIComponent::m_assets_directory to VFSPath
[ ] Update ZEngine CMakeLists.txt to include Core/VFS/ sources
Test cases for test_vfspath.cpp
Parse("/textures/rock.png") → OK, CStr() == "/textures/rock.png"
Parse("textures/rock.png") → OK, CStr() == "/textures/rock.png"
Parse("textures/../rock.png") → OK, CStr() == "/rock.png"
Parse("./textures/rock.png") → OK, CStr() == "/textures/rock.png"
Parse("//textures///rock.png") → OK, CStr() == "/textures/rock.png"
Parse("/textures/rock.png/") → OK, CStr() == "/textures/rock.png"
Parse("../../escape") → Fail(InvalidPath)
Parse("") → Fail(InvalidPath)
FromNative("C:\Assets\rock") → OK, CStr() == "/Assets/rock" [Windows only]
VFSPath::Root() → CStr() == "/"
path.Filename() → "rock.png"
path.Stem() → "rock"
path.Extension() → ".png"
path.Parent() → "/textures"
path.ComponentCount() → 2
path / "sub" → "/textures/rock.png/sub" [asserts on failure]
"/tex".IsPrefixOf("/textures") → false (not a component boundary)
"/textures".IsPrefixOf("/textures/rock.png") → true
Two VFSPaths from same input have equal Hash()
Two VFSPaths from different inputs have different Hash()