From 2b6106c41f3959d4deb8efc58c9055de0339959e Mon Sep 17 00:00:00 2001
From: Andrei Karas <akaras@inbox.ru>
Date: Thu, 23 Feb 2017 01:06:11 +0300
Subject: Impliment basic VirtFsDir for virtual fs based on directories.

Api similar to VirtFs.
VirtFsDir unused for now.
---
 src/fs/files.cpp             |  44 ++-
 src/fs/files.h               |   4 +
 src/fs/virtdirentry.cpp      |  34 +++
 src/fs/virtdirentry.h        |  41 +++
 src/fs/virtfileprivate.cpp   |  16 +-
 src/fs/virtfileprivate.h     |   3 +
 src/fs/virtfs_unittest.cc    |  27 +-
 src/fs/virtfsdir.cpp         | 564 +++++++++++++++++++++++++++++++++++
 src/fs/virtfsdir.h           |  79 +++++
 src/fs/virtfsdir_unittest.cc | 685 +++++++++++++++++++++++++++++++++++++++++++
 src/fs/virtfstools.cpp       |  17 ++
 src/fs/virtfstools.h         |   7 +
 12 files changed, 1509 insertions(+), 12 deletions(-)
 create mode 100644 src/fs/virtdirentry.cpp
 create mode 100644 src/fs/virtdirentry.h
 create mode 100644 src/fs/virtfsdir.cpp
 create mode 100644 src/fs/virtfsdir.h
 create mode 100644 src/fs/virtfsdir_unittest.cc

(limited to 'src/fs')

diff --git a/src/fs/files.cpp b/src/fs/files.cpp
index 516283fe6..abb0ee956 100644
--- a/src/fs/files.cpp
+++ b/src/fs/files.cpp
@@ -30,7 +30,10 @@
 #include "fs/virtlist.h"
 #endif  // defined(ANDROID) || defined(__native_client__)
 
+#include "utils/stringutils.h"
+
 #include <dirent.h>
+#include <sys/stat.h>
 
 #include "debug.h"
 
@@ -206,13 +209,8 @@ int Files::copyFile(const std::string &restrict srcName,
 
 bool Files::existsLocal(const std::string &path)
 {
-    bool flg(false);
-    std::fstream file;
-    file.open(path.c_str(), std::ios::in);
-    if (file.is_open())
-        flg = true;
-    file.close();
-    return flg;
+    struct stat statbuf;
+    return stat(path.c_str(), &statbuf) == 0;
 }
 
 bool Files::loadTextFileLocal(const std::string &fileName,
@@ -266,3 +264,35 @@ void Files::deleteFilesInDirectory(std::string path)
         closedir(dir);
     }
 }
+
+void Files::enumFiles(StringVect &files,
+                      std::string path,
+                      const bool skipSymlinks)
+{
+    if (findLast(path, "/") == false)
+        path += "/";
+    const struct dirent *next_file = nullptr;
+    DIR *const dir = opendir(path.c_str());
+
+    if (dir)
+    {
+        while ((next_file = readdir(dir)))
+        {
+            const std::string file = next_file->d_name;
+            if (file == "." || file == "..")
+                continue;
+            if (skipSymlinks == true)
+            {
+                struct stat statbuf;
+                if (lstat(path.c_str(), &statbuf) == 0 &&
+                    S_ISLNK(statbuf.st_mode) != 0)
+                {
+                    continue;
+                }
+            }
+            files.push_back(file);
+        }
+        closedir(dir);
+    }
+}
+
diff --git a/src/fs/files.h b/src/fs/files.h
index b91f7e9ab..65b2f6325 100644
--- a/src/fs/files.h
+++ b/src/fs/files.h
@@ -63,6 +63,10 @@ namespace Files
                       const std::string &restrict text);
 
     void deleteFilesInDirectory(std::string path);
+
+    void enumFiles(StringVect &files,
+                   std::string path,
+                   const bool skipSymlinks);
 }  // namespace Files
 
 #endif  // UTILS_FILES_H
diff --git a/src/fs/virtdirentry.cpp b/src/fs/virtdirentry.cpp
new file mode 100644
index 000000000..693aba6a3
--- /dev/null
+++ b/src/fs/virtdirentry.cpp
@@ -0,0 +1,34 @@
+/*
+ *  The ManaPlus Client
+ *  Copyright (C) 2017  The ManaPlus Developers
+ *
+ *  This file is part of The ManaPlus Client.
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "fs/virtdirentry.h"
+
+#include "debug.h"
+
+VirtDirEntry::VirtDirEntry(const std::string &userDir,
+                           const std::string &rootDir) :
+    mUserDir(userDir),
+    mRootDir(rootDir)
+{
+}
+
+VirtDirEntry::~VirtDirEntry()
+{
+}
diff --git a/src/fs/virtdirentry.h b/src/fs/virtdirentry.h
new file mode 100644
index 000000000..b3d3faff6
--- /dev/null
+++ b/src/fs/virtdirentry.h
@@ -0,0 +1,41 @@
+/*
+ *  The ManaPlus Client
+ *  Copyright (C) 2017  The ManaPlus Developers
+ *
+ *  This file is part of The ManaPlus Client.
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef UTILS_VIRTDIRENTRY_H
+#define UTILS_VIRTDIRENTRY_H
+
+#include <string>
+
+#include "localconsts.h"
+
+struct VirtDirEntry final
+{
+    VirtDirEntry(const std::string &userDir,
+                 const std::string &rootDir);
+
+    A_DELETE_COPY(VirtDirEntry)
+
+    ~VirtDirEntry();
+
+    std::string mUserDir;
+    std::string mRootDir;
+};
+
+#endif  // UTILS_VIRTDIRENTRY_H
diff --git a/src/fs/virtfileprivate.cpp b/src/fs/virtfileprivate.cpp
index 6bd5d3b9a..1dcc6116c 100644
--- a/src/fs/virtfileprivate.cpp
+++ b/src/fs/virtfileprivate.cpp
@@ -20,15 +20,25 @@
 
 #include "fs/virtfileprivate.h"
 
+#include <unistd.h>
+
 #include "debug.h"
 
 VirtFilePrivate::VirtFilePrivate() :
-    mFile(nullptr)
+    mFile(nullptr),
+    mFd(-1)
+{
+}
+
+VirtFilePrivate::VirtFilePrivate(const int fd) :
+    mFile(nullptr),
+    mFd(fd)
 {
 }
 
 VirtFilePrivate::VirtFilePrivate(PHYSFS_file *const file) :
-    mFile(file)
+    mFile(file),
+    mFd(-1)
 {
 }
 
@@ -39,4 +49,6 @@ VirtFilePrivate::~VirtFilePrivate()
         PHYSFS_close(mFile);
         mFile = nullptr;
     }
+    if (mFd != -1)
+        close(mFd);
 }
diff --git a/src/fs/virtfileprivate.h b/src/fs/virtfileprivate.h
index 5b280de78..62e510142 100644
--- a/src/fs/virtfileprivate.h
+++ b/src/fs/virtfileprivate.h
@@ -34,11 +34,14 @@ struct VirtFilePrivate final
 
     explicit VirtFilePrivate(PHYSFS_file *const file);
 
+    explicit VirtFilePrivate(const int fd);
+
     A_DELETE_COPY(VirtFilePrivate)
 
     ~VirtFilePrivate();
 
     PHYSFS_file *mFile;
+    int mFd;
 };
 
 #endif  // UTILS_VIRTFILEPRIVATE_H
diff --git a/src/fs/virtfs_unittest.cc b/src/fs/virtfs_unittest.cc
index 4f3a74a7b..b26381f08 100644
--- a/src/fs/virtfs_unittest.cc
+++ b/src/fs/virtfs_unittest.cc
@@ -106,7 +106,7 @@ static void removeTemp(StringVect &restrict list)
     }
 }
 
-TEST_CASE("VirtFs enumerateFiles")
+TEST_CASE("VirtFs enumerateFiles1")
 {
     logger = new Logger;
 
@@ -115,8 +115,8 @@ TEST_CASE("VirtFs enumerateFiles")
 
     VirtList *list = nullptr;
 
-    const int cnt1 = VirtFs::exists("test/test2.txt") ? 23 : 22;
-    const int cnt2 = 23;
+    const int cnt1 = VirtFs::exists("test/test2.txt") ? 24 : 23;
+    const int cnt2 = 24;
 
     VirtFs::permitLinks(false);
     list = VirtFs::enumerateFiles("test");
@@ -142,6 +142,27 @@ TEST_CASE("VirtFs enumerateFiles")
     delete2(logger);
 }
 
+TEST_CASE("VirtFs enumerateFiles2")
+{
+    logger = new Logger;
+
+    VirtFs::addDirToSearchPath("data/test/dir1",
+        Append_false);
+    VirtFs::addDirToSearchPath("../data/test/dir1",
+        Append_false);
+
+    VirtList *list = nullptr;
+
+    list = VirtFs::enumerateFiles("/");
+    const size_t sz = list->names.size();
+    REQUIRE(list->names.size() == 5);
+    VirtFs::freeList(list);
+
+    VirtFs::removeDirFromSearchPath("data/test/dir1");
+    VirtFs::removeDirFromSearchPath("../data/test/dir1");
+    delete2(logger);
+}
+
 TEST_CASE("VirtFs isDirectory")
 {
     logger = new Logger();
diff --git a/src/fs/virtfsdir.cpp b/src/fs/virtfsdir.cpp
new file mode 100644
index 000000000..4ca4acafa
--- /dev/null
+++ b/src/fs/virtfsdir.cpp
@@ -0,0 +1,564 @@
+/*
+ *  The ManaPlus Client
+ *  Copyright (C) 2013-2017  The ManaPlus Developers
+ *
+ *  This file is part of The ManaPlus Client.
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "fs/virtfsdir.h"
+
+#include "fs/files.h"
+#include "fs/mkdir.h"
+#include "fs/paths.h"
+#include "fs/virtdirentry.h"
+#include "fs/virtfs.h"
+#include "fs/virtfile.h"
+#include "fs/virtfileprivate.h"
+#include "fs/virtlist.h"
+
+#include "utils/checkutils.h"
+#include "utils/stringutils.h"
+
+#include <dirent.h>
+#include <fcntl.h>
+#include <iostream>
+#include <unistd.h>
+
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#ifdef ANDROID
+#include "fs/paths.h"
+#endif  // ANDROID
+
+#include "debug.h"
+
+extern const char *dirSeparator;
+
+namespace
+{
+    std::vector<VirtDirEntry*> mEntries;
+    std::string mWriteDir;
+    bool mPermitLinks = false;
+}  // namespace
+
+namespace VirtFsDir
+{
+    namespace
+    {
+        static VirtFile *openFile(const std::string &restrict filename,
+                                  const int mode)
+        {
+            if (checkPath(filename) == false)
+            {
+                reportAlways("VirtFsDir::openFile invalid path: %s",
+                    filename.c_str());
+                return nullptr;
+            }
+            VirtDirEntry *const entry = searchEntryByPath(filename);
+            if (entry == nullptr)
+                return nullptr;
+
+            const std::string path = entry->mRootDir + filename;
+            const int fd = open(path.c_str(),
+                mode,
+                S_IRUSR | S_IWUSR);
+            if (fd == -1)
+            {
+                reportAlways("VirtFsDir::openFile file open error: %s",
+                    filename.c_str());
+                return nullptr;
+            }
+            VirtFile *restrict const file = new VirtFile;
+            file->mPrivate = new VirtFilePrivate(fd);
+
+            return file;
+        }
+    }  // namespace
+
+    VirtDirEntry *searchEntryByRoot(const std::string &restrict root)
+    {
+        FOR_EACH (std::vector<VirtDirEntry*>::const_iterator, it, mEntries)
+        {
+            if ((*it)->mRootDir == root)
+                return *it;
+        }
+        return nullptr;
+    }
+
+    VirtDirEntry *searchEntryByPath(const std::string &restrict path)
+    {
+        FOR_EACH (std::vector<VirtDirEntry*>::const_iterator, it, mEntries)
+        {
+            VirtDirEntry *const entry = *it;
+            if (Files::existsLocal(entry->mRootDir + path))
+                return entry;
+        }
+        return nullptr;
+    }
+
+    bool addToSearchPathSilent(const std::string &newDir,
+                               const Append append,
+                               const SkipError skipError)
+    {
+        if (skipError == SkipError_false &&
+            Files::existsLocal(newDir) == false)
+        {
+            logger->log("VirtFsDir::addToSearchPath directory not exists: %s",
+                newDir.c_str());
+            return false;
+        }
+        if (newDir.find(".zip") != std::string::npos)
+        {
+            reportAlways("Called VirtFsDir::addToSearchPath with zip archive");
+            return false;
+        }
+        std::string rootDir = newDir;
+        if (findLast(rootDir, std::string(dirSeparator)) == false)
+            rootDir += dirSeparator;
+        VirtDirEntry *const entry = VirtFsDir::searchEntryByRoot(rootDir);
+        if (entry != nullptr)
+        {
+            reportAlways("VirtFsDir::addToSearchPath already exists: %s",
+                newDir.c_str());
+            return false;
+        }
+        logger->log("Add virtual directory: " + newDir);
+        if (append == Append_true)
+        {
+            mEntries.push_back(new VirtDirEntry(newDir,
+                rootDir));
+        }
+        else
+        {
+            mEntries.insert(mEntries.begin(),
+                new VirtDirEntry(newDir,
+                rootDir));
+        }
+        return true;
+    }
+
+    bool addToSearchPath(const std::string &newDir,
+                         const Append append)
+    {
+        if (Files::existsLocal(newDir) == false)
+        {
+            reportAlways("VirtFsDir::addToSearchPath directory not exists: %s",
+                newDir.c_str());
+            return false;
+        }
+        if (newDir.find(".zip") != std::string::npos)
+        {
+            reportAlways("Called VirtFsDir::addToSearchPath with zip archive");
+            return false;
+        }
+        std::string rootDir = newDir;
+        if (findLast(rootDir, std::string(dirSeparator)) == false)
+            rootDir += dirSeparator;
+        VirtDirEntry *const entry = VirtFsDir::searchEntryByRoot(rootDir);
+        if (entry != nullptr)
+        {
+            reportAlways("VirtFsDir::addToSearchPath already exists: %s",
+                newDir.c_str());
+            return false;
+        }
+        logger->log("Add virtual directory: " + newDir);
+        if (append == Append_true)
+        {
+            mEntries.push_back(new VirtDirEntry(newDir,
+                rootDir));
+        }
+        else
+        {
+            mEntries.insert(mEntries.begin(),
+                new VirtDirEntry(newDir,
+                rootDir));
+        }
+        return true;
+    }
+
+    bool removeFromSearchPathSilent(std::string oldDir)
+    {
+        if (oldDir.find(".zip") != std::string::npos)
+        {
+            reportAlways("Called removeFromSearchPath with zip archive");
+            return false;
+        }
+        if (findLast(oldDir, std::string(dirSeparator)) == false)
+            oldDir += dirSeparator;
+        FOR_EACH (std::vector<VirtDirEntry*>::iterator, it, mEntries)
+        {
+            VirtDirEntry *const entry = *it;
+            if (entry->mRootDir == oldDir)
+            {
+                logger->log("Remove virtual directory: " + oldDir);
+                mEntries.erase(it);
+                delete entry;
+                return true;
+            }
+        }
+
+        logger->log("VirtFsDir::removeFromSearchPath not exists: %s",
+            oldDir.c_str());
+        return false;
+    }
+
+    bool removeFromSearchPath(std::string oldDir)
+    {
+        if (oldDir.find(".zip") != std::string::npos)
+        {
+            reportAlways("Called removeFromSearchPath with zip archive");
+            return false;
+        }
+        if (findLast(oldDir, std::string(dirSeparator)) == false)
+            oldDir += dirSeparator;
+        FOR_EACH (std::vector<VirtDirEntry*>::iterator, it, mEntries)
+        {
+            VirtDirEntry *const entry = *it;
+            if (entry->mRootDir == oldDir)
+            {
+                logger->log("Remove virtual directory: " + oldDir);
+                mEntries.erase(it);
+                delete entry;
+                return true;
+            }
+        }
+
+        reportAlways("VirtFsDir::removeFromSearchPath not exists: %s",
+            oldDir.c_str());
+        return false;
+    }
+
+    std::vector<VirtDirEntry*> &getEntries()
+    {
+        return mEntries;
+    }
+
+    void deinit()
+    {
+        mEntries.clear();
+    }
+
+    std::string getRealDir(const std::string &restrict filename)
+    {
+        if (checkPath(filename) == false)
+        {
+            reportAlways("VirtFsDir::exists invalid path: %s",
+                filename.c_str());
+            return std::string();
+        }
+        FOR_EACH (std::vector<VirtDirEntry*>::iterator, it, mEntries)
+        {
+            VirtDirEntry *const entry = *it;
+            const std::string path = entry->mRootDir + filename;
+            if (Files::existsLocal(path))
+                return entry->mUserDir;
+        }
+        return std::string();
+    }
+
+    bool exists(const std::string &restrict name)
+    {
+        if (checkPath(name) == false)
+        {
+            reportAlways("VirtFsDir::exists invalid path: %s",
+                name.c_str());
+            return false;
+        }
+        FOR_EACH (std::vector<VirtDirEntry*>::iterator, it, mEntries)
+        {
+            VirtDirEntry *const entry = *it;
+            if (Files::existsLocal(entry->mRootDir + name))
+                return true;
+
+        }
+        return false;
+    }
+
+    VirtList *enumerateFiles(const std::string &restrict dirName)
+    {
+        VirtList *const list = new VirtList;
+        if (checkPath(dirName) == false)
+        {
+            reportAlways("VirtFsDir::enumerateFiles invalid path: %s",
+                dirName.c_str());
+            return list;
+        }
+        StringVect &names = list->names;
+        FOR_EACH (std::vector<VirtDirEntry*>::iterator, it, mEntries)
+        {
+            VirtDirEntry *const entry = *it;
+            StringVect files;
+            std::string path = entry->mRootDir + dirName;
+            if (findLast(path, std::string(dirSeparator)) == false)
+                path += dirSeparator;
+            const struct dirent *next_file = nullptr;
+            DIR *const dir = opendir(path.c_str());
+            if (dir)
+            {
+                while ((next_file = readdir(dir)))
+                {
+                    const std::string file = next_file->d_name;
+                    if (file == "." || file == "..")
+                        continue;
+                    if (mPermitLinks == false)
+                    {
+                        struct stat statbuf;
+                        if (lstat(path.c_str(), &statbuf) == 0 &&
+                            S_ISLNK(statbuf.st_mode) != 0)
+                        {
+                            continue;
+                        }
+                    }
+                    bool found(false);
+                    FOR_EACH (StringVectCIter, itn, names)
+                    {
+                        if (*itn == file)
+                        {
+                            found = true;
+                            break;
+                        }
+                    }
+                    if (found == false)
+                        names.push_back(file);
+                }
+                closedir(dir);
+            }
+        }
+        return list;
+    }
+
+    bool isDirectory(const std::string &restrict dirName)
+    {
+        if (checkPath(dirName) == false)
+        {
+            reportAlways("VirtFsDir::isDirectory invalid path: %s",
+                dirName.c_str());
+            return false;
+        }
+        FOR_EACH (std::vector<VirtDirEntry*>::iterator, it, mEntries)
+        {
+            VirtDirEntry *const entry = *it;
+            std::string path = entry->mRootDir + dirName;
+            if (findLast(path, std::string(dirSeparator)) == false)
+                path += dirSeparator;
+
+            struct stat statbuf;
+            if (stat(path.c_str(), &statbuf) == 0 &&
+                S_ISDIR(statbuf.st_mode) != 0)
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    bool isSymbolicLink(const std::string &restrict name)
+    {
+        if (checkPath(name) == false)
+        {
+            reportAlways("VirtFsDir::isSymbolicLink invalid path: %s",
+                name.c_str());
+            return false;
+        }
+        if (mPermitLinks == false)
+            return false;
+
+        struct stat statbuf;
+        return lstat(name.c_str(), &statbuf) == 0 &&
+            S_ISLNK(statbuf.st_mode) != 0;
+    }
+
+    void freeList(VirtList *restrict const handle)
+    {
+        delete handle;
+    }
+
+    VirtFile *openRead(const std::string &restrict filename)
+    {
+        return openFile(filename, O_RDONLY);
+    }
+
+    VirtFile *openWrite(const std::string &restrict filename)
+    {
+        return openFile(filename, O_WRONLY | O_CREAT | O_TRUNC);
+    }
+
+    VirtFile *openAppend(const std::string &restrict filename)
+    {
+        return openFile(filename, O_WRONLY | O_CREAT | O_APPEND);
+    }
+
+    bool setWriteDir(const std::string &restrict newDir)
+    {
+        mWriteDir = newDir;
+        if (findLast(mWriteDir, std::string(dirSeparator)) == false)
+            mWriteDir += dirSeparator;
+        return true;
+    }
+
+    bool mkdir(const std::string &restrict dirname)
+    {
+        if (mWriteDir.empty())
+        {
+            reportAlways("VirtFsDir::mkdir write dir is empty");
+            return false;
+        }
+        return mkdir_r((mWriteDir + dirname).c_str()) != -1;
+    }
+
+    bool remove(const std::string &restrict filename)
+    {
+        if (mWriteDir.empty())
+        {
+            reportAlways("VirtFsDir::remove write dir is empty");
+            return false;
+        }
+        return ::remove((mWriteDir + filename).c_str()) != 0;
+    }
+
+    void permitLinks(const bool val)
+    {
+        mPermitLinks = val;
+    }
+
+    const char *getLastError()
+    {
+        return nullptr;
+    }
+
+    int close(VirtFile *restrict const file)
+    {
+        if (file == nullptr)
+            return 0;
+        delete file;
+        return 1;
+    }
+
+    int64_t read(VirtFile *restrict const file,
+                 void *restrict const buffer,
+                 const uint32_t objSize,
+                 const uint32_t objCount)
+    {
+        if (file == nullptr)
+            return 0;
+        const int fd = file->mPrivate->mFd;
+        if (fd == -1)
+        {
+            reportAlways("VirtFsDir::read file not opened.");
+            return 0;
+        }
+        int max = objSize * objCount;
+        int cnt = ::read(fd, buffer, max);
+        if (cnt <= 0)
+            return cnt;
+        return cnt / objSize;
+    }
+
+    int64_t write(VirtFile *restrict const file,
+                  const void *restrict const buffer,
+                  const uint32_t objSize,
+                  const uint32_t objCount)
+    {
+        if (file == nullptr)
+            return 0;
+        const int fd = file->mPrivate->mFd;
+        if (fd == -1)
+        {
+            reportAlways("VirtFsDir::write file not opened.");
+            return 0;
+        }
+        int max = objSize * objCount;
+        int cnt = ::write(fd, buffer, max);
+        if (cnt <= 0)
+            return cnt;
+        return cnt / objSize;
+    }
+
+    int64_t fileLength(VirtFile *restrict const file)
+    {
+        if (file == nullptr)
+            return -1;
+        const int fd = file->mPrivate->mFd;
+        if (fd == -1)
+        {
+            reportAlways("VirtFsDir::fileLength file not opened.");
+            return 0;
+        }
+        struct stat statbuf;
+        if (fstat(fd, &statbuf) == -1)
+        {
+            reportAlways("VirtFsDir::fileLength error.");
+            return -1;
+        }
+        return (int64_t)statbuf.st_size;
+    }
+
+    int64_t tell(VirtFile *restrict const file)
+    {
+        if (file == nullptr)
+            return -1;
+
+        const int fd = file->mPrivate->mFd;
+        if (fd == -1)
+        {
+            reportAlways("VirtFsDir::tell file not opened.");
+            return 0;
+        }
+        const int64_t pos = lseek(fd, 0, SEEK_CUR);
+        return pos;
+    }
+
+    int seek(VirtFile *restrict const file,
+             const uint64_t pos)
+    {
+        if (file == nullptr)
+            return -1;
+
+        const int fd = file->mPrivate->mFd;
+        if (fd == -1)
+        {
+            reportAlways("VirtFsDir::seek file not opened.");
+            return 0;
+        }
+        const int64_t res = lseek(fd, pos, SEEK_SET);
+        if (res == -1)
+            return 0;
+        return 1;
+    }
+
+    int eof(VirtFile *restrict const file)
+    {
+        if (file == nullptr)
+            return -1;
+
+        const int fd = file->mPrivate->mFd;
+        if (fd == -1)
+        {
+            reportAlways("VirtFsDir::eof file not opened.");
+            return 0;
+        }
+        const int64_t pos = lseek(fd, 0, SEEK_CUR);
+        struct stat statbuf;
+        if (fstat(fd, &statbuf) == -1)
+        {
+            reportAlways("VirtFsDir::fileLength error.");
+            return -1;
+        }
+        const int64_t len = (int64_t)statbuf.st_size;
+        return pos < 0 || len < 0 || pos >= len;
+    }
+}  // namespace VirtFs
diff --git a/src/fs/virtfsdir.h b/src/fs/virtfsdir.h
new file mode 100644
index 000000000..4fa1c20a5
--- /dev/null
+++ b/src/fs/virtfsdir.h
@@ -0,0 +1,79 @@
+/*
+ *  The ManaPlus Client
+ *  Copyright (C) 2013-2017  The ManaPlus Developers
+ *
+ *  This file is part of The ManaPlus Client.
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef UTILS_VIRTFSDIR_H
+#define UTILS_VIRTFSDIR_H
+
+#include "enums/simpletypes/append.h"
+#include "enums/simpletypes/skiperror.h"
+
+#include "localconsts.h"
+
+#include <vector>
+#include <string>
+
+struct VirtDirEntry;
+struct VirtFile;
+struct VirtList;
+
+namespace VirtFsDir
+{
+    VirtDirEntry *searchEntryByRoot(const std::string &restrict root);
+    VirtDirEntry *searchEntryByPath(const std::string &restrict path);
+    bool addToSearchPath(const std::string &newDir,
+                         const Append append);
+    bool addToSearchPathSilent(const std::string &newDir,
+                               const Append append,
+                               const SkipError skipError);
+    bool removeFromSearchPath(std::string oldDir);
+    bool removeFromSearchPathSilent(std::string oldDir);
+    void deinit();
+    std::vector<VirtDirEntry*> &getEntries();
+    bool exists(const std::string &restrict name);
+    VirtList *enumerateFiles(const std::string &restrict dir) RETURNS_NONNULL;
+    bool isDirectory(const std::string &restrict dirName);
+    bool isSymbolicLink(const std::string &restrict name);
+    void freeList(VirtList *restrict const handle);
+    VirtFile *openRead(const std::string &restrict filename);
+    VirtFile *openWrite(const std::string &restrict filename);
+    VirtFile *openAppend(const std::string &restrict filename);
+    bool setWriteDir(const std::string &restrict newDir);
+    std::string getRealDir(const std::string &restrict filename);
+    bool mkdir(const std::string &restrict dirName);
+    bool remove(const std::string &restrict filename);
+    void permitLinks(const bool val);
+    const char *getLastError();
+    int64_t read(VirtFile *restrict const handle,
+                 void *restrict const buffer,
+                 const uint32_t objSize,
+                 const uint32_t objCount);
+    int64_t write(VirtFile *restrict const file,
+                  const void *restrict const buffer,
+                  const uint32_t objSize,
+                  const uint32_t objCount);
+    int close(VirtFile *restrict const file);
+    int64_t fileLength(VirtFile *restrict const file);
+    int64_t tell(VirtFile *restrict const file);
+    int seek(VirtFile *restrict const file,
+             const uint64_t pos);
+    int eof(VirtFile *restrict const file);
+}  // namespace VirtFsDir
+
+#endif  // UTILS_VIRTFSDIR_H
diff --git a/src/fs/virtfsdir_unittest.cc b/src/fs/virtfsdir_unittest.cc
new file mode 100644
index 000000000..742c1aa72
--- /dev/null
+++ b/src/fs/virtfsdir_unittest.cc
@@ -0,0 +1,685 @@
+/*
+ *  The ManaPlus Client
+ *  Copyright (C) 2016-2017  The ManaPlus Developers
+ *
+ *  This file is part of The ManaPlus Client.
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "catch.hpp"
+
+#include "fs/virtdirentry.h"
+#include "fs/virtfs.h"
+#include "fs/virtfsdir.h"
+#include "fs/virtfstools.h"
+#include "fs/virtlist.h"
+
+#include "utils/checkutils.h"
+#include "utils/delete2.h"
+
+#include "debug.h"
+
+TEST_CASE("VirtFsDir getEntries")
+{
+    REQUIRE(VirtFsDir::getEntries().empty());
+    REQUIRE(VirtFsDir::searchEntryByRoot("test") == nullptr);
+}
+
+TEST_CASE("VirtFsDir addToSearchPath")
+{
+    logger = new Logger();
+    SECTION("simple 1")
+    {
+        REQUIRE(VirtFsDir::addToSearchPathSilent("dir1",
+            Append_false,
+            SkipError_true));
+        REQUIRE(VirtFsDir::searchEntryByRoot("dir1/") != nullptr);
+        REQUIRE(VirtFsDir::searchEntryByRoot("test/") == nullptr);
+        REQUIRE(VirtFsDir::getEntries().size() == 1);
+        REQUIRE(VirtFsDir::getEntries()[0]->mRootDir == "dir1/");
+        REQUIRE(VirtFsDir::getEntries()[0]->mUserDir == "dir1");
+    }
+
+    SECTION("simple 2")
+    {
+        REQUIRE(VirtFsDir::addToSearchPathSilent("dir1/",
+            Append_true,
+            SkipError_true));
+        REQUIRE(VirtFsDir::searchEntryByRoot("dir1/") != nullptr);
+        REQUIRE(VirtFsDir::searchEntryByRoot("test/") == nullptr);
+        REQUIRE(VirtFsDir::getEntries().size() == 1);
+        REQUIRE(VirtFsDir::getEntries()[0]->mRootDir == "dir1/");
+        REQUIRE(VirtFsDir::getEntries()[0]->mUserDir == "dir1/");
+    }
+
+    SECTION("simple 3")
+    {
+        REQUIRE(VirtFsDir::addToSearchPathSilent("dir1",
+            Append_false,
+            SkipError_true));
+        REQUIRE(VirtFsDir::addToSearchPathSilent("dir2",
+            Append_false,
+            SkipError_true));
+        REQUIRE(VirtFsDir::searchEntryByRoot("dir1/") != nullptr);
+        REQUIRE(VirtFsDir::searchEntryByRoot("dir2/") != nullptr);
+        REQUIRE(VirtFsDir::searchEntryByRoot("test/") == nullptr);
+        REQUIRE(VirtFsDir::getEntries().size() == 2);
+        REQUIRE(VirtFsDir::getEntries()[0]->mRootDir == "dir2/");
+        REQUIRE(VirtFsDir::getEntries()[1]->mRootDir == "dir1/");
+        REQUIRE(VirtFsDir::getEntries()[0]->mUserDir == "dir2");
+        REQUIRE(VirtFsDir::getEntries()[1]->mUserDir == "dir1");
+    }
+
+    SECTION("simple 4")
+    {
+        REQUIRE(VirtFsDir::addToSearchPathSilent("dir1",
+            Append_true,
+            SkipError_true));
+        REQUIRE(VirtFsDir::addToSearchPathSilent("dir2",
+            Append_true,
+            SkipError_true));
+        REQUIRE(VirtFsDir::searchEntryByRoot("dir1/") != nullptr);
+        REQUIRE(VirtFsDir::searchEntryByRoot("dir2/") != nullptr);
+        REQUIRE(VirtFsDir::searchEntryByRoot("test/") == nullptr);
+        REQUIRE(VirtFsDir::getEntries().size() == 2);
+        REQUIRE(VirtFsDir::getEntries()[0]->mRootDir == "dir1/");
+        REQUIRE(VirtFsDir::getEntries()[1]->mRootDir == "dir2/");
+        REQUIRE(VirtFsDir::getEntries()[0]->mUserDir == "dir1");
+        REQUIRE(VirtFsDir::getEntries()[1]->mUserDir == "dir2");
+    }
+
+    SECTION("simple 5")
+    {
+        REQUIRE(VirtFsDir::addToSearchPathSilent("dir1",
+            Append_true,
+            SkipError_true));
+        REQUIRE(VirtFsDir::addToSearchPathSilent("dir2",
+            Append_true,
+            SkipError_true));
+        REQUIRE(VirtFsDir::addToSearchPathSilent("dir3/test",
+            Append_true,
+            SkipError_true));
+        REQUIRE(VirtFsDir::searchEntryByRoot("dir1/") != nullptr);
+        REQUIRE(VirtFsDir::searchEntryByRoot("dir2/") != nullptr);
+        REQUIRE(VirtFsDir::searchEntryByRoot("dir3/test/") != nullptr);
+        REQUIRE(VirtFsDir::searchEntryByRoot("test/") == nullptr);
+        REQUIRE(VirtFsDir::getEntries().size() == 3);
+        REQUIRE(VirtFsDir::getEntries()[0]->mRootDir == "dir1/");
+        REQUIRE(VirtFsDir::getEntries()[0]->mUserDir == "dir1");
+        REQUIRE(VirtFsDir::getEntries()[1]->mRootDir == "dir2/");
+        REQUIRE(VirtFsDir::getEntries()[1]->mUserDir == "dir2");
+        REQUIRE(VirtFsDir::getEntries()[2]->mRootDir == "dir3/test/");
+        REQUIRE(VirtFsDir::getEntries()[2]->mUserDir == "dir3/test");
+    }
+
+    SECTION("simple 6")
+    {
+        REQUIRE(VirtFsDir::addToSearchPathSilent("dir1",
+            Append_true,
+            SkipError_true));
+        REQUIRE(VirtFsDir::addToSearchPathSilent("dir2",
+            Append_true,
+            SkipError_true));
+        REQUIRE(VirtFsDir::addToSearchPathSilent("dir3/test",
+            Append_false,
+            SkipError_true));
+        REQUIRE(VirtFsDir::searchEntryByRoot("dir1/") != nullptr);
+        REQUIRE(VirtFsDir::searchEntryByRoot("dir2/") != nullptr);
+        REQUIRE(VirtFsDir::searchEntryByRoot("dir3/test/") != nullptr);
+        REQUIRE(VirtFsDir::searchEntryByRoot("test/") == nullptr);
+        REQUIRE(VirtFsDir::getEntries().size() == 3);
+        REQUIRE(VirtFsDir::getEntries()[0]->mRootDir == "dir3/test/");
+        REQUIRE(VirtFsDir::getEntries()[0]->mUserDir == "dir3/test");
+        REQUIRE(VirtFsDir::getEntries()[1]->mRootDir == "dir1/");
+        REQUIRE(VirtFsDir::getEntries()[1]->mUserDir == "dir1");
+        REQUIRE(VirtFsDir::getEntries()[2]->mRootDir == "dir2/");
+        REQUIRE(VirtFsDir::getEntries()[2]->mUserDir == "dir2");
+    }
+
+    VirtFsDir::deinit();
+    delete2(logger);
+}
+
+TEST_CASE("VirtFsDir removeFromSearchPath")
+{
+    logger = new Logger();
+
+    SECTION("simple 1")
+    {
+        REQUIRE_THROWS(VirtFsDir::removeFromSearchPath("dir1"));
+    }
+
+    SECTION("simple 2")
+    {
+        REQUIRE(VirtFsDir::addToSearchPathSilent("dir1",
+            Append_true,
+            SkipError_true));
+        REQUIRE_THROWS(VirtFsDir::removeFromSearchPath("dir2"));
+        REQUIRE(VirtFsDir::removeFromSearchPath("dir1"));
+    }
+
+    SECTION("simple 3")
+    {
+        REQUIRE(VirtFsDir::addToSearchPathSilent("dir1",
+            Append_true,
+            SkipError_true));
+        REQUIRE(VirtFsDir::addToSearchPathSilent("dir2/dir3",
+            Append_true,
+            SkipError_true));
+        REQUIRE(VirtFsDir::addToSearchPathSilent("dir3",
+            Append_false,
+            SkipError_true));
+        REQUIRE(VirtFsDir::getEntries().size() == 3);
+        REQUIRE_THROWS(VirtFsDir::removeFromSearchPath("dir2"));
+        REQUIRE(VirtFsDir::removeFromSearchPath("dir1"));
+        REQUIRE(VirtFsDir::getEntries().size() == 2);
+        REQUIRE(VirtFsDir::getEntries()[0]->mRootDir == "dir3/");
+        REQUIRE(VirtFsDir::getEntries()[0]->mUserDir == "dir3");
+        REQUIRE(VirtFsDir::getEntries()[1]->mRootDir == "dir2/dir3/");
+        REQUIRE(VirtFsDir::getEntries()[1]->mUserDir == "dir2/dir3");
+        REQUIRE_THROWS(VirtFsDir::removeFromSearchPath("dir1"));
+        REQUIRE(VirtFsDir::getEntries().size() == 2);
+        REQUIRE(VirtFsDir::getEntries()[0]->mRootDir == "dir3/");
+        REQUIRE(VirtFsDir::getEntries()[0]->mUserDir == "dir3");
+        REQUIRE(VirtFsDir::getEntries()[1]->mRootDir == "dir2/dir3/");
+        REQUIRE(VirtFsDir::getEntries()[1]->mUserDir == "dir2/dir3");
+        REQUIRE(VirtFsDir::removeFromSearchPath("dir2/dir3"));
+        REQUIRE_THROWS(VirtFsDir::removeFromSearchPath("dir2/dir3/"));
+        REQUIRE(VirtFsDir::getEntries().size() == 1);
+        REQUIRE(VirtFsDir::getEntries()[0]->mRootDir == "dir3/");
+        REQUIRE(VirtFsDir::getEntries()[0]->mUserDir == "dir3");
+    }
+
+    SECTION("simple 4")
+    {
+        REQUIRE(VirtFsDir::addToSearchPathSilent("dir1",
+            Append_true,
+            SkipError_true));
+        REQUIRE(VirtFsDir::getEntries().size() == 1);
+        REQUIRE(VirtFsDir::getEntries()[0]->mRootDir == "dir1/");
+        REQUIRE(VirtFsDir::getEntries()[0]->mUserDir == "dir1");
+        REQUIRE_THROWS(VirtFsDir::removeFromSearchPath("dir2"));
+        REQUIRE(VirtFsDir::removeFromSearchPath("dir1"));
+        REQUIRE(VirtFsDir::getEntries().size() == 0);
+        REQUIRE(VirtFsDir::addToSearchPathSilent("dir1",
+            Append_true,
+            SkipError_true));
+        REQUIRE(VirtFsDir::getEntries().size() == 1);
+        REQUIRE(VirtFsDir::getEntries()[0]->mRootDir == "dir1/");
+        REQUIRE(VirtFsDir::getEntries()[0]->mUserDir == "dir1");
+    }
+
+    VirtFsDir::deinit();
+    delete2(logger);
+}
+
+TEST_CASE("VirtFsDir exists")
+{
+    logger = new Logger();
+    VirtFsDir::addToSearchPathSilent("data",
+        Append_false,
+        SkipError_false);
+    VirtFsDir::addToSearchPathSilent("../data",
+        Append_false,
+        SkipError_false);
+
+    REQUIRE(VirtFsDir::exists("test/units.xml") == true);
+    REQUIRE(VirtFsDir::exists("test/units123.xml") == false);
+    REQUIRE(VirtFsDir::exists("tesQ/units.xml") == false);
+    REQUIRE(VirtFsDir::exists("units.xml") == false);
+
+    VirtFsDir::addToSearchPathSilent("data/test",
+        Append_false,
+        SkipError_false);
+    VirtFsDir::addToSearchPathSilent("../data/test",
+        Append_false,
+        SkipError_false);
+
+    REQUIRE(VirtFsDir::exists("test/units.xml") == true);
+    REQUIRE(VirtFsDir::exists("test/units123.xml") == false);
+    REQUIRE(VirtFsDir::exists("tesQ/units.xml") == false);
+    REQUIRE(VirtFsDir::exists("units.xml") == true);
+
+    VirtFsDir::removeFromSearchPathSilent("data/test");
+    VirtFsDir::removeFromSearchPathSilent("../data/test");
+
+    REQUIRE(VirtFsDir::exists("test/units.xml") == true);
+    REQUIRE(VirtFsDir::exists("test/units123.xml") == false);
+    REQUIRE(VirtFsDir::exists("tesQ/units.xml") == false);
+    REQUIRE(VirtFsDir::exists("units.xml") == false);
+
+    REQUIRE_THROWS(VirtFsDir::exists("test/../units.xml"));
+
+    VirtFsDir::deinit();
+    delete2(logger);
+}
+
+static void removeTemp(StringVect &restrict list)
+{
+    int cnt = 0;
+    std::sort(list.begin(), list.end());
+
+    FOR_EACH (StringVectIter, it, list)
+    {
+        if (*it != "serverlistplus.xml.part")
+        {
+            logger->log("file: %d %s",
+                cnt,
+                (*it).c_str());
+            cnt ++;
+        }
+    }
+
+    FOR_EACH (StringVectIter, it, list)
+    {
+        if (*it == "serverlistplus.xml.part")
+        {
+            list.erase(it);
+            return;
+        }
+    }
+}
+
+TEST_CASE("VirtFsDir getRealDir")
+{
+    logger = new Logger();
+    REQUIRE(VirtFsDir::getRealDir(".") == "");
+    REQUIRE(VirtFsDir::getRealDir("..") == "");
+    const bool dir1 = VirtFsDir::addToSearchPathSilent("data",
+        Append_false,
+        SkipError_false);
+    REQUIRE((dir1 || VirtFsDir::addToSearchPathSilent("../data",
+        Append_false,
+        SkipError_false)) == true);
+    if (dir1 == true)
+    {
+        REQUIRE(VirtFsDir::getRealDir("test") == "data");
+        REQUIRE(VirtFsDir::getRealDir("test/test.txt") ==
+            "data");
+    }
+    else
+    {
+        REQUIRE(VirtFsDir::getRealDir("test") == "../data");
+        REQUIRE(VirtFsDir::getRealDir("test/test.txt") ==
+            "../data");
+    }
+    REQUIRE(VirtFsDir::getRealDir("zzz") == "");
+
+    VirtFsDir::addToSearchPathSilent("data/test",
+        Append_false,
+        SkipError_false);
+    VirtFsDir::addToSearchPathSilent("../data/test",
+        Append_false,
+        SkipError_false);
+    if (dir1 == true)
+    {
+        REQUIRE(VirtFsDir::getRealDir("test") == "data");
+        REQUIRE(VirtFsDir::getRealDir("test/test.txt") ==
+            "data");
+        REQUIRE(VirtFsDir::getRealDir("test.txt") ==
+            "data/test");
+    }
+    else
+    {
+        REQUIRE(VirtFsDir::getRealDir("test") == "../data");
+        REQUIRE(VirtFsDir::getRealDir("test/test.txt") ==
+            "../data");
+        REQUIRE(VirtFsDir::getRealDir("test.txt") ==
+            "../data/test");
+    }
+    REQUIRE(VirtFsDir::getRealDir("zzz") == "");
+
+    VirtFsDir::removeFromSearchPathSilent("data/test");
+    VirtFsDir::removeFromSearchPathSilent("../data/test");
+
+    if (dir1 == true)
+    {
+        REQUIRE(VirtFsDir::getRealDir("test") == "data");
+        REQUIRE(VirtFsDir::getRealDir("test/test.txt") ==
+            "data");
+    }
+    else
+    {
+        REQUIRE(VirtFsDir::getRealDir("test") == "../data");
+        REQUIRE(VirtFsDir::getRealDir("test/test.txt") ==
+            "../data");
+    }
+    REQUIRE(VirtFsDir::getRealDir("zzz") == "");
+
+    VirtFsDir::removeFromSearchPathSilent("data");
+    VirtFsDir::removeFromSearchPathSilent("../data");
+    VirtFsDir::deinit();
+    delete2(logger);
+}
+
+static bool inList(VirtList *list,
+                   const std::string &name)
+{
+    FOR_EACH (StringVectCIter, it, list->names)
+    {
+        if (*it == name)
+            return true;
+    }
+    return false;
+}
+
+TEST_CASE("VirtFsDir enumerateFiles1")
+{
+    logger = new Logger;
+
+    VirtFsDir::addToSearchPathSilent("data",
+        Append_false,
+        SkipError_false);
+    VirtFsDir::addToSearchPathSilent("../data",
+        Append_false,
+        SkipError_false);
+
+    VirtList *list = nullptr;
+
+    const int cnt1 = VirtFsDir::exists("test/test2.txt") ? 24 : 23;
+    const int cnt2 = 24;
+
+    VirtFsDir::permitLinks(false);
+    list = VirtFsDir::enumerateFiles("test");
+    removeTemp(list->names);
+    const size_t sz = list->names.size();
+    REQUIRE(sz == cnt1);
+    VirtFsDir::freeList(list);
+
+    VirtFsDir::permitLinks(true);
+    list = VirtFsDir::enumerateFiles("test");
+    removeTemp(list->names);
+    REQUIRE(list->names.size() == cnt2);
+    VirtFsDir::freeList(list);
+
+    VirtFsDir::permitLinks(false);
+    list = VirtFsDir::enumerateFiles("test");
+    removeTemp(list->names);
+    REQUIRE(list->names.size() == cnt1);
+    VirtFsDir::freeList(list);
+
+    VirtFsDir::removeFromSearchPathSilent("data");
+    VirtFsDir::removeFromSearchPathSilent("../data");
+    VirtFsDir::deinit();
+    delete2(logger);
+}
+
+TEST_CASE("VirtFsDir enumerateFiles2")
+{
+    logger = new Logger;
+
+    VirtFsDir::addToSearchPathSilent("data/test/dir1",
+        Append_false,
+        SkipError_false);
+    VirtFsDir::addToSearchPathSilent("../data/test/dir1",
+        Append_false,
+        SkipError_false);
+
+    VirtList *list = nullptr;
+
+    list = VirtFsDir::enumerateFiles("/");
+    const size_t sz = list->names.size();
+    REQUIRE(list->names.size() == 5);
+    REQUIRE(inList(list, "file1.txt"));
+    REQUIRE_FALSE(inList(list, "file2.txt"));
+    VirtFsDir::freeList(list);
+    VirtFsDir::deinit();
+    delete2(logger);
+}
+
+TEST_CASE("VirtFsDir enumerateFiles3")
+{
+    logger = new Logger;
+
+    VirtFsDir::addToSearchPathSilent("data/test/dir1",
+        Append_false,
+        SkipError_false);
+    VirtFsDir::addToSearchPathSilent("../data/test/dir1",
+        Append_false,
+        SkipError_false);
+    VirtFsDir::addToSearchPathSilent("data/test/dir2",
+        Append_false,
+        SkipError_false);
+    VirtFsDir::addToSearchPathSilent("../data/test/dir2",
+        Append_false,
+        SkipError_false);
+
+    VirtList *list = nullptr;
+
+    list = VirtFsDir::enumerateFiles("/");
+    const size_t sz = list->names.size();
+    REQUIRE(list->names.size() == 6);
+    REQUIRE(inList(list, "file1.txt"));
+    REQUIRE(inList(list, "file2.txt"));
+    VirtFsDir::freeList(list);
+    VirtFsDir::deinit();
+    delete2(logger);
+}
+
+TEST_CASE("VirtFsDir isDirectory")
+{
+    logger = new Logger();
+    VirtFsDir::addToSearchPathSilent("data",
+        Append_false,
+        SkipError_false);
+    VirtFsDir::addToSearchPathSilent("../data",
+        Append_false,
+        SkipError_false);
+
+    REQUIRE(VirtFsDir::isDirectory("test/units.xml") == false);
+    REQUIRE(VirtFsDir::isDirectory("test/units.xml/") == false);
+    REQUIRE(VirtFsDir::isDirectory("test//units.xml") == false);
+    REQUIRE(VirtFsDir::isDirectory("test/units123.xml") == false);
+    REQUIRE(VirtFsDir::isDirectory("test//units123.xml") == false);
+    REQUIRE(VirtFsDir::isDirectory("tesQ/units.xml") == false);
+    REQUIRE(VirtFsDir::isDirectory("tesQ//units.xml") == false);
+    REQUIRE(VirtFsDir::isDirectory("units.xml") == false);
+    REQUIRE(VirtFsDir::isDirectory("test") == true);
+    REQUIRE(VirtFsDir::isDirectory("test/") == true);
+    REQUIRE(VirtFsDir::isDirectory("test//") == true);
+    REQUIRE(VirtFsDir::isDirectory("test/dir1") == true);
+    REQUIRE(VirtFsDir::isDirectory("test//dir1") == true);
+    REQUIRE(VirtFsDir::isDirectory("test//dir1/") == true);
+    REQUIRE(VirtFsDir::isDirectory("test//dir1//") == true);
+    REQUIRE(VirtFsDir::isDirectory("test/dir1/") == true);
+    REQUIRE(VirtFsDir::isDirectory("test/dir1//") == true);
+    REQUIRE(VirtFsDir::isDirectory("testQ") == false);
+    REQUIRE(VirtFsDir::isDirectory("testQ/") == false);
+    REQUIRE(VirtFsDir::isDirectory("testQ//") == false);
+
+    VirtFsDir::addToSearchPathSilent("data/test",
+        Append_false,
+        SkipError_false);
+    VirtFsDir::addToSearchPathSilent("../data/test",
+        Append_false,
+        SkipError_false);
+
+    REQUIRE(VirtFsDir::isDirectory("test/units.xml") == false);
+    REQUIRE(VirtFsDir::isDirectory("test/units.xml/") == false);
+    REQUIRE(VirtFsDir::isDirectory("test//units.xml") == false);
+    REQUIRE(VirtFsDir::isDirectory("test/units123.xml") == false);
+    REQUIRE(VirtFsDir::isDirectory("tesQ/units.xml") == false);
+    REQUIRE(VirtFsDir::isDirectory("units.xml") == false);
+    REQUIRE(VirtFsDir::isDirectory("test") == true);
+    REQUIRE(VirtFsDir::isDirectory("testQ") == false);
+    REQUIRE(VirtFsDir::isDirectory("test/dir1") == true);
+
+    VirtFsDir::removeFromSearchPathSilent("data/test");
+    VirtFsDir::removeFromSearchPathSilent("../data/test");
+
+    REQUIRE(VirtFsDir::isDirectory("test/units.xml") == false);
+    REQUIRE(VirtFsDir::isDirectory("test/units123.xml") == false);
+    REQUIRE(VirtFsDir::isDirectory("tesQ/units.xml") == false);
+    REQUIRE(VirtFsDir::isDirectory("units.xml") == false);
+    REQUIRE(VirtFsDir::isDirectory("units.xml/") == false);
+    REQUIRE(VirtFsDir::isDirectory("test") == true);
+    REQUIRE(VirtFsDir::isDirectory("test/") == true);
+    REQUIRE(VirtFsDir::isDirectory("testQ") == false);
+    REQUIRE(VirtFsDir::isDirectory("test/dir1") == true);
+
+    VirtFsDir::removeFromSearchPathSilent("data");
+    VirtFsDir::removeFromSearchPathSilent("../data");
+    VirtFsDir::deinit();
+    delete2(logger);
+}
+
+TEST_CASE("VirtFsDir openRead")
+{
+    logger = new Logger();
+    VirtFsDir::addToSearchPathSilent("data",
+        Append_false,
+        SkipError_false);
+    VirtFsDir::addToSearchPathSilent("../data",
+        Append_false,
+        SkipError_false);
+
+    VirtFile *file = nullptr;
+
+    file = VirtFsDir::openRead("test/units.xml");
+    REQUIRE(file != nullptr);
+    VirtFsDir::close(file);
+    file = VirtFsDir::openRead("test/units123.xml");
+    REQUIRE(file == nullptr);
+    file = VirtFsDir::openRead("tesQ/units.xml");
+    REQUIRE(file == nullptr);
+    file = VirtFsDir::openRead("units.xml");
+    REQUIRE(file == nullptr);
+//    file = VirtFsDir::openRead("test");
+//    REQUIRE(file == nullptr);
+    file = VirtFsDir::openRead("testQ");
+    REQUIRE(file == nullptr);
+
+    VirtFsDir::addToSearchPathSilent("data/test",
+        Append_false,
+        SkipError_false);
+    VirtFsDir::addToSearchPathSilent("../data/test",
+        Append_false,
+        SkipError_false);
+
+    file = VirtFsDir::openRead("test/units.xml");
+    REQUIRE(file != nullptr);
+    VirtFsDir::close(file);
+    file = VirtFsDir::openRead("test/units123.xml");
+    REQUIRE(file == nullptr);
+    file = VirtFsDir::openRead("tesQ/units.xml");
+    REQUIRE(file == nullptr);
+    file = VirtFsDir::openRead("units.xml");
+    REQUIRE(file != nullptr);
+    VirtFsDir::close(file);
+//    file = VirtFsDir::openRead("test");
+//    REQUIRE(file == nullptr);
+    file = VirtFsDir::openRead("testQ");
+    REQUIRE(file == nullptr);
+
+    VirtFsDir::removeFromSearchPathSilent("data/test");
+    VirtFsDir::removeFromSearchPathSilent("../data/test");
+
+    file = VirtFsDir::openRead("test/units.xml");
+    REQUIRE(file != nullptr);
+    VirtFsDir::close(file);
+    file = VirtFsDir::openRead("test/units123.xml");
+    REQUIRE(file == nullptr);
+    file = VirtFsDir::openRead("tesQ/units.xml");
+    REQUIRE(file == nullptr);
+    file = VirtFsDir::openRead("units.xml");
+    REQUIRE(file == nullptr);
+//    file = VirtFsDir::openRead("test");
+//    REQUIRE(file == nullptr);
+    file = VirtFsDir::openRead("testQ");
+    REQUIRE(file == nullptr);
+
+    VirtFsDir::removeFromSearchPathSilent("data");
+    VirtFsDir::removeFromSearchPathSilent("../data");
+    VirtFsDir::deinit();
+    delete2(logger);
+}
+
+
+TEST_CASE("VirtFsDir permitLinks")
+{
+    logger = new Logger();
+    VirtFsDir::addToSearchPathSilent("data",
+        Append_false,
+        SkipError_false);
+    VirtFsDir::addToSearchPathSilent("../data",
+        Append_false,
+        SkipError_false);
+
+    const int cnt1 = VirtFsDir::exists("test/test2.txt") ? 22 : 21;
+    const int cnt2 = 22;
+
+    StringVect list;
+    VirtFsDir::permitLinks(false);
+    VirtFsDir::getFiles("test", list);
+    removeTemp(list);
+    const size_t sz = list.size();
+    REQUIRE(sz == cnt1);
+
+    list.clear();
+    VirtFsDir::permitLinks(true);
+    VirtFsDir::getFiles("test", list);
+    removeTemp(list);
+    REQUIRE(list.size() == cnt2);
+
+    list.clear();
+    VirtFsDir::permitLinks(false);
+    VirtFsDir::getFiles("test", list);
+    removeTemp(list);
+    REQUIRE(list.size() == cnt1);
+
+    VirtFsDir::removeFromSearchPathSilent("data");
+    VirtFsDir::removeFromSearchPathSilent("../data");
+    delete2(logger);
+}
+
+TEST_CASE("VirtFsDir read")
+{
+    logger = new Logger();
+    VirtFsDir::addToSearchPathSilent("data",
+        Append_false,
+        SkipError_false);
+    VirtFsDir::addToSearchPathSilent("../data",
+        Append_false,
+        SkipError_false);
+
+    VirtFile *file = VirtFsDir::openRead("test/test.txt");
+    REQUIRE(file != nullptr);
+    REQUIRE(VirtFsDir::fileLength(file) == 23);
+    const int fileSize = VirtFsDir::fileLength(file);
+
+    void *restrict buffer = calloc(fileSize + 1, 1);
+    REQUIRE(VirtFsDir::read(file, buffer, 1, fileSize) == fileSize);
+    REQUIRE(strcmp(static_cast<char*>(buffer),
+        "test line 1\ntest line 2") == 0);
+    REQUIRE(VirtFsDir::tell(file) == fileSize);
+    REQUIRE(VirtFsDir::eof(file) == true);
+
+    free(buffer);
+    buffer = calloc(fileSize + 1, 1);
+    REQUIRE(VirtFsDir::seek(file, 12) != 0);
+    REQUIRE(VirtFsDir::eof(file) == false);
+    REQUIRE(VirtFsDir::tell(file) == 12);
+    REQUIRE(VirtFsDir::read(file, buffer, 1, 11) == 11);
+    REQUIRE(strcmp(static_cast<char*>(buffer),
+        "test line 2") == 0);
+    REQUIRE(VirtFsDir::eof(file) == true);
+
+    VirtFsDir::close(file);
+    free(buffer);
+
+    VirtFsDir::removeFromSearchPathSilent("data");
+    VirtFsDir::removeFromSearchPathSilent("../data");
+    delete2(logger);
+}
diff --git a/src/fs/virtfstools.cpp b/src/fs/virtfstools.cpp
index 889739658..d227f5f07 100644
--- a/src/fs/virtfstools.cpp
+++ b/src/fs/virtfstools.cpp
@@ -24,6 +24,7 @@
 
 #include "fs/paths.h"
 #include "fs/virtfs.h"
+#include "fs/virtfsdir.h"
 #include "fs/virtlist.h"
 
 #include "utils/stringutils.h"
@@ -221,3 +222,19 @@ namespace VirtFs
         return true;
     }
 }  // namespace VirtFs
+
+// +++ temporary add it here
+namespace VirtFsDir
+{
+    void getFiles(const std::string &path,
+                  StringVect &list)
+    {
+        VirtList *const fonts = VirtFsDir::enumerateFiles(path);
+        FOR_EACH (StringVectCIter, i, fonts->names)
+        {
+            if (!VirtFsDir::isDirectory(path + dirSeparator + *i))
+                list.push_back(*i);
+        }
+        VirtFsDir::freeList(fonts);
+    }
+}  // namespace VirtFs
diff --git a/src/fs/virtfstools.h b/src/fs/virtfstools.h
index 4f2a77f4c..e29e3e427 100644
--- a/src/fs/virtfstools.h
+++ b/src/fs/virtfstools.h
@@ -51,4 +51,11 @@ namespace VirtFs
     std::string loadTextFileString(const std::string &fileName);
 }  // namespace VirtFs
 
+// +++ temporary add it here
+namespace VirtFsDir
+{
+    void getFiles(const std::string &path,
+                  StringVect &list);
+}  // namespace VirtFs
+
 #endif  // UTILS_VIRTFSTOOLS_H
-- 
cgit v1.2.3-70-g09d2