Skip to content
vfstool

Library Morrowind active

vfstool_lib

OpenMW's virtual file system as a Jess package in Luau: provider stacks, conflict and provenance reports, locks and materialization.

"org.dreamweave.vfs" = "2.0.0"
Stable
2.0.0 2026-10-21
License
MIT OR Apache-2.0

Overview

vfstool_lib is the virtual file system vfstool runs on, as a crate. Give it data directories in load order, and archives if you have them, and it builds OpenMW's VFS: one normalized key per file, later directories over earlier ones, loose files over archives. It keeps every provider of every key, not only the winner, so it can say why a file resolves the way it does, what a mod overrides, and what would change if the load order did.

local vfs = require '@vfs'

-- Two data directories, the second winning: OpenMW's order.
local stack = vfs.VFS.fromDirectories { base, patch }

assert(stack:getFile [[Textures\TX_WOOD_01.DDS]])        -- any spelling of the path
local report = stack:explain 'textures/tx_wood_01.dds'
print(report.key, #report.overridden)                    -- the winner, and every copy it hides
  • Lookups by any spelling of a path, prefix and substring searches, and trees for display or

serialization.

  • Provider reports: the chain behind one key, every duplicate, what each archive and data

directory contributes.

  • Load-order analysis: conflicts, fully shadowed sources, diffs between sources, and whether

two versions of an INI, TOML or JSON file differ in what they say or only in how.

  • Locks and drift: a deterministic manifest of every winner, with BLAKE3 hashes, and a report

of what changed since.

  • Materialization: the VFS as one folder, with hardlinks, symbolic links, copies or extracted

archives, and a plan of what that would do.

BSA and BA2 archives are read through dream_archive, the package org.dreamweave.dream-archive.

Documentation§

The package's reference is vfs/src/init.jsl and the modules beside it: every function is documented where it is written, and jess luau-types prints the module's types. The guide that explained the Rust crate lives with it on the rust branch; the Luau package's own guide is being written. vfstool is the command line over this library.

  1. 2.0.0 stable 2026-10-21

    vfstool_lib is the Jess package org.dreamweave.vfs, written in Luau; the Rust crate continues on the rust branch at 1.0.0.

    Breaking changes

    • The library is the Jess package org.dreamweave.vfs, module @vfs, installed with jess rather than Cargo. The Rust crate vfstool_lib stays at 1.0.0 on crates.io and on this repository's rust branch, and is no longer developed on main.
    org.dreamweave.vfs-2.0.0.jpk 119.0 kB · jpk
    Verify · sha256 66df02c8b706…a20018401
    Release
    2.0.0 (stable)
    Size
    121894 bytes
    SHA-256
    66df02c8b706cc1a2579bb956521d4cfca970d4b9216a23028f51a6a20018401
    Commit
    7050b6f37bd7
    Package
    org.dreamweave.vfs
    Content digest
    b501db857f2b619c32963763173330348ea57602f1e53303a487fa6025994fab
    Sources
    1

    Check it yourself: sha256sum org.dreamweave.vfs-2.0.0.jpk must print the digest above.

  2. 1.0.0 stable 2026-09-29

    First stable release: the Luau binding is an l3i extension providing @dream/vfs, keys stay bytes end to end, and the top-level API is under semver.

    Breaking changes

    • The lua feature makes the crate an l3i extension, lua::VfsExtension, providing the module @dream/vfs. lua::open, lua::register and mlua are gone.
    • In Luau: key lists are sequence views and trees are streams (:toTable() gives the old tables), indices are 1-based, sizes are integers, unknown option keys are errors, and paths are bytes.
    • Building with lua needs l3i's toolchain: clang, lld and cross-language thin LTO.
    • dream-path 1.0.
    • The library writes nothing to standard output or error. collapse_into and dump_to_directory return a MaterializationReport, the files written and each winner left out as a MaterializationSkip, instead of () and a count, and DirectoryDiff gains unreadable, what diff_directory's walk could not read. In Luau, collapseInto returns the skipped files and dumpToDirectory returns them after the count.

    Added

    • VFS::len and VFS::is_empty, in constant time, and get_file_normalized, contains_normalized and paths_matching_bytes for keys already normalized.
    • VfsKeyInput::vfs_key_bytes, and VfsKeyInput for [u8]; VfsFile::path_bytes.
    • In Luau: file:readInto(buffer, offset?), tree:count(), snapshot:len(), and a type for every function, method and view element in the plan's definitions.
    • File I/O in Luau: file:size(), positional file:readAt(buffer, fileOffset, length?, bufferOffset?) through a memory map, file:readRange, file:readAllBuffer, and file:open() giving a Reader with read, readInto, seek, skip, tell and close.
    • The lua-write feature: a write root on the VFS (writeRoot option, setWriteRoot), writeFile, openWrite giving a Writer, mkdir, remove and rename, behind the filesystem.write runtime capability; the root is kept canonical and refuses keys that pass through a symbolic link beneath it.
    • The lua-host feature: a host table of host-path functions (readFile, readAt, writeFile, open, openWrite, stat, exists, list, mkdir, remove, rename, copy, canonicalize), behind the filesystem.host runtime capability.
    • lua::VfsRequireNavigator, l3i's require over a VFS with .luaurc aliases and relative paths, and lua::TemplateCache, OpenMW's script template cache: one compiled template per module per VM, cloned per instance, invalidated explicitly. The jit feature forwards l3i's and reports each template's native code status.
    • Zip archives: .jpk files are read as zip archives, and Zstandard entries are listed and decoded through a pure-Rust decoder.

    Changed

    • Looking up a key that is already normalized borrows it instead of allocating, and searches compare bytes.
    • The sorted key list is built once per change to the VFS and shared by every keys() call.
    • lua::Vfs is shared and Clone, and lua::VfsFileHandle is built with VfsFileHandle::new. dream_archive 1.0.0 and l3i 1.0.0 come from crates.io; Rust 1.92.

    Fixed

    • On Unix, a key that is not UTF-8 keeps its exact bytes in provider records, lock manifests and semantic conflicts, instead of a lossy spelling that looks up nothing.
    • collapse_into and dump_to_directory into one of the VFS's own data directories leave a loose file that already is its destination where it is. They deleted it before linking or copying it.
    • extract_file into the folder a loose file already lives in leaves the file there. It deleted the file, then failed to copy it.
    • run_finalize and run_finalize_tracked leave a destination that already is the changed file, rewritten in place through a hard link from output_dir, as it is. They copied it onto itself, which truncated it to nothing.
    • VfsFile::file_name and file_stem end an archive entry's folders at \ as well as / on every platform: a Morrowind BSA's meshes\x\ex_door.nif is ex_door.nif on Linux and macOS too, in trees, Display and Luau's file:fileName().
    • ConflictIndex::diff_report compares the keys the index holds for each source, found by exact spelling or with case and separators folded, so archives, indexes built from file lists and sources spelled in another case compare correctly; it read both paths from disk as typed. A path the index does not have is still read from disk.
    • conflicts_report and shadowed_report without use_relative join a loose source's path with each file's own spelling, instead of the lowercase key, so every path names a file that exists. ConflictIndex::from_directories records those spellings.
    • simulate, simulate_with_opts, simulate_impact and plan_candidate_directory find each key's current winner by its place in the provider stack. They compared the VFS's number for the source with the index's, which differ after a source that provides nothing, and counted unchanged keys as changed and left current_winner_source empty.
    • The layer index of a VFS keeps a source that never provided a key, such as an empty data directory, so its positions are the source_index of the VFS's provider records and ArchiveInfo; it dropped such sources and numbered later ones lower. A source whose providers were all removed still leaves it.
    • A simulated reorder never lifts an archive above a loose file: simulate, simulate_with_opts and simulate_impact ranked every source by the new order alone, so moving an archive after a data directory reported the archive winning.
    • set_winner_file and set_winner_loose_file record a new key's directories, so set_winner_file("a/b.txt", …) followed by set_winner_file("a", …) refuses the second instead of keeping a file and a directory named a, which no VFS can write out.
    • remove_resolved_file, remove_resolved_prefix and remove_resolved_matching_glob release the removed keys' directories: after remove_resolved_file("a/b.txt"), push_provider("a", …) returned false.
    • push_provider_batch checks each entry against the ones before it, so push_directory and push_archive no longer add a file and a directory with one name; push_directory keeps the same one from_directories keeps.
    • Display for VFS and display_filtered list a file whose path has no file name, such as VfsFile::from("/"), by its whole path instead of panicking, and serialized trees list it too instead of leaving it out.
    • Display for VFS, display_filtered and Luau's vfs:display() nest each directory under its parent, one │ per level, and list directories that hold only directories. Every directory was printed by its last name at one level, so meshes/x and textures/x both showed as x/, and an archive's folders looked like loose ones.
    • Luau's serialize writes Entries and Providers views, tree streams and VfsFile handles, each file as its file row's fields; it refused every userdata but Keys and ProviderRecords views, although the documentation said views serialize.
    • collapse_into with use_symlinks links to each file's absolute path. A VFS built from relative directories made links that resolved from the output folder instead, and dangled.
    • In Luau, an unknown source kind in VfsProvider.new reads VfsProvider.new.kind: unknown source kind 'zip' (...), without a second dream.vfs: inside it.
    • lua::KeyBlob is exported, so a host can fill the lua::Keys(pub KeyBlob) view it could only build empty.
    • Every PathBuf in a report serializes as a string, with U+FFFD in place of bytes that are not UTF-8, instead of failing with path contains invalid UTF-8 characters; diff_against_lock compares keys and sources as that text, so a lock read back from a file does not drift.
    vfstool_lib-1.0.0.crate 211.7 kB · crate
    Verify · sha256 db40ecfedfb7…12ecaeff0
    Release
    1.0.0 (stable)
    Size
    216746 bytes
    SHA-256
    db40ecfedfb73886364eeecf08f09124a888426636616e03d73137c12ecaeff0
    Commit
    1a4193604e82
    Sources
    1

    Check it yourself: sha256sum vfstool_lib-1.0.0.crate must print the digest above.

  3. 0.10.2 stable 2026-09-27

    Fixed

    • VfsFile:readAll() returns a Luau string, as documented, instead of a table of bytes.
    vfstool_lib-0.10.2.crate 131.9 kB · crate
    Verify · sha256 fa683d03c638…56b8aa2b2
    Release
    0.10.2 (stable)
    Size
    135093 bytes
    SHA-256
    fa683d03c638112193eccf6bd9aed7218e3fded4ae2fa8025dd600d56b8aa2b2
    Commit
    7ec4c060b6a5
    Sources
    1

    Check it yourself: sha256sum vfstool_lib-0.10.2.crate must print the digest above.

Credits