From dcc37329df40e9b591c7a16c862de500c979349a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joakim=20Nohlg=C3=A5rd?= <joakim.nohlgard@eistec.se>
Date: Fri, 8 Jul 2016 15:56:09 +0200
Subject: [PATCH] sys/vfs: A virtual file system (VFS) layer for RIOT

The VFS layer provides file system abstractions to allow using a unified
interface to access files from mounted file systems.
---
 Makefile.dep                                  |   4 +
 dist/tools/mkconstfs/README.md                |  16 +
 dist/tools/mkconstfs/mkconstfs.py             |  91 ++
 sys/Makefile                                  |   4 +
 sys/Makefile.include                          |   4 +
 sys/fs/constfs/Makefile                       |   2 +
 sys/fs/constfs/constfs.c                      | 319 ++++++
 sys/fs/doc.txt                                |  12 +
 sys/include/fs/constfs.h                      |  66 ++
 sys/include/vfs.h                             | 834 ++++++++++++++++
 sys/shell/commands/Makefile                   |   3 +
 sys/shell/commands/sc_vfs.c                   | 424 ++++++++
 sys/shell/commands/shell_commands.c           |   9 +
 sys/vfs/Makefile                              |   1 +
 sys/vfs/vfs.c                                 | 932 ++++++++++++++++++
 tests/unittests/tests-vfs/Makefile            |   1 +
 tests/unittests/tests-vfs/Makefile.include    |   2 +
 tests/unittests/tests-vfs/tests-vfs-bind.c    | 122 +++
 tests/unittests/tests-vfs/tests-vfs-dir-ops.c | 124 +++
 .../unittests/tests-vfs/tests-vfs-file-ops.c  | 169 ++++
 .../tests-vfs/tests-vfs-file-system-ops.c     | 169 ++++
 .../tests-vfs/tests-vfs-mount-constfs.c       | 223 +++++
 .../tests-vfs/tests-vfs-normalize_path.c      | 153 +++
 .../tests-vfs/tests-vfs-open-close.c          |  52 +
 tests/unittests/tests-vfs/tests-vfs.c         |  40 +
 tests/unittests/tests-vfs/tests-vfs.h         |  37 +
 26 files changed, 3813 insertions(+)
 create mode 100644 dist/tools/mkconstfs/README.md
 create mode 100755 dist/tools/mkconstfs/mkconstfs.py
 create mode 100644 sys/fs/constfs/Makefile
 create mode 100644 sys/fs/constfs/constfs.c
 create mode 100644 sys/fs/doc.txt
 create mode 100644 sys/include/fs/constfs.h
 create mode 100644 sys/include/vfs.h
 create mode 100644 sys/shell/commands/sc_vfs.c
 create mode 100644 sys/vfs/Makefile
 create mode 100644 sys/vfs/vfs.c
 create mode 100644 tests/unittests/tests-vfs/Makefile
 create mode 100644 tests/unittests/tests-vfs/Makefile.include
 create mode 100644 tests/unittests/tests-vfs/tests-vfs-bind.c
 create mode 100644 tests/unittests/tests-vfs/tests-vfs-dir-ops.c
 create mode 100644 tests/unittests/tests-vfs/tests-vfs-file-ops.c
 create mode 100644 tests/unittests/tests-vfs/tests-vfs-file-system-ops.c
 create mode 100644 tests/unittests/tests-vfs/tests-vfs-mount-constfs.c
 create mode 100644 tests/unittests/tests-vfs/tests-vfs-normalize_path.c
 create mode 100644 tests/unittests/tests-vfs/tests-vfs-open-close.c
 create mode 100644 tests/unittests/tests-vfs/tests-vfs.c
 create mode 100644 tests/unittests/tests-vfs/tests-vfs.h

diff --git a/Makefile.dep b/Makefile.dep
index cc85b1fcef..b909cf125f 100644
--- a/Makefile.dep
+++ b/Makefile.dep
@@ -597,6 +597,10 @@ ifneq (,$(filter emcute,$(USEMODULE)))
   USEMODULE += xtimer
 endif
 
+ifneq (,$(filter constfs,$(USEMODULE)))
+  USEMODULE += vfs
+endif
+
 # include package dependencies
 -include $(USEPKG:%=$(RIOTPKG)/%/Makefile.dep)
 
diff --git a/dist/tools/mkconstfs/README.md b/dist/tools/mkconstfs/README.md
new file mode 100644
index 0000000000..c71a69ba04
--- /dev/null
+++ b/dist/tools/mkconstfs/README.md
@@ -0,0 +1,16 @@
+# Introduction
+
+This tool creates a .c file including all data from a local directory as data
+structures that can be mounted using constfs.
+
+# Usage
+
+    mkconstfs.py /path/to/files /
+
+    #include "vfs.h"
+    #include "fs/constfs.h"
+    extern const vfs_mount_t _constfs;
+
+    [...]
+
+    vfs_mount((vfs_mount_t *)&_constfs);
diff --git a/dist/tools/mkconstfs/mkconstfs.py b/dist/tools/mkconstfs/mkconstfs.py
new file mode 100755
index 0000000000..bda89c76d8
--- /dev/null
+++ b/dist/tools/mkconstfs/mkconstfs.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env python3
+
+import codecs
+import os
+import sys
+
+FILE_TYPE = "static const uint8_t"
+
+def mkconstfs(root_path, mount_point, constfs_name):
+    print("/* This file was automatically generated by mkconstfs */")
+    print("#include \"fs/constfs.h\"")
+    print("")
+
+    for dirname, subdir_list, file_list in os.walk(root_path):
+        target_dirname = os.path.join("/", dirname[len(root_path):])
+        for fname in file_list:
+            local_fname = os.path.join(dirname, fname)
+            target_fname = os.path.join(target_dirname, fname)
+            print_file_data(local_fname, target_fname)
+
+    print("\nstatic const constfs_file_t _files[] = {")
+
+    for mangled_name, target_name, _ in files:
+        print("    {")
+        print("    .path = \"%s\"," % target_name)
+        print("    .data = %s," % mangled_name)
+        print("    .size = sizeof(%s)" % mangled_name)
+        print("    },")
+    print("};")
+
+    print("""
+static const constfs_t _fs_data = {
+    .files = _files,
+    .nfiles = sizeof(_files) / sizeof(_files[0]),
+};
+
+vfs_mount_t %s = {
+    .fs = &constfs_file_system,
+    .mount_point = \"%s\",
+    .private_data = (void *)&_fs_data,
+};
+    """ % (constfs_name, mount_point))
+
+def mangle_name(fname):
+    fname = fname.replace("/", "__")
+    fname = fname.replace(".", "__")
+
+    return fname
+
+def print_file_data(local_fname, target_fname):
+    mangled_name = mangle_name(target_fname)
+    print(FILE_TYPE, mangled_name, "[] = {", end="")
+
+    line_length = 8
+    nread = 0
+    with open(local_fname, 'rb') as f:
+        byte = f.read(1)
+        while byte:
+            if nread == 0:
+                print("\n    ", end="")
+            elif nread % line_length == 0:
+                print(",\n    ", end="")
+            else:
+                print(", ", end="")
+            nread += 1
+            print ("0x" + codecs.encode(byte, 'hex').decode('ascii'), end="")
+            # Do stuff with byte.
+            byte = f.read(1)
+
+    print("\n};")
+
+    files.append((mangled_name, target_fname, nread))
+
+files = []
+
+if __name__=="__main__":
+    mountpoint = "/"
+    constfs_name = "_constfs"
+
+    if len(sys.argv) < 2:
+        print("usage: mkconstfs.py <path> [mountpoint] [constfs_name]")
+        exit(1)
+
+    path = sys.argv[1]
+    if len(sys.argv) > 2:
+        mountpoint = sys.argv[2]
+
+    if len(sys.argv) > 3:
+        constfs_name = sys.argv[3]
+
+    mkconstfs(path, mountpoint, constfs_name)
diff --git a/sys/Makefile b/sys/Makefile
index 80ebe4ee28..8e4f043577 100644
--- a/sys/Makefile
+++ b/sys/Makefile
@@ -112,6 +112,10 @@ ifneq (,$(filter emcute,$(USEMODULE)))
     DIRS += net/application_layer/emcute
 endif
 
+ifneq (,$(filter constfs,$(USEMODULE)))
+    DIRS += fs/constfs
+endif
+
 DIRS += $(dir $(wildcard $(addsuffix /Makefile, ${USEMODULE})))
 
 include $(RIOTBASE)/Makefile.base
diff --git a/sys/Makefile.include b/sys/Makefile.include
index 7a460af0f8..680f655b17 100644
--- a/sys/Makefile.include
+++ b/sys/Makefile.include
@@ -32,6 +32,10 @@ ifneq (,$(filter oneway_malloc,$(USEMODULE)))
     USEMODULE_INCLUDES += $(RIOTBASE)/sys/oneway-malloc/include
 endif
 
+ifneq (,$(filter vfs,$(USEMODULE)))
+    USEMODULE_INCLUDES += $(RIOTBASE)/sys/posix/include
+endif
+
 ifneq (,$(filter cpp11-compat,$(USEMODULE)))
     USEMODULE_INCLUDES += $(RIOTBASE)/sys/cpp11-compat/include
     # make sure cppsupport.o is linked explicitly because __dso_handle is not
diff --git a/sys/fs/constfs/Makefile b/sys/fs/constfs/Makefile
new file mode 100644
index 0000000000..9798aac2d4
--- /dev/null
+++ b/sys/fs/constfs/Makefile
@@ -0,0 +1,2 @@
+MODULE=constfs
+include $(RIOTBASE)/Makefile.base
diff --git a/sys/fs/constfs/constfs.c b/sys/fs/constfs/constfs.c
new file mode 100644
index 0000000000..9b29a08a19
--- /dev/null
+++ b/sys/fs/constfs/constfs.c
@@ -0,0 +1,319 @@
+/*
+ * Copyright (C) 2016 Eistec AB
+ *
+ * This file is subject to the terms and conditions of the GNU Lesser
+ * General Public License v2.1. See the file LICENSE in the top level
+ * directory for more details.
+ */
+
+/**
+ * @ingroup     fs_constfs
+ * @{
+ *
+ * @file
+ * @brief       ConstFS implementation
+ *
+ * @author      Joakim Nohlgård <joakim.nohlgard@eistec.se>
+ *
+ * @}
+ */
+
+/* Required for strnlen in string.h, when building with -std=c99 */
+#define _DEFAULT_SOURCE 1
+#include <stddef.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+
+#include "fs/constfs.h"
+#include "vfs.h"
+
+#define ENABLE_DEBUG (0)
+#include "debug.h"
+
+/* File system operations */
+static int constfs_mount(vfs_mount_t *mountp);
+static int constfs_umount(vfs_mount_t *mountp);
+static int constfs_unlink(vfs_mount_t *mountp, const char *name);
+static int constfs_stat(vfs_mount_t *mountp, const char *restrict name, struct stat *restrict buf);
+static int constfs_statvfs(vfs_mount_t *mountp, const char *restrict path, struct statvfs *restrict buf);
+
+/* File operations */
+static int constfs_close(vfs_file_t *filp);
+static int constfs_fstat(vfs_file_t *filp, struct stat *buf);
+static off_t constfs_lseek(vfs_file_t *filp, off_t off, int whence);
+static int constfs_open(vfs_file_t *filp, const char *name, int flags, mode_t mode, const char *abs_path);
+static ssize_t constfs_read(vfs_file_t *filp, void *dest, size_t nbytes);
+static ssize_t constfs_write(vfs_file_t *filp, const void *src, size_t nbytes);
+
+/* Directory operations */
+static int constfs_opendir(vfs_DIR *dirp, const char *dirname, const char *abs_path);
+static int constfs_readdir(vfs_DIR *dirp, vfs_dirent_t *entry);
+static int constfs_closedir(vfs_DIR *dirp);
+
+static const vfs_file_system_ops_t constfs_fs_ops = {
+    .mount = constfs_mount,
+    .umount = constfs_umount,
+    .unlink = constfs_unlink,
+    .statvfs = constfs_statvfs,
+    .stat = constfs_stat,
+};
+
+static const vfs_file_ops_t constfs_file_ops = {
+    .close = constfs_close,
+    .fstat = constfs_fstat,
+    .lseek = constfs_lseek,
+    .open  = constfs_open,
+    .read  = constfs_read,
+    .write = constfs_write,
+};
+
+static const vfs_dir_ops_t constfs_dir_ops = {
+    .opendir = constfs_opendir,
+    .readdir = constfs_readdir,
+    .closedir = constfs_closedir,
+};
+
+
+const vfs_file_system_t constfs_file_system = {
+    .f_op = &constfs_file_ops,
+    .fs_op = &constfs_fs_ops,
+    .d_op = &constfs_dir_ops,
+};
+
+/**
+ * @internal
+ * @brief Fill a file information struct with information about the file
+ * pointed to by @p fp
+ *
+ * @param[in]  fp     file to query
+ * @param[out] buf    output buffer
+ */
+static void _constfs_write_stat(const constfs_file_t *fp, struct stat *restrict buf);
+
+static int constfs_mount(vfs_mount_t *mountp)
+{
+    /* perform any extra initialization here */
+    (void) mountp; /* prevent warning: unused parameter */
+    return 0;
+}
+
+static int constfs_umount(vfs_mount_t *mountp)
+{
+    /* free resources and perform any clean up here */
+    (void) mountp; /* prevent warning: unused parameter */
+    return 0;
+}
+
+static int constfs_unlink(vfs_mount_t *mountp, const char *name)
+{
+    /* Removing files is prohibited */
+    (void) mountp; /* prevent warning: unused parameter */
+    (void) name; /* prevent warning: unused parameter */
+    return -EROFS;
+}
+
+static int constfs_stat(vfs_mount_t *mountp, const char *restrict name, struct stat *restrict buf)
+{
+    (void) name;
+    /* Fill out some information about this file */
+    if (buf == NULL) {
+        return -EFAULT;
+    }
+    constfs_t *fs = mountp->private_data;
+    /* linear search through the files array */
+    for (size_t i = 0; i < fs->nfiles; ++i) {
+        DEBUG("constfs_stat ? \"%s\"\n", fs->files[i].path);
+        if (strcmp(fs->files[i].path, name) == 0) {
+            DEBUG("constfs_stat: Found :)\n");
+            _constfs_write_stat(&fs->files[i], buf);
+            buf->st_ino = i;
+            return 0;
+        }
+    }
+    DEBUG("constfs_stat: Not found :(\n");
+    return -ENOENT;
+}
+
+static int constfs_statvfs(vfs_mount_t *mountp, const char *restrict path, struct statvfs *restrict buf)
+{
+    (void) path;
+    /* Fill out some information about this file system */
+    if (buf == NULL) {
+        return -EFAULT;
+    }
+    constfs_t *fs = mountp->private_data;
+    /* clear out the stat buffer first */
+    memset(buf, 0, sizeof(*buf));
+    buf->f_bsize = sizeof(uint8_t); /* block size */
+    buf->f_frsize = sizeof(uint8_t); /* fundamental block size */
+    fsblkcnt_t f_blocks = 0;
+    for (size_t i = 0; i < fs->nfiles; ++i) {
+        f_blocks += fs->files[i].size;
+    }
+    buf->f_blocks = f_blocks;  /* Blocks total */
+    buf->f_bfree = 0;          /* Blocks free */
+    buf->f_bavail = 0;         /* Blocks available to non-privileged processes */
+    buf->f_files = fs->nfiles; /* Total number of file serial numbers */
+    buf->f_ffree = 0;          /* Total number of free file serial numbers */
+    buf->f_favail = 0;         /* Number of file serial numbers available to non-privileged process */
+    buf->f_fsid = 0;           /* File system id */
+    buf->f_flag = (ST_RDONLY | ST_NOSUID); /* File system flags */
+    buf->f_namemax = UINT8_MAX; /* Maximum file name length */
+    return 0;
+}
+
+static int constfs_close(vfs_file_t *filp)
+{
+    /* perform any necessary clean ups */
+    (void) filp; /* prevent warning: unused parameter */
+    return 0;
+}
+
+static int constfs_fstat(vfs_file_t *filp, struct stat *buf)
+{
+    constfs_file_t *fp = filp->private_data.ptr;
+    if (buf == NULL) {
+        return -EFAULT;
+    }
+    _constfs_write_stat(fp, buf);
+    return 0;
+}
+
+static off_t constfs_lseek(vfs_file_t *filp, off_t off, int whence)
+{
+    constfs_file_t *fp = filp->private_data.ptr;
+    switch (whence) {
+        case SEEK_SET:
+            break;
+        case SEEK_CUR:
+            off += filp->pos;
+            break;
+        case SEEK_END:
+            off += fp->size;
+            break;
+        default:
+            return -EINVAL;
+    }
+    if (off < 0) {
+        /* the resulting file offset would be negative */
+        return -EINVAL;
+    }
+    /* POSIX allows seeking past the end of the file, even with O_RDONLY */
+    filp->pos = off;
+    return off;
+}
+
+static int constfs_open(vfs_file_t *filp, const char *name, int flags, mode_t mode, const char *abs_path)
+{
+    (void) mode;
+    (void) abs_path;
+    constfs_t *fs = filp->mp->private_data;
+    DEBUG("constfs_open: %p, \"%s\", 0x%x, 0%03lo, \"%s\"\n", (void *)filp, name, flags, (unsigned long)mode, abs_path);
+    /* We only support read access */
+    if ((flags & O_ACCMODE) != O_RDONLY) {
+        return -EROFS;
+    }
+    /* linear search through the files array */
+    for (size_t i = 0; i < fs->nfiles; ++i) {
+        DEBUG("constfs_open ? \"%s\"\n", fs->files[i].path);
+        if (strcmp(fs->files[i].path, name) == 0) {
+            DEBUG("constfs_open: Found :)\n");
+            filp->private_data.ptr = (void *)&fs->files[i];
+            return 0;
+        }
+    }
+    DEBUG("constfs_open: Not found :(\n");
+    return -ENOENT;
+}
+
+static ssize_t constfs_read(vfs_file_t *filp, void *dest, size_t nbytes)
+{
+    constfs_file_t *fp = filp->private_data.ptr;
+    DEBUG("constfs_read: %p, %p, %lu\n", (void *)filp, dest, (unsigned long)nbytes);
+    if ((size_t)filp->pos >= fp->size) {
+        /* Current offset is at or beyond end of file */
+        return 0;
+    }
+
+    if (nbytes > (fp->size - filp->pos)) {
+        nbytes = fp->size - filp->pos;
+    }
+    memcpy(dest, fp->data + filp->pos, nbytes);
+    DEBUG("constfs_read: read %d bytes\n", nbytes);
+    filp->pos += nbytes;
+    return nbytes;
+}
+
+static ssize_t constfs_write(vfs_file_t *filp, const void *src, size_t nbytes)
+{
+    DEBUG("constfs_write: %p, %p, %lu\n", (void *)filp, src, (unsigned long)nbytes);
+    /* Read only file system */
+    DEBUG("constfs_write: read only FS\n");
+    /* prevent warning: unused parameter */
+    (void) filp;
+    (void) src;
+    (void) nbytes;
+    return -EBADF;
+}
+
+static int constfs_opendir(vfs_DIR *dirp, const char *dirname, const char *abs_path)
+{
+    (void) abs_path;
+    DEBUG("constfs_opendir: %p, \"%s\", \"%s\"\n", (void *)dirp, dirname, abs_path);
+    if (strncmp(dirname, "/", 2) != 0) {
+        /* We keep it simple and only support a flat file system, only a root directory */
+        return -ENOENT;
+    }
+    dirp->private_data.value = 0;
+    return 0;
+}
+
+static int constfs_readdir(vfs_DIR *dirp, vfs_dirent_t *entry)
+{
+    DEBUG("constfs_readdir: %p, %p\n", (void *)dirp, (void *)entry);
+    constfs_t *fs = dirp->mp->private_data;
+    int filenum = dirp->private_data.value;
+    if ((size_t)filenum >= fs->nfiles) {
+        /* End of stream */
+        return 0;
+    }
+    const constfs_file_t *fp = &fs->files[filenum];
+    if (fp->path == NULL) {
+        return -EIO;
+    }
+    size_t len = strnlen(fp->path, VFS_NAME_MAX + 1);
+    if (len > VFS_NAME_MAX) {
+        /* name does not fit in vfs_dirent_t buffer */
+        /* skipping past the broken entry */
+        ++filenum;
+        dirp->private_data.value = filenum;
+        return -EAGAIN;
+    }
+    /* copy the string, including terminating null */
+    memcpy(&entry->d_name[0], fp->path, len + 1);
+    entry->d_ino = filenum;
+    ++filenum;
+    dirp->private_data.value = filenum;
+    return 1;
+}
+
+static int constfs_closedir(vfs_DIR *dirp)
+{
+    /* Just an example, it's not necessary to define closedir if there is
+     * nothing to clean up */
+    (void) dirp;
+    return 0;
+}
+
+static void _constfs_write_stat(const constfs_file_t *fp, struct stat *restrict buf)
+{
+    /* clear out the stat buffer first */
+    memset(buf, 0, sizeof(*buf));
+    buf->st_nlink = 1;
+    buf->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
+    buf->st_size = fp->size;
+    buf->st_blocks = fp->size;
+    buf->st_blksize = sizeof(uint8_t);
+}
diff --git a/sys/fs/doc.txt b/sys/fs/doc.txt
new file mode 100644
index 0000000000..f20b48aac5
--- /dev/null
+++ b/sys/fs/doc.txt
@@ -0,0 +1,12 @@
+/*
+ * Copyright (C) 2016 Eistec AB
+ *
+ * This file is subject to the terms and conditions of the GNU Lesser
+ * General Public License v2.1. See the file LICENSE in the top level
+ * directory for more details.
+ */
+
+/**
+ * @defgroup    fs File systems
+ * @brief       File system libraries
+ */
diff --git a/sys/include/fs/constfs.h b/sys/include/fs/constfs.h
new file mode 100644
index 0000000000..a0286e6dea
--- /dev/null
+++ b/sys/include/fs/constfs.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2016 Eistec AB
+ *
+ * This file is subject to the terms and conditions of the GNU Lesser
+ * General Public License v2.1. See the file LICENSE in the top level
+ * directory for more details.
+ */
+
+/**
+ * @defgroup  fs_constfs ConstFS static file system
+ * @ingroup   fs
+ * @brief     Constant file system resident in arrays
+ *
+ * This is an example of how to implement a simple file system driver for the
+ * RIOT VFS layer. The implementation uses an array of @c constfs_file_t objects
+ * as its storage back-end.
+ *
+ * @{
+ * @file
+ * @brief   ConstFS public API
+ * @author  Joakim Nohlgård <joakim.nohlgard@eistec.se>
+ */
+
+#ifndef CONSTFS_H_
+#define CONSTFS_H_
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "vfs.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief A file in ConstFS (file name + contents)
+ */
+typedef struct {
+    const char *path; /**< file system relative path to file */
+    const size_t size; /**< length of @c data */
+    const uint8_t *data; /**< pointer to file contents */
+} constfs_file_t;
+
+/**
+ * @brief ConstFS file system superblock
+ */
+typedef struct {
+    const size_t nfiles; /**< Number of files */
+    const constfs_file_t *files; /**< Files array */
+} constfs_t;
+
+/**
+ * @brief ConstFS file system driver
+ *
+ * For use with vfs_mount
+ */
+extern const vfs_file_system_t constfs_file_system;
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
+
+/** @} */
diff --git a/sys/include/vfs.h b/sys/include/vfs.h
new file mode 100644
index 0000000000..60c36420ce
--- /dev/null
+++ b/sys/include/vfs.h
@@ -0,0 +1,834 @@
+/*
+ * Copyright (C) 2016 Eistec AB
+ *
+ * This file is subject to the terms and conditions of the GNU Lesser
+ * General Public License v2.1. See the file LICENSE in the top level
+ * directory for more details.
+ */
+
+/**
+ * @defgroup  sys_vfs Virtual File System (VFS) layer
+ * @ingroup   sys
+ * @brief     Provides an interface for accessing files and directories from
+ *            different devices and file systems
+ *
+ * This layer is modeled as a mix between POSIX syscalls (e.g. open) and the
+ * Linux VFS layer implementation, with major reductions in the feature set, in
+ * order to fit the resource constrained platforms that RIOT targets.
+ *
+ * The overall design goals are:
+ * - Provide implementations for all newlib "file" syscalls
+ * - Keep it simple, do not add every possible file operation from Linux VFS.
+ * - Easy to map existing file system implementations for resource constrained systems onto the VFS layer API
+ * - Avoid keeping a central `enum` of all file system drivers that has to be kept up to date with external packages etc.
+ * - Use POSIX `<errno.h>` numbers as negative return codes for errors, avoid the global `errno` variable.
+ * - Only absolute paths to files (no per-process working directory)
+ * - No dynamic memory allocation
+ *
+ *
+ * The API should be easy to understand for users who are familiar with the
+ * POSIX file functions (open, close, read, write, fstat, lseek etc.)
+ *
+ * The VFS layer keeps track of mounted file systems and open files, the
+ * `vfs_open` function searches the array of mounted file systems and dispatches
+ * the call to the file system instance with the longest matching mount point prefix.
+ * Subsequent calls to `vfs_read`, `vfs_write`, etc will do a look up in the
+ * table of open files and dispatch the call to the correct file system driver
+ * for handling.
+ *
+ * `vfs_mount` takes a string containing the mount point, a file system driver
+ * specification (`struct file_system`), and an opaque pointer that only the FS
+ * driver knows how to use, which can be used to keep driver parameters in order
+ * to allow dynamic handling of multiple devices.
+ *
+ * @todo VFS layer reference counting and locking for open files and
+ *       simultaneous access.
+ *
+ * @{
+ * @file
+ * @brief   VFS layer API declarations
+ * @author  Joakim Nohlgård <joakim.nohlgard@eistec.se>
+ */
+
+#ifndef VFS_H_
+#define VFS_H_
+
+#include <stdint.h>
+/* The stdatomic.h in GCC gives compilation errors with C++
+ * see: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=60932
+ */
+#ifdef __cplusplus
+#include <atomic>
+/* Make atomic_int available without namespace specifier */
+using std::atomic_int;
+#else
+#include <stdatomic.h> /* for atomic_int */
+#endif
+#include <sys/stat.h> /* for struct stat */
+#include <sys/types.h> /* for off_t etc. */
+#include <sys/statvfs.h> /* for struct statvfs */
+
+#include "kernel_types.h"
+#include "clist.h"
+
+#ifdef __cplusplus
+extern "C" {
+/* restrict is a C99 keyword, not valid in C++, but GCC and Clang have the
+ * __restrict__ extension keyword which can be used instead */
+#define restrict __restrict__
+/* If the above is not supported by the compiler, you can replace it with an
+ * empty definition instead: */
+/* #define restrict */
+#endif
+
+#ifndef VFS_MAX_OPEN_FILES
+/**
+ * @brief Maximum number of simultaneous open files
+ */
+#define VFS_MAX_OPEN_FILES (16)
+#endif
+
+#ifndef VFS_DIR_BUFFER_SIZE
+/**
+ * @brief Size of buffer space in vfs_DIR
+ *
+ * This space is needed to avoid dynamic memory allocations for some file
+ * systems where a single pointer is not enough space for its directory stream
+ * state, e.g. SPIFFS.
+ *
+ * Guidelines:
+ *
+ * SPIFFS requires a sizeof(spiffs_DIR) (6-16 bytes, depending on target
+ * platform and configuration) buffer for its DIR struct.
+ *
+ * @attention File system developers: If your file system requires a buffer for
+ * DIR streams that is larger than a single pointer or @c int variable, ensure
+ * that you have a preprocessor check in your header file (so that it is
+ * impossible to attempt to mount the file system without running into a
+ * compiler error):
+ *
+ * @attention @code
+ * #if VFS_DIR_BUFFER_SIZE < 123
+ * #error VFS_DIR_BUFFER_SIZE is too small, at least 123 bytes is required
+ * #endif
+ * @endcode
+ *
+ * @attention Put the check in the public header file (.h), do not put the check in the
+ * implementation (.c) file.
+ */
+#define VFS_DIR_BUFFER_SIZE (12)
+#endif
+
+#ifndef VFS_NAME_MAX
+/**
+ * @brief Maximum length of the name in a @c vfs_dirent_t (not including terminating null)
+ *
+ * Maximum number of bytes in a filename (not including terminating null).
+ *
+ * Similar to the POSIX macro NAME_MAX
+ */
+#define VFS_NAME_MAX (31)
+#endif
+
+/**
+ * @brief Used with vfs_bind to bind to any available fd number
+ */
+#define VFS_ANY_FD (-1)
+
+/* Forward declarations */
+/**
+ * @brief struct @c vfs_file_ops typedef
+ */
+typedef struct vfs_file_ops vfs_file_ops_t;
+
+/**
+ * @brief struct @c vfs_dir_ops typedef
+ */
+typedef struct vfs_dir_ops vfs_dir_ops_t;
+
+/**
+ * @brief struct @c vfs_file_system_ops typedef
+ */
+typedef struct vfs_file_system_ops vfs_file_system_ops_t;
+
+/**
+ * @brief struct @c vfs_mount_struct typedef
+ */
+/* not struct vfs_mount because of name collision with the function */
+typedef struct vfs_mount_struct vfs_mount_t;
+
+/**
+ * @brief A file system driver
+ */
+typedef struct {
+    const vfs_file_ops_t *f_op;         /**< File operations table */
+    const vfs_dir_ops_t *d_op;          /**< Directory operations table */
+    const vfs_file_system_ops_t *fs_op; /**< File system operations table */
+} vfs_file_system_t;
+
+/**
+ * @brief A mounted file system
+ */
+struct vfs_mount_struct {
+    clist_node_t list_entry;     /**< List entry for the _vfs_mount_list list */
+    const vfs_file_system_t *fs; /**< The file system driver for the mount point */
+    const char *mount_point;     /**< Mount point, e.g. "/mnt/cdrom" */
+    size_t mount_point_len;      /**< Length of mount_point string (set by vfs_mount) */
+    atomic_int open_files;       /**< Number of currently open files */
+    void *private_data;          /**< File system driver private data, implementation defined */
+};
+
+/**
+ * @brief Information about an open file
+ *
+ * Similar to, but not equal to, <tt>struct file</tt> in Linux
+ */
+typedef struct {
+    const vfs_file_ops_t *f_op; /**< File operations table */
+    vfs_mount_t *mp;            /**< Pointer to mount table entry */
+    int flags;                  /**< File flags */
+    off_t pos;                  /**< Current position in the file */
+    kernel_pid_t pid;           /**< PID of the process that opened the file */
+    union {
+        void *ptr;              /**< pointer to private data */
+        int value;              /**< alternatively, you can use private_data as an int */
+    } private_data;             /**< File system driver private data, implementation defined */
+} vfs_file_t;
+
+/**
+ * @brief Internal representation of a file system directory entry
+ *
+ * Used by opendir, readdir, closedir
+ *
+ * @attention This structure should be treated as an opaque blob and must not be
+ * modified by user code. The contents should only be used by file system drivers.
+ */
+typedef struct {
+    const vfs_dir_ops_t *d_op; /**< Directory operations table */
+    vfs_mount_t *mp;           /**< Pointer to mount table entry */
+    union {
+        void *ptr;             /**< pointer to private data */
+        int value;             /**< alternatively, you can use private_data as an int */
+        uint8_t buffer[VFS_DIR_BUFFER_SIZE]; /**< Buffer space, in case a single pointer is not enough */
+    } private_data;            /**< File system driver private data, implementation defined */
+} vfs_DIR;
+
+/**
+ * @brief User facing directory entry
+ *
+ * Used to hold the output from readdir
+ *
+ * @note size, modification time, and other information is part of the file
+ * status, not the directory entry.
+ */
+typedef struct {
+    ino_t d_ino; /**< file serial number, unique for the file system ("inode" in Linux) */
+    char  d_name[VFS_NAME_MAX + 1]; /**< file name, relative to its containing directory */
+} vfs_dirent_t;
+
+/**
+ * @brief Operations on open files
+ *
+ * Similar, but not equal, to struct file_operations in Linux
+ */
+struct vfs_file_ops {
+    /**
+     * @brief Close an open file
+     *
+     * This function must perform any necessary clean ups and flush any internal
+     * buffers in the file system driver.
+     *
+     * If an error occurs, the file will still be considered closed by the VFS
+     * layer. Therefore, the proper clean up must still be performed by the file
+     * system driver before returning any error code.
+     *
+     * @note This implementation does not consider @c -EINTR a special return code,
+     * the file is still considered closed.
+     *
+     * @param[in]  filp     pointer to open file
+     *
+     * @return 0 on success
+     * @return <0 on error, the file is considered closed anyway
+     */
+    int (*close) (vfs_file_t *filp);
+
+    /**
+     * @brief Query/set options on an open file
+     *
+     * @param[in]  filp     pointer to open file
+     * @param[in]  cmd      fcntl command, see man 3p fcntl
+     * @param[in]  arg      argument to fcntl command, see man 3p fcntl
+     *
+     * @return 0 on success
+     * @return <0 on error
+     */
+    int (*fcntl) (vfs_file_t *filp, int cmd, int arg);
+
+    /**
+     * @brief Get status of an open file
+     *
+     * @param[in]  filp     pointer to open file
+     * @param[out] buf      pointer to stat struct to fill
+     *
+     * @return 0 on success
+     * @return <0 on error
+     */
+    int (*fstat) (vfs_file_t *filp, struct stat *buf);
+
+    /**
+     * @brief Seek to position in file
+     *
+     * @p whence determines the function of the seek and should be set to one of
+     * the following values:
+     *
+     *  - @c SEEK_SET: Seek to absolute offset @p off
+     *  - @c SEEK_CUR: Seek to current location + @p off
+     *  - @c SEEK_END: Seek to end of file + @p off
+     *
+     * @param[in]  filp     pointer to open file
+     * @param[in]  off      seek offset
+     * @param[in]  whence   determines the seek method, see detailed description
+     *
+     * @return the new seek location in the file on success
+     * @return <0 on error
+     */
+    off_t (*lseek) (vfs_file_t *filp, off_t off, int whence);
+
+    /**
+     * @brief Attempt to open a file in the file system at rel_path
+     *
+     * A file system driver should perform the necessary checks for file
+     * existence etc in this function.
+     *
+     * The VFS layer will initialize the contents of @p *filp so that
+     * @c filp->f_op points to the mounted file system's @c vfs_file_ops_t.
+     * @c filp->private_data.ptr will be initialized to NULL, @c filp->pos will
+     * be set to 0.
+     *
+     * @note @p name is an absolute path inside the file system, @p abs_path is
+     * the path to the file in the VFS, example:
+     * @p abs_path = "/mnt/hd/foo/bar", @p name = "/foo/bar"
+     *
+     * @note @p name and @p abs_path may point to different locations within the
+     * same const char array and the strings may overlap
+     *
+     * @param[in]  filp     pointer to open file
+     * @param[in]  name     null-terminated name of the file to open, relative to the file system root, including a leading slash
+     * @param[in]  flags    flags for opening, see man 2 open, man 3p open
+     * @param[in]  mode     mode for creating a new file, see man 2 open, man 3p open
+     * @param[in]  abs_path null-terminated name of the file to open, relative to the VFS root ("/")
+     *
+     * @return 0 on success
+     * @return <0 on error
+     */
+    int (*open) (vfs_file_t *filp, const char *name, int flags, mode_t mode, const char *abs_path);
+
+    /**
+     * @brief Read bytes from an open file
+     *
+     * @param[in]  filp     pointer to open file
+     * @param[in]  dest     pointer to destination buffer
+     * @param[in]  nbytes   maximum number of bytes to read
+     *
+     * @return number of bytes read on success
+     * @return <0 on error
+     */
+    ssize_t (*read) (vfs_file_t *filp, void *dest, size_t nbytes);
+
+    /**
+     * @brief Write bytes to an open file
+     *
+     * @param[in]  filp     pointer to open file
+     * @param[in]  src      pointer to source buffer
+     * @param[in]  nbytes   maximum number of bytes to write
+     *
+     * @return number of bytes written on success
+     * @return <0 on error
+     */
+    ssize_t (*write) (vfs_file_t *filp, const void *src, size_t nbytes);
+};
+
+/**
+ * @brief Operations on open directories
+ */
+struct vfs_dir_ops {
+    /**
+     * @brief Open a directory for reading with readdir
+     *
+     * @param[in]  dirp     pointer to open directory
+     * @param[in]  name     null-terminated name of the dir to open, relative to the file system root, including a leading slash
+     * @param[in]  abs_path null-terminated name of the dir to open, relative to the VFS root ("/")
+     *
+     * @return 0 on success
+     * @return <0 on error
+     */
+    int (*opendir) (vfs_DIR *dirp, const char *dirname, const char *abs_path);
+
+    /**
+     * @brief Read a single entry from the open directory dirp and advance the
+     * read position by one
+     *
+     * @p entry will be populated with information about the next entry in the
+     * directory stream @p dirp
+     *
+     * If @p entry was updated successfully, @c readdir shall return 1.
+     *
+     * If the end of stream was reached, @c readdir shall return 0 and @p entry
+     * shall remain untouched.
+     *
+     * @param[in]  dirp     pointer to open directory
+     * @param[out] entry    directory entry information
+     *
+     * @return 1 if @p entry was updated
+     * @return 0 if @p dirp has reached the end of the directory index
+     * @return <0 on error
+     */
+    int (*readdir) (vfs_DIR *dirp, vfs_dirent_t *entry);
+
+    /**
+     * @brief Close an open directory
+     *
+     * @param[in]  dirp     pointer to open directory
+     *
+     * @return 0 on success
+     * @return <0 on error, the directory stream dirp should be considered invalid
+     */
+    int (*closedir) (vfs_DIR *dirp);
+};
+
+/**
+ * @brief Operations on mounted file systems
+ *
+ * Similar, but not equal, to struct super_operations in Linux
+ */
+struct vfs_file_system_ops {
+    /**
+     * @brief Perform any extra processing needed after mounting a file system
+     *
+     * If this call returns an error, the whole vfs_mount call will signal a
+     * failure.
+     *
+     * All fields of @p mountp will be initialized by vfs_mount beforehand,
+     * @c private_data will be initialized to NULL.
+     *
+     * @param[in]  mountp  file system mount being mounted
+     *
+     * @return 0 on success
+     * @return <0 on error
+     */
+    int (*mount) (vfs_mount_t *mountp);
+
+    /**
+     * @brief Perform the necessary clean up for unmounting a file system
+     *
+     * @param[in]  mountp  file system mount being unmounted
+     *
+     * @return 0 on success
+     * @return <0 on error
+     */
+    int (*umount) (vfs_mount_t *mountp);
+
+    /**
+     * @brief Rename a file
+     *
+     * The file @p from_path will be renamed to @p to_path
+     *
+     * @note it is not possible to rename files across different file system
+     *
+     * @param[in]  mountp     file system mount to operate on
+     * @param[in]  from_path  absolute path to existing file
+     * @param[in]  to_path    absolute path to destination
+     *
+     * @return 0 on success
+     * @return <0 on error
+     */
+    int (*rename) (vfs_mount_t *mountp, const char *from_path, const char *to_path);
+
+    /**
+     * @brief Unlink (delete) a file from the file system
+     *
+     * @param[in]  mountp  file system mount to operate on
+     * @param[in]  name    name of the file to delete
+     *
+     * @return 0 on success
+     * @return <0 on error
+     */
+    int (*unlink) (vfs_mount_t *mountp, const char *name);
+
+    /**
+     * @brief Create a directory on the file system
+     *
+     * @param[in]  mountp  file system mount to operate on
+     * @param[in]  name    name of the directory to create
+     * @param[in]  mode    file creation mode bits
+     *
+     * @return 0 on success
+     * @return <0 on error
+     */
+    int (*mkdir) (vfs_mount_t *mountp, const char *name, mode_t mode);
+
+    /**
+     * @brief Remove a directory from the file system
+     *
+     * Only empty directories may be removed.
+     *
+     * @param[in]  mountp  file system mount to operate on
+     * @param[in]  name    name of the directory to remove
+     *
+     * @return 0 on success
+     * @return <0 on error
+     */
+    int (*rmdir) (vfs_mount_t *mountp, const char *name);
+
+    /**
+     * @brief Get file status
+     *
+     * @param[in]  mountp  file system mount to operate on
+     * @param[in]  path    path to file being queried
+     * @param[out] buf     pointer to stat struct to fill
+     *
+     * @return 0 on success
+     * @return <0 on error
+     */
+    int (*stat) (vfs_mount_t *mountp, const char *restrict path, struct stat *restrict buf);
+
+    /**
+     * @brief Get file system status
+     *
+     * @p path is only passed for consistency against the POSIX statvfs function.
+     * @c vfs_statvfs calls this function only when it has determined that
+     * @p path belongs to this file system. @p path is a file system relative
+     * path and does not necessarily name an existing file.
+     *
+     * @param[in]  mountp  file system mount to operate on
+     * @param[in]  path    path to a file on the file system being queried
+     * @param[out] buf     pointer to statvfs struct to fill
+     *
+     * @return 0 on success
+     * @return <0 on error
+     */
+    int (*statvfs) (vfs_mount_t *mountp, const char *restrict path, struct statvfs *restrict buf);
+
+    /**
+     * @brief Get file system status of an open file
+     *
+     * @p path is only passed for consistency against the POSIX statvfs function.
+     * @c vfs_statvfs calls this function only when it has determined that
+     * @p path belongs to this file system. @p path is a file system relative
+     * path and does not necessarily name an existing file.
+     *
+     * @param[in]  mountp  file system mount to operate on
+     * @param[in]  filp    pointer to an open file on the file system being queried
+     * @param[out] buf     pointer to statvfs struct to fill
+     *
+     * @return 0 on success
+     * @return <0 on error
+     */
+    int (*fstatvfs) (vfs_mount_t *mountp, vfs_file_t *filp, struct statvfs *buf);
+};
+
+/**
+ * @brief Close an open file
+ *
+ * @param[in]  fd    fd number to close
+ *
+ * @return 0 on success
+ * @return <0 on error
+ */
+int vfs_close(int fd);
+
+/**
+ * @brief Query/set options on an open file
+ *
+ * @param[in]  fd    fd number to operate on
+ * @param[in]  cmd   fcntl command, see man 3p fcntl
+ * @param[in]  arg   argument to fcntl command, see man 3p fcntl
+ *
+ * @return 0 on success
+ * @return <0 on error
+ */
+int vfs_fcntl(int fd, int cmd, int arg);
+
+/**
+ * @brief Get status of an open file
+ *
+ * @param[in]  fd       fd number obtained from vfs_open
+ * @param[out] buf      pointer to stat struct to fill
+ *
+ * @return 0 on success
+ * @return <0 on error
+ */
+int vfs_fstat(int fd, struct stat *buf);
+
+/**
+ * @brief Get file system status of the file system containing an open file
+ *
+ * @param[in]  fd       fd number obtained from vfs_open
+ * @param[out] buf      pointer to statvfs struct to fill
+ *
+ * @return 0 on success
+ * @return <0 on error
+ */
+int vfs_fstatvfs(int fd, struct statvfs *buf);
+
+/**
+ * @brief Seek to position in file
+ *
+ * @p whence determines the function of the seek and should be set to one of
+ * the following values:
+ *
+ *  - @c SEEK_SET: Seek to absolute offset @p off
+ *  - @c SEEK_CUR: Seek to current location + @p off
+ *  - @c SEEK_END: Seek to end of file + @p off
+ *
+ * @param[in]  fd       fd number obtained from vfs_open
+ * @param[in]  off      seek offset
+ * @param[in]  whence   determines the seek method, see detailed description
+ *
+ * @return the new seek location in the file on success
+ * @return <0 on error
+ */
+off_t vfs_lseek(int fd, off_t off, int whence);
+
+/**
+ * @brief Open a file
+ *
+ * @param[in]  name    file name to open
+ * @param[in]  flags   flags for opening, see man 3p open
+ * @param[in]  mode    file mode
+ *
+ * @return fd number on success (>= 0)
+ * @return <0 on error
+ */
+int vfs_open(const char *name, int flags, mode_t mode);
+
+/**
+ * @brief Read bytes from an open file
+ *
+ * @param[in]  fd       fd number obtained from vfs_open
+ * @param[out] dest     destination buffer to hold the file contents
+ * @param[in]  count    maximum number of bytes to read
+ *
+ * @return number of bytes read on success
+ * @return <0 on error
+ */
+ssize_t vfs_read(int fd, void *dest, size_t count);
+
+/**
+ * @brief Write bytes to an open file
+ *
+ * @param[in]  fd       fd number obtained from vfs_open
+ * @param[in]  src      pointer to source buffer
+ * @param[in]  count    maximum number of bytes to write
+ *
+ * @return number of bytes written on success
+ * @return <0 on error
+ */
+ssize_t vfs_write(int fd, const void *src, size_t count);
+
+/**
+ * @brief Open a directory for reading with readdir
+ *
+ * The data in @c *dirp will be initialized by @c vfs_opendir
+ *
+ * @param[out] dirp     pointer to directory stream struct for storing the state
+ * @param[in]  dirname  null-terminated name of the dir to open, absolute file system path
+ *
+ * @return 0 on success
+ * @return <0 on error
+ */
+int vfs_opendir(vfs_DIR *dirp, const char *dirname);
+
+/**
+ * @brief Read a single entry from the open directory dirp and advance the
+ * read position by one
+ *
+ * @p entry will be populated with information about the next entry in the
+ * directory stream @p dirp
+ *
+ * @attention Calling vfs_readdir on an uninitialized @c vfs_DIR is forbidden
+ * and may lead to file system corruption and random system failures.
+ *
+ * @param[in]  dirp     pointer to open directory
+ * @param[out] entry    directory entry information
+ *
+ * @return 0 on success
+ * @return <0 on error
+ */
+int vfs_readdir(vfs_DIR *dirp, vfs_dirent_t *entry);
+
+/**
+ * @brief Close an open directory
+ *
+ * @attention Calling vfs_closedir on an uninitialized @c vfs_DIR is forbidden
+ * and may lead to file system corruption and random system failures.
+ *
+ * @param[in]  dirp     pointer to open directory
+ *
+ * @return 0 on success
+ * @return <0 on error, the directory stream dirp should be considered invalid
+ */
+int vfs_closedir(vfs_DIR *dirp);
+
+/**
+ * @brief Mount a file system
+ *
+ * @p mountp should have been populated in advance with a file system driver,
+ * a mount point, and private_data (if the file system driver uses one).
+ *
+ * @param[in]  mountp    pointer to the mount structure of the file system to mount
+ *
+ * @return 0 on success
+ * @return <0 on error
+ */
+int vfs_mount(vfs_mount_t *mountp);
+
+/**
+ * @brief Rename a file
+ *
+ * The file @p from_path will be renamed to @p to_path
+ *
+ * @note it is not possible to rename files across different file system
+ *
+ * @param[in]  from_path  absolute path to existing file
+ * @param[in]  to_path    absolute path to destination
+ *
+ * @return 0 on success
+ * @return <0 on error
+ */
+int vfs_rename(const char *from_path, const char *to_path);
+
+/**
+ * @brief Unmount a mounted file system
+ *
+ * This will fail if there are any open files on the mounted file system
+ *
+ * @param[in]  mountp    pointer to the mount structure of the file system to unmount
+ *
+ * @return 0 on success
+ * @return <0 on error
+ */
+int vfs_umount(vfs_mount_t *mountp);
+
+/**
+ * @brief Unlink (delete) a file from a mounted file system
+ *
+ * @param[in]  name   name of file to delete
+ *
+ * @return 0 on success
+ * @return <0 on error
+ */
+int vfs_unlink(const char *name);
+
+/**
+ * @brief Create a directory on the file system
+ *
+ * @param[in]  name    name of the directory to create
+ * @param[in]  mode    file creation mode bits
+ *
+ * @return 0 on success
+ * @return <0 on error
+ */
+int vfs_mkdir(const char *name, mode_t mode);
+
+/**
+ * @brief Remove a directory from the file system
+ *
+ * Only empty directories may be removed.
+ *
+ * @param[in]  name    name of the directory to remove
+ *
+ * @return 0 on success
+ * @return <0 on error
+ */
+int vfs_rmdir(const char *name);
+
+/**
+ * @brief Get file status
+ *
+ * @param[in]  path    path to file being queried
+ * @param[out] buf     pointer to stat struct to fill
+ *
+ * @return 0 on success
+ * @return <0 on error
+ */
+int vfs_stat(const char *restrict path, struct stat *restrict buf);
+
+/**
+ * @brief Get file system status
+ *
+ * @p path can be any path that resolves to the file system being queried, it
+ * does not have to be an existing file.
+ *
+ * @param[in]  path    path to a file on the file system being queried
+ * @param[out] buf     pointer to statvfs struct to fill
+ *
+ * @return 0 on success
+ * @return <0 on error
+ */
+int vfs_statvfs(const char *restrict path, struct statvfs *restrict buf);
+
+/**
+ * @brief Allocate a new file descriptor and give it file operations
+ *
+ * The new fd will be initialized with pointers to the given @p f_op file
+ * operations table and @p private_data.
+ *
+ * This function can be used to give file-like functionality to devices, e.g. UART.
+ *
+ * @p private_data can be used for passing instance information to the file
+ * operation handlers in @p f_op.
+ *
+ * @param[in]  fd            Desired fd number, use VFS_ANY_FD for any available fd
+ * @param[in]  flags         not implemented yet
+ * @param[in]  f_op          pointer to file operations table
+ * @param[in]  private_data  opaque pointer to private data
+ *
+ * @return fd number on success (>= 0)
+ * @return <0 on error
+ */
+int vfs_bind(int fd, int flags, const vfs_file_ops_t *f_op, void *private_data);
+
+/**
+ * @brief Normalize a path
+ *
+ * Normalizing a path means to remove all relative components ("..", ".") and
+ * any double slashes.
+ *
+ * @note @p buf is allowed to overlap @p path if @p &buf[0] <= @p &path[0]
+ *
+ * @attention @p path must be an absolute path (starting with @c / )
+ *
+ * @param[out] buf        buffer to store normalized path
+ * @param[in]  path       path to normalize
+ * @param[in]  buflen     available space in @p buf
+ *
+ * @return number of path components in the normalized path on success
+ * @return <0 on error
+ */
+int vfs_normalize_path(char *buf, const char *path, size_t buflen);
+
+/**
+ * @brief Iterate through all mounted file systems
+ *
+ * @attention Not thread safe! Do not mix calls to this function with other
+ * calls which modify the mount table, such as vfs_mount() and vfs_umount()
+ *
+ * Set @p cur to @c NULL to start from the beginning
+ *
+ * @see @c sc_vfs.c (@c df command) for a usage example
+ *
+ * @param[in]  cur  current iterator value
+ *
+ * @return     Pointer to next mounted file system in list after @p cur
+ * @return     NULL if @p cur is the last element in the list
+ */
+const vfs_mount_t *vfs_iterate_mounts(const vfs_mount_t *cur);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
+
+/** @} */
diff --git a/sys/shell/commands/Makefile b/sys/shell/commands/Makefile
index d3c9ccfc70..135bcc4669 100644
--- a/sys/shell/commands/Makefile
+++ b/sys/shell/commands/Makefile
@@ -61,6 +61,9 @@ endif
 ifneq (,$(filter sntp,$(USEMODULE)))
   SRC += sc_sntp.c
 endif
+ifneq (,$(filter vfs,$(USEMODULE)))
+  SRC += sc_vfs.c
+endif
 
 # TODO
 # Conditional building not possible at the moment due to
diff --git a/sys/shell/commands/sc_vfs.c b/sys/shell/commands/sc_vfs.c
new file mode 100644
index 0000000000..5571542d56
--- /dev/null
+++ b/sys/shell/commands/sc_vfs.c
@@ -0,0 +1,424 @@
+/*
+ * Copyright (C) 2016 Eistec AB
+ *
+ * This file is subject to the terms and conditions of the GNU Lesser
+ * General Public License v2.1. See the file LICENSE in the top level
+ * directory for more details.
+ */
+
+/**
+ * @ingroup     sys_shell_commands
+ * @{
+ *
+ * @file
+ * @brief       Shell commands for the VFS module
+ *
+ * @author      Joakim Nohlgård <joakim.nohlgard@eistec.se>
+ *
+ * @}
+ */
+
+#if MODULE_VFS
+#include <stdint.h>
+#include <inttypes.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <ctype.h>
+#include <errno.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+#include "vfs.h"
+
+#define SHELL_VFS_BUFSIZE 256
+static uint8_t _shell_vfs_data_buffer[SHELL_VFS_BUFSIZE];
+
+static void _ls_usage(char **argv)
+{
+    printf("%s <path>\n", argv[0]);
+    puts("list files in <path>");
+}
+
+static void _vfs_usage(char **argv)
+{
+    printf("%s <r|w> <path> [bytes] [offset]\n", argv[0]);
+    printf("%s ls <path>\n", argv[0]);
+    printf("%s cp <src> <dest>\n", argv[0]);
+    printf("%s mv <src> <dest>\n", argv[0]);
+    printf("%s rm <file>\n", argv[0]);
+    printf("%s df [path]\n", argv[0]);
+    puts("r: Read [bytes] bytes at [offset] in file <path>");
+    puts("w: not implemented yet");
+    puts("ls: list files in <path>");
+    puts("mv: Move <src> file to <dest>");
+    puts("cp: Copy <src> file to <dest>");
+    puts("rm: Unlink (delete) <file>");
+    puts("df: show file system space utilization stats");
+}
+
+/* Macro used by _errno_string to expand errno labels to string and print it */
+#define _case_snprintf_errno_name(x) \
+    case x: \
+        res = snprintf(buf, buflen, #x); \
+        break
+
+static int _errno_string(int err, char *buf, size_t buflen)
+{
+    int len = 0;
+    int res;
+    if (err < 0) {
+        res = snprintf(buf, buflen, "-");
+        if (res < 0) {
+            return res;
+        }
+        if ((size_t)res <= buflen) {
+            buf += res;
+            buflen -= res;
+        }
+        len += res;
+        err = -err;
+    }
+    switch (err) {
+        _case_snprintf_errno_name(EACCES);
+        _case_snprintf_errno_name(ENOENT);
+        _case_snprintf_errno_name(EINVAL);
+        _case_snprintf_errno_name(EFAULT);
+        _case_snprintf_errno_name(EROFS);
+        _case_snprintf_errno_name(EIO);
+        _case_snprintf_errno_name(ENAMETOOLONG);
+        _case_snprintf_errno_name(EPERM);
+
+        default:
+            res = snprintf(buf, buflen, "%d", err);
+            break;
+    }
+    if (res < 0) {
+        return res;
+    }
+    len += res;
+    return len;
+}
+#undef _case_snprintf_errno_name
+
+static void _print_df(const char *path)
+{
+    struct statvfs buf;
+    int res = vfs_statvfs(path, &buf);
+    printf("%-16s ", path);
+    if (res < 0) {
+        char err[16];
+        _errno_string(res, err, sizeof(err));
+        printf("statvfs failed: %s\n", err);
+        return;
+    }
+    printf("%12lu %12lu %12lu %7lu%%\n", (unsigned long)buf.f_blocks,
+        (unsigned long)(buf.f_blocks - buf.f_bfree), (unsigned long)buf.f_bavail,
+        (unsigned long)(((buf.f_blocks - buf.f_bfree) * 100) / buf.f_blocks));
+}
+
+static int _df_handler(int argc, char **argv)
+{
+    puts("Mountpoint              Total         Used    Available     Capacity");
+    if (argc > 1) {
+        const char *path = argv[1];
+        _print_df(path);
+    }
+    else {
+        /* Iterate through all mount points */
+        const vfs_mount_t *it = NULL;
+        while ((it = vfs_iterate_mounts(it)) != NULL) {
+            _print_df(it->mount_point);
+        }
+    }
+    return 0;
+}
+
+static int _read_handler(int argc, char **argv)
+{
+    uint8_t buf[16];
+    size_t nbytes = sizeof(buf);
+    off_t offset = 0;
+    char *path = argv[1];
+    if (argc < 2) {
+        puts("vfs read: missing file name");
+        return 1;
+    }
+    if (argc > 2) {
+        nbytes = atoi(argv[2]);
+    }
+    if (argc > 3) {
+        offset = atoi(argv[3]);
+    }
+
+    int res;
+    res = vfs_normalize_path(path, path, strlen(path) + 1);
+    if (res < 0) {
+        _errno_string(res, (char *)buf, sizeof(buf));
+        printf("Invalid path \"%s\": %s\n", path, buf);
+        return 5;
+    }
+
+    int fd = vfs_open(path, O_RDONLY, 0);
+    if (fd < 0) {
+        _errno_string(fd, (char *)buf, sizeof(buf));
+        printf("Error opening file \"%s\": %s\n", path, buf);
+        return 3;
+    }
+
+    res = vfs_lseek(fd, offset, SEEK_SET);
+    if (res < 0) {
+        _errno_string(res, (char *)buf, sizeof(buf));
+        printf("Seek error: %s\n", buf);
+        vfs_close(fd);
+        return 4;
+    }
+
+    while (nbytes > 0) {
+        memset(buf, 0, sizeof(buf));
+        size_t line_len = (nbytes < sizeof(buf) ? nbytes : sizeof(buf));
+        res = vfs_read(fd, buf, line_len);
+        if (res < 0) {
+            _errno_string(res, (char *)buf, sizeof(buf));
+            printf("Read error: %s\n", buf);
+            vfs_close(fd);
+            return 5;
+        }
+        else if ((size_t)res > line_len) {
+            printf("BUFFER OVERRUN! %d > %lu\n", res, (unsigned long)line_len);
+            vfs_close(fd);
+            return 6;
+        }
+        else if (res == 0) {
+            /* EOF */
+            printf("-- EOF --\n");
+            break;
+        }
+        printf("%08lx:", (unsigned long)offset);
+        for (int k = 0; k < res; ++k) {
+            if ((k % 2) == 0) {
+                putchar(' ');
+            }
+            printf("%02x", buf[k]);
+        }
+        for (int k = res; k < sizeof(buf); ++k) {
+            if ((k % 2) == 0) {
+                putchar(' ');
+            }
+            putchar(' ');
+            putchar(' ');
+        }
+        putchar(' ');
+        putchar(' ');
+        for (int k = 0; k < res; ++k) {
+            if (isprint(buf[k])) {
+                putchar(buf[k]);
+            }
+            else {
+                putchar('.');
+            }
+        }
+        puts("");
+        offset += res;
+        nbytes -= res;
+    }
+
+    vfs_close(fd);
+    return 0;
+}
+
+static int _cp_handler(int argc, char **argv)
+{
+    char errbuf[16];
+    if (argc < 3) {
+        _vfs_usage(argv);
+        return 1;
+    }
+    char *src_name = argv[1];
+    char *dest_name = argv[2];
+    printf("%s: copy src: %s dest: %s\n", argv[0], src_name, dest_name);
+
+    int fd_in = vfs_open(src_name, O_RDONLY, 0);
+    if (fd_in < 0) {
+        _errno_string(fd_in, (char *)errbuf, sizeof(errbuf));
+        printf("Error opening file for reading \"%s\": %s\n", src_name, errbuf);
+        return 2;
+    }
+    int fd_out = vfs_open(dest_name, O_WRONLY | O_TRUNC | O_CREAT, S_IRWXU | S_IRWXG | S_IRWXO);
+    if (fd_out < 0) {
+        _errno_string(fd_out, (char *)errbuf, sizeof(errbuf));
+        printf("Error opening file for writing \"%s\": %s\n", dest_name, errbuf);
+        return 2;
+    }
+    int eof = 0;
+    while (eof == 0) {
+        size_t bufspace = sizeof(_shell_vfs_data_buffer);
+        size_t pos = 0;
+        while (bufspace > 0) {
+            int res = vfs_read(fd_in, &_shell_vfs_data_buffer[pos], bufspace);
+            if (res < 0) {
+                _errno_string(res, (char *)errbuf, sizeof(errbuf));
+                printf("Error reading %lu bytes @ 0x%lx in \"%s\" (%d): %s\n",
+                    (unsigned long)bufspace, (unsigned long)pos, src_name, fd_in, errbuf);
+                vfs_close(fd_in);
+                vfs_close(fd_out);
+                return 2;
+            }
+            if (res == 0) {
+                /* EOF */
+                eof = 1;
+                break;
+            }
+            if (res > bufspace) {
+                printf("READ BUFFER OVERRUN! %d > %lu\n", res, (unsigned long)bufspace);
+                vfs_close(fd_in);
+                vfs_close(fd_out);
+                return 3;
+            }
+            pos += res;
+            bufspace -= res;
+        }
+        bufspace = pos;
+        pos = 0;
+        while (bufspace > 0) {
+            int res = vfs_write(fd_out, &_shell_vfs_data_buffer[pos], bufspace);
+            if (res <= 0) {
+                _errno_string(res, (char *)errbuf, sizeof(errbuf));
+                printf("Error writing %lu bytes @ 0x%lx in \"%s\" (%d): %s\n",
+                    (unsigned long)bufspace, (unsigned long)pos, dest_name, fd_out, errbuf);
+                vfs_close(fd_in);
+                vfs_close(fd_out);
+                return 4;
+            }
+            if (res > bufspace) {
+                printf("WRITE BUFFER OVERRUN! %d > %lu\n", res, (unsigned long)bufspace);
+                vfs_close(fd_in);
+                vfs_close(fd_out);
+                return 5;
+            }
+            bufspace -= res;
+        }
+    }
+    printf("Copied: %s -> %s\n", src_name, dest_name);
+    vfs_close(fd_in);
+    vfs_close(fd_out);
+    return 0;
+}
+
+static int _mv_handler(int argc, char **argv)
+{
+    if (argc < 3) {
+        _vfs_usage(argv);
+        return 1;
+    }
+    char *src_name = argv[1];
+    char *dest_name = argv[2];
+    printf("%s: move src: %s dest: %s\n", argv[0], src_name, dest_name);
+
+    int res = vfs_rename(src_name, dest_name);
+    if (res < 0) {
+        char errbuf[16];
+        _errno_string(res, (char *)errbuf, sizeof(errbuf));
+        printf("mv ERR: %s\n", errbuf);
+        return 2;
+    }
+    return 0;
+}
+
+static int _rm_handler(int argc, char **argv)
+{
+    if (argc < 2) {
+        _vfs_usage(argv);
+        return 1;
+    }
+    char *rm_name = argv[1];
+    printf("%s: unlink: %s\n", argv[0], rm_name);
+
+    int res = vfs_unlink(rm_name);
+    if (res < 0) {
+        char errbuf[16];
+        _errno_string(res, (char *)errbuf, sizeof(errbuf));
+        printf("rm ERR: %s\n", errbuf);
+        return 2;
+    }
+    return 0;
+}
+
+int _ls_handler(int argc, char **argv)
+{
+    if (argc < 2) {
+        _ls_usage(argv);
+        return 1;
+    }
+    char *path = argv[1];
+    uint8_t buf[16];
+    int res;
+    res = vfs_normalize_path(path, path, strlen(path) + 1);
+    if (res < 0) {
+        _errno_string(res, (char *)buf, sizeof(buf));
+        printf("Invalid path \"%s\": %s\n", path, buf);
+        return 5;
+    }
+    vfs_DIR dir;
+    res = vfs_opendir(&dir, path);
+    if (res < 0) {
+        _errno_string(res, (char *)buf, sizeof(buf));
+        printf("vfs_opendir error: %s\n", buf);
+        return 1;
+    }
+    unsigned int nfiles = 0;
+
+    while (1) {
+        vfs_dirent_t entry;
+        res = vfs_readdir(&dir, &entry);
+        if (res < 0) {
+            _errno_string(res, (char *)buf, sizeof(buf));
+            printf("vfs_opendir error: %s\n", buf);
+            if (res == -EAGAIN) {
+                /* try again */
+                continue;
+            }
+            return 2;
+        }
+        if (res == 0) {
+            /* end of stream */
+            break;
+        }
+        printf("%s\n", entry.d_name);
+        ++nfiles;
+    }
+    printf("total %u files\n", nfiles);
+    return 0;
+}
+
+int _vfs_handler(int argc, char **argv)
+{
+    if (argc < 2) {
+        _vfs_usage(argv);
+        return 1;
+    }
+    if (strcmp(argv[1], "r") == 0) {
+        /* pass on to read handler, shifting the arguments by one */
+        return _read_handler(argc - 1, &argv[1]);
+    }
+    else if (strcmp(argv[1], "ls") == 0) {
+        return _ls_handler(argc - 1, &argv[1]);
+    }
+    else if (strcmp(argv[1], "cp") == 0) {
+        return _cp_handler(argc - 1, &argv[1]);
+    }
+    else if (strcmp(argv[1], "mv") == 0) {
+        return _mv_handler(argc - 1, &argv[1]);
+    }
+    else if (strcmp(argv[1], "rm") == 0) {
+        return _rm_handler(argc - 1, &argv[1]);
+    }
+    else if (strcmp(argv[1], "df") == 0) {
+        return _df_handler(argc - 1, &argv[1]);
+    }
+    else {
+        printf("vfs: unsupported sub-command \"%s\"\n", argv[1]);
+        return 1;
+    }
+}
+#endif
diff --git a/sys/shell/commands/shell_commands.c b/sys/shell/commands/shell_commands.c
index 5713d6b550..d72730051c 100644
--- a/sys/shell/commands/shell_commands.c
+++ b/sys/shell/commands/shell_commands.c
@@ -127,6 +127,11 @@ extern int _ccnl_fib(int argc, char **argv);
 extern int _ntpdate(int argc, char **argv);
 #endif
 
+#ifdef MODULE_VFS
+extern int _vfs_handler(int argc, char **argv);
+extern int _ls_handler(int argc, char **argv);
+#endif
+
 const shell_command_t _shell_command_list[] = {
     {"reboot", "Reboot the node", _reboot_handler},
 #ifdef MODULE_CONFIG
@@ -211,6 +216,10 @@ const shell_command_t _shell_command_list[] = {
 #endif
 #ifdef MODULE_SNTP
     { "ntpdate", "synchronizes with a remote time server", _ntpdate },
+#endif
+#ifdef MODULE_VFS
+    {"vfs", "virtual file system operations", _vfs_handler},
+    {"ls", "list files", _ls_handler},
 #endif
     {NULL, NULL, NULL}
 };
diff --git a/sys/vfs/Makefile b/sys/vfs/Makefile
new file mode 100644
index 0000000000..48422e909a
--- /dev/null
+++ b/sys/vfs/Makefile
@@ -0,0 +1 @@
+include $(RIOTBASE)/Makefile.base
diff --git a/sys/vfs/vfs.c b/sys/vfs/vfs.c
new file mode 100644
index 0000000000..0217dd4fe3
--- /dev/null
+++ b/sys/vfs/vfs.c
@@ -0,0 +1,932 @@
+/*
+ * Copyright (C) 2016 Eistec AB
+ *
+ * This file is subject to the terms and conditions of the GNU Lesser
+ * General Public License v2.1. See the file LICENSE in the top level
+ * directory for more details.
+ */
+
+/**
+ * @ingroup     sys_vfs
+ * @{
+ * @file
+ * @brief   VFS layer implementation
+ * @author  Joakim Nohlgård <joakim.nohlgard@eistec.se>
+ */
+
+#include <errno.h> /* for error codes */
+#include <string.h> /* for strncmp */
+#include <stddef.h> /* for NULL */
+#include <sys/types.h> /* for off_t etc */
+#include <sys/stat.h> /* for struct stat */
+#include <sys/statvfs.h> /* for struct statvfs */
+#include <fcntl.h> /* for O_ACCMODE, ..., fcntl */
+
+#include "vfs.h"
+#include "mutex.h"
+#include "thread.h"
+#include "kernel_types.h"
+#include "clist.h"
+
+#define ENABLE_DEBUG (0)
+#include "debug.h"
+#if ENABLE_DEBUG
+/* Since some of these functions are called by printf, we can't really call
+ * printf from our functions or we end up in an infinite recursion. */
+#include <unistd.h> /* for STDOUT_FILENO */
+#define DEBUG_NOT_STDOUT(fd, ...) if (fd != STDOUT_FILENO) { DEBUG(__VA_ARGS__); }
+#else
+#define DEBUG_NOT_STDOUT(...)
+#endif
+
+/**
+ * @internal
+ * @brief Array of all currently open files
+ *
+ * This table maps POSIX fd numbers to vfs_file_t instances
+ *
+ * @attention STDIN, STDOUT, STDERR will use the three first items in this array.
+ */
+static vfs_file_t _vfs_open_files[VFS_MAX_OPEN_FILES];
+
+/**
+ * @internal
+ * @brief List handle for list of all currently mounted file systems
+ *
+ * This singly linked list is used to dispatch vfs calls to the appropriate file
+ * system driver.
+ */
+static clist_node_t _vfs_mounts_list;
+
+/**
+ * @internal
+ * @brief Find an unused entry in the _vfs_open_files array and mark it as used
+ *
+ * If the @p fd argument is non-negative, the allocation fails if the
+ * corresponding slot in the open files table is already occupied, no iteration
+ * is done to find another free number in this case.
+ *
+ * If the @p fd argument is negative, the algorithm will iterate through the
+ * open files table until it find an unused slot and return the number of that
+ * slot.
+ *
+ * @param[in]  fd  Desired fd number, use VFS_ANY_FD for any free fd
+ *
+ * @return fd on success
+ * @return <0 on error
+ */
+inline static int _allocate_fd(int fd);
+
+/**
+ * @internal
+ * @brief Mark an allocated entry as unused in the _vfs_open_files array
+ *
+ * @param[in]  fd     fd to free
+ */
+inline static void _free_fd(int fd);
+
+/**
+ * @internal
+ * @brief Initialize an entry in the _vfs_open_files array and mark it as used.
+ *
+ * @param[in]  fd           desired fd number, passed to _allocate_fd
+ * @param[in]  f_op         pointer to file operations table
+ * @param[in]  mountp       pointer to mount table entry, may be NULL
+ * @param[in]  flags        file flags
+ * @param[in]  private_data private_data initial value
+ *
+ * @return fd on success
+ * @return <0 on error
+ */
+inline static int _init_fd(int fd, const vfs_file_ops_t *f_op, vfs_mount_t *mountp, int flags, void *private_data);
+
+/**
+ * @internal
+ * @brief Find the file system associated with the file name @p name, and
+ * increment the open_files counter
+ *
+ * A pointer to the vfs_mount_t associated with the found mount will be written to @p mountpp.
+ * A pointer to the mount point-relative file name will be written to @p rel_path.
+ *
+ * @param[out] mountpp   write address of the found mount to this pointer
+ * @param[in]  name      absolute path to file
+ * @param[out] rel_path  (optional) output pointer for relative path
+ *
+ * @return mount index on success
+ * @return <0 on error
+ */
+inline static int _find_mount(vfs_mount_t **mountpp, const char *name, const char **rel_path);
+
+/**
+ * @internal
+ * @brief Check that a given fd number is valid
+ *
+ * @param[in]  fd    fd to check
+ *
+ * @return 0 if the fd is valid
+ * @return <0 if the fd is not valid
+ */
+inline static int _fd_is_valid(int fd);
+
+static mutex_t _mount_mutex = MUTEX_INIT;
+static mutex_t _open_mutex = MUTEX_INIT;
+
+int vfs_close(int fd)
+{
+    DEBUG("vfs_close: %d\n", fd);
+    int res = _fd_is_valid(fd);
+    if (res < 0) {
+        return res;
+    }
+    vfs_file_t *filp = &_vfs_open_files[fd];
+    if (filp->f_op->close != NULL) {
+        /* We will invalidate the fd regardless of the outcome of the file
+         * system driver close() call below */
+        res = filp->f_op->close(filp);
+    }
+    _free_fd(fd);
+    return res;
+}
+
+int vfs_fcntl(int fd, int cmd, int arg)
+{
+    DEBUG("vfs_fcntl: %d, %d, %d\n", fd, cmd, arg);
+    int res = _fd_is_valid(fd);
+    if (res < 0) {
+        return res;
+    }
+    vfs_file_t *filp = &_vfs_open_files[fd];
+    /* The default fcntl implementation below only allows querying flags,
+     * any other command requires insight into the file system driver */
+    switch (cmd) {
+        case F_GETFL:
+            /* Get file flags */
+            DEBUG("vfs_fcntl: GETFL: %d\n", filp->flags);
+            return filp->flags;
+        default:
+            break;
+    }
+    /* pass on to file system driver */
+    if (filp->f_op->fcntl != NULL) {
+        return filp->f_op->fcntl(filp, cmd, arg);
+    }
+    return -EINVAL;
+}
+
+int vfs_fstat(int fd, struct stat *buf)
+{
+    DEBUG_NOT_STDOUT(fd, "vfs_fstat: %d, %p\n", fd, (void *)buf);
+    if (buf == NULL) {
+        return -EFAULT;
+    }
+    int res = _fd_is_valid(fd);
+    if (res < 0) {
+        return res;
+    }
+    vfs_file_t *filp = &_vfs_open_files[fd];
+    if (filp->f_op->fstat == NULL) {
+        /* driver does not implement fstat() */
+        return -EINVAL;
+    }
+    return filp->f_op->fstat(filp, buf);
+}
+
+int vfs_fstatvfs(int fd, struct statvfs *buf)
+{
+    DEBUG("vfs_fstatvfs: %d, %p\n", fd, (void *)buf);
+    if (buf == NULL) {
+        return -EFAULT;
+    }
+    int res = _fd_is_valid(fd);
+    if (res < 0) {
+        return res;
+    }
+    vfs_file_t *filp = &_vfs_open_files[fd];
+    if (filp->mp->fs->fs_op->fstatvfs == NULL) {
+        /* file system driver does not implement fstatvfs() */
+        if (filp->mp->fs->fs_op->statvfs != NULL) {
+            /* Fall back to statvfs */
+            return filp->mp->fs->fs_op->statvfs(filp->mp, "/", buf);
+        }
+        return -EINVAL;
+    }
+    return filp->mp->fs->fs_op->fstatvfs(filp->mp, filp, buf);
+}
+
+off_t vfs_lseek(int fd, off_t off, int whence)
+{
+    DEBUG("vfs_lseek: %d, %ld, %d\n", fd, (long)off, whence);
+    int res = _fd_is_valid(fd);
+    if (res < 0) {
+        return res;
+    }
+    vfs_file_t *filp = &_vfs_open_files[fd];
+    if (filp->f_op->lseek == NULL) {
+        /* driver does not implement lseek() */
+        /* default seek functionality is naive */
+        switch (whence) {
+            case SEEK_SET:
+                break;
+            case SEEK_CUR:
+                off += filp->pos;
+                break;
+            case SEEK_END:
+                /* we could use fstat here, but most file system drivers will
+                 * likely already implement lseek in a more efficient fashion */
+                return -EINVAL;
+            default:
+                return -EINVAL;
+        }
+        if (off < 0) {
+            /* the resulting file offset would be negative */
+            return -EINVAL;
+        }
+        filp->pos = off;
+
+        return off;
+    }
+    return filp->f_op->lseek(filp, off, whence);
+}
+
+int vfs_open(const char *name, int flags, mode_t mode)
+{
+    DEBUG("vfs_open: \"%s\", 0x%x, 0%03lo\n", name, flags, (long unsigned int)mode);
+    if (name == NULL) {
+        return -EINVAL;
+    }
+    const char *rel_path;
+    vfs_mount_t *mountp;
+    int res = _find_mount(&mountp, name, &rel_path);
+    /* _find_mount implicitly increments the open_files count on success */
+    if (res < 0) {
+        /* No mount point maps to the requested file name */
+        DEBUG("vfs_open: no matching mount\n");
+        return res;
+    }
+    mutex_lock(&_open_mutex);
+    int fd = _init_fd(VFS_ANY_FD, mountp->fs->f_op, mountp, flags, NULL);
+    mutex_unlock(&_open_mutex);
+    if (fd < 0) {
+        DEBUG("vfs_open: _init_fd: ERR %d!\n", fd);
+        /* remember to decrement the open_files count */
+        atomic_fetch_sub(&mountp->open_files, 1);
+        return fd;
+    }
+    vfs_file_t *filp = &_vfs_open_files[fd];
+    if (filp->f_op->open != NULL) {
+        res = filp->f_op->open(filp, rel_path, flags, mode, name);
+        if (res < 0) {
+            /* something went wrong during open */
+            DEBUG("vfs_open: open: ERR %d!\n", res);
+            /* clean up */
+            _free_fd(fd);
+            return res;
+        }
+    }
+    DEBUG("vfs_open: opened %d\n", fd);
+    return fd;
+}
+
+ssize_t vfs_read(int fd, void *dest, size_t count)
+{
+    DEBUG("vfs_read: %d, %p, %lu\n", fd, dest, (unsigned long)count);
+    if (dest == NULL) {
+        return -EFAULT;
+    }
+    int res = _fd_is_valid(fd);
+    if (res < 0) {
+        return res;
+    }
+    vfs_file_t *filp = &_vfs_open_files[fd];
+    if (((filp->flags & O_ACCMODE) != O_RDONLY) & ((filp->flags & O_ACCMODE) != O_RDWR)) {
+        /* File not open for reading */
+        return -EBADF;
+    }
+    if (filp->f_op->read == NULL) {
+        /* driver does not implement read() */
+        return -EINVAL;
+    }
+    return filp->f_op->read(filp, dest, count);
+}
+
+
+ssize_t vfs_write(int fd, const void *src, size_t count)
+{
+    DEBUG_NOT_STDOUT(fd, "vfs_write: %d, %p, %lu\n", fd, src, (unsigned long)count);
+    if (src == NULL) {
+        return -EFAULT;
+    }
+    int res = _fd_is_valid(fd);
+    if (res < 0) {
+        return res;
+    }
+    vfs_file_t *filp = &_vfs_open_files[fd];
+    if (((filp->flags & O_ACCMODE) != O_WRONLY) & ((filp->flags & O_ACCMODE) != O_RDWR)) {
+        /* File not open for writing */
+        return -EBADF;
+    }
+    if (filp->f_op->write == NULL) {
+        /* driver does not implement write() */
+        return -EINVAL;
+    }
+    return filp->f_op->write(filp, src, count);
+}
+
+int vfs_opendir(vfs_DIR *dirp, const char *dirname)
+{
+    DEBUG("vfs_opendir: %p, \"%s\"\n", (void *)dirp, dirname);
+    if ((dirp == NULL) || (dirname == NULL)) {
+        return -EINVAL;
+    }
+    const char *rel_path;
+    vfs_mount_t *mountp;
+    int res = _find_mount(&mountp, dirname, &rel_path);
+    /* _find_mount implicitly increments the open_files count on success */
+    if (res < 0) {
+        /* No mount point maps to the requested file name */
+        DEBUG("vfs_open: no matching mount\n");
+        return res;
+    }
+    if (rel_path[0] == '\0') {
+        /* if the trailing slash is missing we will get an empty string back, to
+         * be consistent against the file system drivers we give the relative
+         * path "/" instead */
+        rel_path = "/";
+    }
+    if (mountp->fs->d_op == NULL) {
+        /* file system driver does not support directories */
+        return -EINVAL;
+    }
+    /* initialize dirp */
+    memset(dirp, 0, sizeof(*dirp));
+    dirp->mp = mountp;
+    dirp->d_op = mountp->fs->d_op;
+    if (dirp->d_op->opendir != NULL) {
+        int res = dirp->d_op->opendir(dirp, rel_path, dirname);
+        if (res < 0) {
+            /* remember to decrement the open_files count */
+            atomic_fetch_sub(&mountp->open_files, 1);
+            return res;
+        }
+    }
+    return 0;
+}
+
+int vfs_readdir(vfs_DIR *dirp, vfs_dirent_t *entry)
+{
+    DEBUG("vfs_readdir: %p, %p\n", (void *)dirp, (void *)entry);
+    if ((dirp == NULL) || (entry == NULL)) {
+        return -EINVAL;
+    }
+    if (dirp->d_op != NULL) {
+        if (dirp->d_op->readdir != NULL) {
+            return dirp->d_op->readdir(dirp, entry);
+        }
+    }
+    return -EINVAL;
+}
+
+int vfs_closedir(vfs_DIR *dirp)
+{
+    DEBUG("vfs_closedir: %p\n", (void *)dirp);
+    if (dirp == NULL) {
+        return -EINVAL;
+    }
+    vfs_mount_t *mountp = dirp->mp;
+    if (mountp == NULL) {
+        return -EBADF;
+    }
+    int res = 0;
+    if (dirp->d_op != NULL) {
+        if (dirp->d_op->closedir != NULL) {
+            res = dirp->d_op->closedir(dirp);
+        }
+    }
+    memset(dirp, 0, sizeof(*dirp));
+    atomic_fetch_sub(&mountp->open_files, 1);
+    return res;
+}
+
+int vfs_mount(vfs_mount_t *mountp)
+{
+    DEBUG("vfs_mount: %p\n", (void *)mountp);
+    if ((mountp == NULL) || (mountp->fs == NULL) || (mountp->mount_point == NULL)) {
+        return -EINVAL;
+    }
+    DEBUG("vfs_mount: -> \"%s\" (%p), %p\n", mountp->mount_point, (void *)mountp->mount_point, mountp->private_data);
+    if (mountp->mount_point[0] != '/') {
+        DEBUG("vfs_mount: not absolute mount_point path\n");
+        return -EINVAL;
+    }
+    mountp->mount_point_len = strlen(mountp->mount_point);
+    mutex_lock(&_mount_mutex);
+    /* Check for the same mount in the list of mounts to avoid loops */
+    clist_node_t *found = clist_find(&_vfs_mounts_list, &mountp->list_entry);
+    if (found != NULL) {
+        /* Same mount is already mounted */
+        mutex_unlock(&_mount_mutex);
+        DEBUG("vfs_mount: Already mounted\n");
+        return -EBUSY;
+    }
+    if (mountp->fs->fs_op != NULL) {
+        if (mountp->fs->fs_op->mount != NULL) {
+            /* yes, a file system driver does not need to implement mount/umount */
+            int res = mountp->fs->fs_op->mount(mountp);
+            if (res < 0) {
+                mutex_unlock(&_mount_mutex);
+                return res;
+            }
+        }
+    }
+    /* insert last in list */
+    clist_rpush(&_vfs_mounts_list, &mountp->list_entry);
+    mutex_unlock(&_mount_mutex);
+    DEBUG("vfs_mount: mount done\n");
+    return 0;
+}
+
+
+int vfs_umount(vfs_mount_t *mountp)
+{
+    DEBUG("vfs_umount: %p\n", (void *)mountp);
+    if ((mountp == NULL) || (mountp->mount_point == NULL)) {
+        return -EINVAL;
+    }
+    mutex_lock(&_mount_mutex);
+    DEBUG("vfs_umount: -> \"%s\" open=%d\n", mountp->mount_point, atomic_load(&mountp->open_files));
+    if (atomic_load(&mountp->open_files) > 0) {
+        mutex_unlock(&_mount_mutex);
+        return -EBUSY;
+    }
+    if (mountp->fs->fs_op != NULL) {
+        if (mountp->fs->fs_op->umount != NULL) {
+            int res = mountp->fs->fs_op->umount(mountp);
+            if (res < 0) {
+                /* umount failed */
+                DEBUG("vfs_umount: ERR %d!\n", res);
+                mutex_unlock(&_mount_mutex);
+                return res;
+            }
+        }
+    }
+    /* find mountp in the list and remove it */
+    clist_node_t *node = clist_remove(&_vfs_mounts_list, &mountp->list_entry);
+    if (node == NULL) {
+        /* not found */
+        DEBUG("vfs_umount: ERR not mounted!\n");
+        mutex_unlock(&_mount_mutex);
+        return -EINVAL;
+    }
+    mutex_unlock(&_mount_mutex);
+    return 0;
+}
+
+int vfs_rename(const char *from_path, const char *to_path)
+{
+    DEBUG("vfs_rename: \"%s\", \"%s\"\n", from_path, to_path);
+    if ((from_path == NULL) || (to_path == NULL)) {
+        return -EINVAL;
+    }
+    const char *rel_from;
+    vfs_mount_t *mountp;
+    int res = _find_mount(&mountp, from_path, &rel_from);
+    /* _find_mount implicitly increments the open_files count on success */
+    if (res < 0) {
+        /* No mount point maps to the requested file name */
+        DEBUG("vfs_rename: from: no matching mount\n");
+        return res;
+    }
+    if ((mountp->fs->fs_op == NULL) || (mountp->fs->fs_op->rename == NULL)) {
+        /* rename not supported */
+        DEBUG("vfs_rename: rename not supported by fs!\n");
+        /* remember to decrement the open_files count */
+        atomic_fetch_sub(&mountp->open_files, 1);
+        return -EPERM;
+    }
+    const char *rel_to;
+    vfs_mount_t *mountp_to;
+    res = _find_mount(&mountp_to, to_path, &rel_to);
+    /* _find_mount implicitly increments the open_files count on success */
+    if (res < 0) {
+        /* No mount point maps to the requested file name */
+        DEBUG("vfs_rename: to: no matching mount\n");
+        /* remember to decrement the open_files count */
+        atomic_fetch_sub(&mountp->open_files, 1);
+        return res;
+    }
+    if (mountp_to != mountp) {
+        /* The paths are on different file systems */
+        DEBUG("vfs_rename: from_path and to_path are on different mounts\n");
+        /* remember to decrement the open_files count */
+        atomic_fetch_sub(&mountp->open_files, 1);
+        atomic_fetch_sub(&mountp_to->open_files, 1);
+        return -EXDEV;
+    }
+    res = mountp->fs->fs_op->rename(mountp, rel_from, rel_to);
+    DEBUG("vfs_rename: rename %p, \"%s\" -> \"%s\"", (void *)mountp, rel_from, rel_to);
+    if (res < 0) {
+        /* something went wrong during rename */
+        DEBUG(": ERR %d!\n", res);
+    }
+    else {
+        DEBUG("\n");
+    }
+    /* remember to decrement the open_files count */
+    atomic_fetch_sub(&mountp->open_files, 1);
+    atomic_fetch_sub(&mountp_to->open_files, 1);
+    return res;
+}
+
+/* TODO: Share code between vfs_unlink, vfs_mkdir, vfs_rmdir since they are almost identical */
+
+int vfs_unlink(const char *name)
+{
+    DEBUG("vfs_unlink: \"%s\"\n", name);
+    if (name == NULL) {
+        return -EINVAL;
+    }
+    const char *rel_path;
+    vfs_mount_t *mountp;
+    int res;
+    res = _find_mount(&mountp, name, &rel_path);
+    /* _find_mount implicitly increments the open_files count on success */
+    if (res < 0) {
+        /* No mount point maps to the requested file name */
+        DEBUG("vfs_unlink: no matching mount\n");
+        return res;
+    }
+    if ((mountp->fs->fs_op == NULL) || (mountp->fs->fs_op->unlink == NULL)) {
+        /* unlink not supported */
+        DEBUG("vfs_unlink: unlink not supported by fs!\n");
+        /* remember to decrement the open_files count */
+        atomic_fetch_sub(&mountp->open_files, 1);
+        return -EPERM;
+    }
+    res = mountp->fs->fs_op->unlink(mountp, rel_path);
+    DEBUG("vfs_unlink: unlink %p, \"%s\"", (void *)mountp, rel_path);
+    if (res < 0) {
+        /* something went wrong during unlink */
+        DEBUG(": ERR %d!\n", res);
+    }
+    else {
+        DEBUG("\n");
+    }
+    /* remember to decrement the open_files count */
+    atomic_fetch_sub(&mountp->open_files, 1);
+    return res;
+}
+
+int vfs_mkdir(const char *name, mode_t mode)
+{
+    DEBUG("vfs_mkdir: \"%s\", 0%03lo\n", name, (long unsigned int)mode);
+    if (name == NULL) {
+        return -EINVAL;
+    }
+    const char *rel_path;
+    vfs_mount_t *mountp;
+    int res;
+    res = _find_mount(&mountp, name, &rel_path);
+    /* _find_mount implicitly increments the open_files count on success */
+    if (res < 0) {
+        /* No mount point maps to the requested file name */
+        DEBUG("vfs_mkdir: no matching mount\n");
+        return res;
+    }
+    if ((mountp->fs->fs_op == NULL) || (mountp->fs->fs_op->mkdir == NULL)) {
+        /* mkdir not supported */
+        DEBUG("vfs_mkdir: mkdir not supported by fs!\n");
+        /* remember to decrement the open_files count */
+        atomic_fetch_sub(&mountp->open_files, 1);
+        return -EPERM;
+    }
+    res = mountp->fs->fs_op->mkdir(mountp, rel_path, mode);
+    DEBUG("vfs_mkdir: mkdir %p, \"%s\"", (void *)mountp, rel_path);
+    if (res < 0) {
+        /* something went wrong during mkdir */
+        DEBUG(": ERR %d!\n", res);
+    }
+    else {
+        DEBUG("\n");
+    }
+    /* remember to decrement the open_files count */
+    atomic_fetch_sub(&mountp->open_files, 1);
+    return res;
+}
+
+int vfs_rmdir(const char *name)
+{
+    DEBUG("vfs_rmdir: \"%s\"\n", name);
+    if (name == NULL) {
+        return -EINVAL;
+    }
+    const char *rel_path;
+    vfs_mount_t *mountp;
+    int res;
+    res = _find_mount(&mountp, name, &rel_path);
+    /* _find_mount implicitly increments the open_files count on success */
+    if (res < 0) {
+        /* No mount point maps to the requested file name */
+        DEBUG("vfs_rmdir: no matching mount\n");
+        return res;
+    }
+    if ((mountp->fs->fs_op == NULL) || (mountp->fs->fs_op->rmdir == NULL)) {
+        /* rmdir not supported */
+        DEBUG("vfs_rmdir: rmdir not supported by fs!\n");
+        /* remember to decrement the open_files count */
+        atomic_fetch_sub(&mountp->open_files, 1);
+        return -EPERM;
+    }
+    res = mountp->fs->fs_op->rmdir(mountp, rel_path);
+    DEBUG("vfs_rmdir: rmdir %p, \"%s\"", (void *)mountp, rel_path);
+    if (res < 0) {
+        /* something went wrong during rmdir */
+        DEBUG(": ERR %d!\n", res);
+    }
+    else {
+        DEBUG("\n");
+    }
+    /* remember to decrement the open_files count */
+    atomic_fetch_sub(&mountp->open_files, 1);
+    return res;
+}
+
+int vfs_stat(const char *restrict path, struct stat *restrict buf)
+{
+    DEBUG("vfs_stat: \"%s\", %p\n", path, (void *)buf);
+    if (path == NULL || buf == NULL) {
+        return -EINVAL;
+    }
+    const char *rel_path;
+    vfs_mount_t *mountp;
+    int res;
+    res = _find_mount(&mountp, path, &rel_path);
+    /* _find_mount implicitly increments the open_files count on success */
+    if (res < 0) {
+        /* No mount point maps to the requested file name */
+        DEBUG("vfs_stat: no matching mount\n");
+        return res;
+    }
+    if ((mountp->fs->fs_op == NULL) || (mountp->fs->fs_op->stat == NULL)) {
+        /* stat not supported */
+        DEBUG("vfs_stat: stat not supported by fs!\n");
+        /* remember to decrement the open_files count */
+        atomic_fetch_sub(&mountp->open_files, 1);
+        return -EPERM;
+    }
+    res = mountp->fs->fs_op->stat(mountp, rel_path, buf);
+    /* remember to decrement the open_files count */
+    atomic_fetch_sub(&mountp->open_files, 1);
+    return res;
+}
+
+int vfs_statvfs(const char *restrict path, struct statvfs *restrict buf)
+{
+    DEBUG("vfs_statvfs: \"%s\", %p\n", path, (void *)buf);
+    if (path == NULL || buf == NULL) {
+        return -EINVAL;
+    }
+    const char *rel_path;
+    vfs_mount_t *mountp;
+    int res;
+    res = _find_mount(&mountp, path, &rel_path);
+    /* _find_mount implicitly increments the open_files count on success */
+    if (res < 0) {
+        /* No mount point maps to the requested file name */
+        DEBUG("vfs_statvfs: no matching mount\n");
+        return res;
+    }
+    if ((mountp->fs->fs_op == NULL) || (mountp->fs->fs_op->statvfs == NULL)) {
+        /* statvfs not supported */
+        DEBUG("vfs_statvfs: statvfs not supported by fs!\n");
+        /* remember to decrement the open_files count */
+        atomic_fetch_sub(&mountp->open_files, 1);
+        return -EPERM;
+    }
+    res = mountp->fs->fs_op->statvfs(mountp, rel_path, buf);
+    /* remember to decrement the open_files count */
+    atomic_fetch_sub(&mountp->open_files, 1);
+    return res;
+}
+
+int vfs_bind(int fd, int flags, const vfs_file_ops_t *f_op, void *private_data)
+{
+    DEBUG("vfs_bind: %d, %d, %p, %p\n", fd, flags, (void*)f_op, private_data);
+    if (f_op == NULL) {
+        return -EINVAL;
+    }
+    mutex_lock(&_open_mutex);
+    fd = _init_fd(fd, f_op, NULL, flags, private_data);
+    mutex_unlock(&_open_mutex);
+    if (fd < 0) {
+        DEBUG("vfs_bind: _init_fd: ERR %d!\n", fd);
+        return fd;
+    }
+    DEBUG("vfs_bind: bound %d\n", fd);
+    return fd;
+}
+
+int vfs_normalize_path(char *buf, const char *path, size_t buflen)
+{
+    DEBUG("vfs_normalize_path: %p, \"%s\" (%p), %lu\n", buf, path, path, (unsigned long)buflen);
+    size_t len = 0;
+    int npathcomp = 0;
+    const char *path_end = path + strlen(path); /* Find the terminating null byte */
+    if (len >= buflen) {
+        return -ENAMETOOLONG;
+    }
+
+    while(path <= path_end) {
+        DEBUG("vfs_normalize_path: + %d \"%.*s\" <- \"%s\" (%p)\n", npathcomp, len, buf, path, path);
+        if (path[0] == '\0') {
+            break;
+        }
+        while (path[0] == '/') {
+            /* skip extra slashes */
+            ++path;
+        }
+        if (path[0] == '.') {
+            ++path;
+            if (path[0] == '/' || path[0] == '\0') {
+                /* skip /./ components */
+                DEBUG("vfs_normalize_path: skip .\n");
+                continue;
+            }
+            if (path[0] == '.' && (path[1] == '/' || path[1] == '\0')) {
+                DEBUG("vfs_normalize_path: reduce ../\n");
+                if (len == 0) {
+                    /* outside root */
+                    return -EINVAL;
+                }
+                ++path;
+                /* delete the last component of the path */
+                while (len > 0 && buf[--len] != '/') {}
+                --npathcomp;
+                continue;
+            }
+        }
+        buf[len++] = '/';
+        if (len >= buflen) {
+            return -ENAMETOOLONG;
+        }
+        if (path[0] == '\0') {
+            /* trailing slash in original path, don't increment npathcomp */
+            break;
+        }
+        ++npathcomp;
+        /* copy the path component */
+        while (len < buflen && path[0] != '/' && path[0] != '\0') {
+            buf[len++] = path[0];
+            ++path;
+        }
+        if (len >= buflen) {
+            return -ENAMETOOLONG;
+        }
+    }
+    /* special case for "/": (otherwise it will be zero) */
+    if (len == 1) {
+        npathcomp = 1;
+    }
+    buf[len] = '\0';
+    DEBUG("vfs_normalize_path: = %d, \"%s\"\n", npathcomp, buf);
+    return npathcomp;
+}
+
+const vfs_mount_t *vfs_iterate_mounts(const vfs_mount_t *cur)
+{
+    clist_node_t *node;
+    if (cur == NULL) {
+        node = _vfs_mounts_list.next;
+        if (node == NULL) {
+            /* empty list */
+            return NULL;
+        }
+    }
+    else {
+        node = cur->list_entry.next;
+        if (node == _vfs_mounts_list.next) {
+            return NULL;
+        }
+    }
+    return container_of(node, vfs_mount_t, list_entry);
+}
+
+inline static int _allocate_fd(int fd)
+{
+    if (fd < 0) {
+        for (fd = 0; fd < VFS_MAX_OPEN_FILES; ++fd) {
+            if (_vfs_open_files[fd].pid == KERNEL_PID_UNDEF) {
+                break;
+            }
+        }
+        if (fd >= VFS_MAX_OPEN_FILES) {
+            /* The _vfs_open_files array is full */
+            return -ENFILE;
+        }
+    }
+    else if (_vfs_open_files[fd].pid != KERNEL_PID_UNDEF) {
+        /* The desired fd is already in use */
+        return -EEXIST;
+    }
+    kernel_pid_t pid = thread_getpid();
+    if (pid == KERNEL_PID_UNDEF) {
+        /* This happens when calling vfs_bind during boot, before threads have
+         * been started. */
+        pid = -1;
+    }
+    _vfs_open_files[fd].pid = pid;
+    return fd;
+}
+
+inline static void _free_fd(int fd)
+{
+    DEBUG("_free_fd: %d, pid=%d\n", fd, _vfs_open_files[fd].pid);
+    if (_vfs_open_files[fd].mp != NULL) {
+        atomic_fetch_sub(&_vfs_open_files[fd].mp->open_files, 1);
+    }
+    _vfs_open_files[fd].pid = KERNEL_PID_UNDEF;
+}
+
+inline static int _init_fd(int fd, const vfs_file_ops_t *f_op, vfs_mount_t *mountp, int flags, void *private_data)
+{
+    fd = _allocate_fd(fd);
+    if (fd < 0) {
+        return fd;
+    }
+    vfs_file_t *filp = &_vfs_open_files[fd];
+    filp->mp = mountp;
+    filp->f_op = f_op;
+    filp->flags = flags;
+    filp->pos = 0;
+    filp->private_data.ptr = private_data;
+    return fd;
+}
+
+inline static int _find_mount(vfs_mount_t **mountpp, const char *name, const char **rel_path)
+{
+    size_t longest_match = 0;
+    size_t name_len = strlen(name);
+    mutex_lock(&_mount_mutex);
+
+    clist_node_t *node = _vfs_mounts_list.next;
+    if (node == NULL) {
+        /* list empty */
+        mutex_unlock(&_mount_mutex);
+        return -ENOENT;
+    }
+    vfs_mount_t *mountp = NULL;
+    do {
+        node = node->next;
+        vfs_mount_t *it = container_of(node, vfs_mount_t, list_entry);
+        size_t len = it->mount_point_len;
+        if (len < longest_match) {
+            /* Already found a longer prefix */
+            continue;
+        }
+        if (len > name_len) {
+            /* path name is shorter than the mount point name */
+            continue;
+        }
+        if ((len > 1) && (name[len] != '/') && (name[len] != '\0')) {
+            /* name does not have a directory separator where mount point name ends */
+            continue;
+        }
+        if (strncmp(name, it->mount_point, len) == 0) {
+            /* mount_point is a prefix of name */
+            /* special check for mount_point == "/" */
+            if (len > 1) {
+                longest_match = len;
+            }
+            mountp = it;
+        }
+    } while (node != _vfs_mounts_list.next);
+    if (mountp == NULL) {
+        /* not found */
+        mutex_unlock(&_mount_mutex);
+        return -ENOENT;
+    }
+    /* Increment open files counter for this mount */
+    atomic_fetch_add(&mountp->open_files, 1);
+    mutex_unlock(&_mount_mutex);
+    *mountpp = mountp;
+    if (rel_path != NULL) {
+        *rel_path = name + longest_match;
+    }
+    return 0;
+}
+
+inline static int _fd_is_valid(int fd)
+{
+    if ((unsigned int)fd >= VFS_MAX_OPEN_FILES) {
+        return -EBADF;
+    }
+    vfs_file_t *filp = &_vfs_open_files[fd];
+    if (filp->pid == KERNEL_PID_UNDEF) {
+        return -EBADF;
+    }
+    if (filp->f_op == NULL) {
+        return -EBADF;
+    }
+    return 0;
+}
+
+/** @} */
diff --git a/tests/unittests/tests-vfs/Makefile b/tests/unittests/tests-vfs/Makefile
new file mode 100644
index 0000000000..48422e909a
--- /dev/null
+++ b/tests/unittests/tests-vfs/Makefile
@@ -0,0 +1 @@
+include $(RIOTBASE)/Makefile.base
diff --git a/tests/unittests/tests-vfs/Makefile.include b/tests/unittests/tests-vfs/Makefile.include
new file mode 100644
index 0000000000..f35c90d855
--- /dev/null
+++ b/tests/unittests/tests-vfs/Makefile.include
@@ -0,0 +1,2 @@
+USEMODULE += vfs
+USEMODULE += constfs
diff --git a/tests/unittests/tests-vfs/tests-vfs-bind.c b/tests/unittests/tests-vfs/tests-vfs-bind.c
new file mode 100644
index 0000000000..ddce730fe3
--- /dev/null
+++ b/tests/unittests/tests-vfs/tests-vfs-bind.c
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2016 Eistec AB
+ *
+ * This file is subject to the terms and conditions of the GNU Lesser
+ * General Public License v2.1. See the file LICENSE in the top level
+ * directory for more details.
+ */
+
+/**
+ * @{
+ *
+ * @file
+ * @brief       Unittests for vfs_bind
+ *
+ * @author      Joakim Nohlgård <joakim.nohlgard@eistec.se>
+ */
+#include <errno.h>
+#include <stdint.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+#include "embUnit/embUnit.h"
+
+#include "vfs.h"
+
+#include "tests-vfs.h"
+
+static const uint8_t str_data[] = "This is a test file";
+                                /* 01234567890123456789 */
+                                /* 0         1          */
+
+#define _VFS_TEST_BIND_BUFSIZE 8
+
+static ssize_t _mock_write(vfs_file_t *filp, const void *src, size_t nbytes);
+static ssize_t _mock_read(vfs_file_t *filp, void *dest, size_t nbytes);
+
+static volatile int _mock_write_calls = 0;
+static volatile int _mock_read_calls = 0;
+
+static vfs_file_ops_t _test_bind_ops = {
+    .read = _mock_read,
+    .write = _mock_write,
+};
+
+static ssize_t _mock_write(vfs_file_t *filp, const void *src, size_t nbytes)
+{
+    void *dest = filp->private_data.ptr;
+    ++_mock_write_calls;
+    if (nbytes > _VFS_TEST_BIND_BUFSIZE) {
+        nbytes = _VFS_TEST_BIND_BUFSIZE;
+    }
+    memcpy(dest, src, nbytes);
+    return nbytes;
+}
+
+static ssize_t _mock_read(vfs_file_t *filp, void *dest, size_t nbytes)
+{
+    void *src = filp->private_data.ptr;
+    ++_mock_read_calls;
+    if (nbytes > _VFS_TEST_BIND_BUFSIZE) {
+        nbytes = _VFS_TEST_BIND_BUFSIZE;
+    }
+    memcpy(dest, src, nbytes);
+    return nbytes;
+}
+
+static void test_vfs_bind(void)
+{
+    int fd;
+    uint8_t buf[_VFS_TEST_BIND_BUFSIZE];
+    fd = vfs_bind(VFS_ANY_FD, O_RDWR, &_test_bind_ops, &buf[0]);
+    TEST_ASSERT(fd >= 0);
+    if (fd < 0) {
+        return;
+    }
+
+    ssize_t nbytes;
+    int ncalls = _mock_write_calls;
+    nbytes = vfs_write(fd, &str_data[0], sizeof(str_data));
+    TEST_ASSERT_EQUAL_INT(_mock_write_calls, ncalls + 1);
+    TEST_ASSERT_EQUAL_INT(_VFS_TEST_BIND_BUFSIZE, nbytes);
+    TEST_ASSERT_EQUAL_INT(0, memcmp(&str_data[0], &buf[0], nbytes));
+
+    char strbuf[64];
+    memset(strbuf, '\0', sizeof(strbuf));
+    ncalls = _mock_read_calls;
+    nbytes = vfs_read(fd, strbuf, sizeof(strbuf));
+    TEST_ASSERT_EQUAL_INT(_mock_read_calls, ncalls + 1);
+    TEST_ASSERT_EQUAL_INT(_VFS_TEST_BIND_BUFSIZE, nbytes);
+    TEST_ASSERT_EQUAL_INT(0, memcmp(&buf[0], &strbuf[0], nbytes));
+
+    int res = vfs_close(fd);
+    TEST_ASSERT_EQUAL_INT(0, res);
+}
+
+static void test_vfs_bind__leak_fds(void)
+{
+    /* This test was added after a bug was discovered in the _allocate_fd code to
+     * make sure that fds are not leaked when doing multiple bind/close calls */
+    /* Try >VFS_MAX_OPEN_FILES times to open and close fds to see that they are
+     * not leaked */
+    for (unsigned int i = 0; i < VFS_MAX_OPEN_FILES; ++i) {
+        test_vfs_bind();
+    }
+    /* The following will fail if the fds above are not properly freed */
+    test_vfs_bind();
+}
+
+Test *tests_vfs_bind_tests(void)
+{
+    EMB_UNIT_TESTFIXTURES(fixtures) {
+        new_TestFixture(test_vfs_bind),
+        new_TestFixture(test_vfs_bind__leak_fds),
+    };
+
+    EMB_UNIT_TESTCALLER(vfs_bind_tests, NULL, NULL, fixtures);
+
+    return (Test *)&vfs_bind_tests;
+}
+
+/** @} */
diff --git a/tests/unittests/tests-vfs/tests-vfs-dir-ops.c b/tests/unittests/tests-vfs/tests-vfs-dir-ops.c
new file mode 100644
index 0000000000..fa56a04ff7
--- /dev/null
+++ b/tests/unittests/tests-vfs/tests-vfs-dir-ops.c
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2016 Eistec AB
+ *
+ * This file is subject to the terms and conditions of the GNU Lesser
+ * General Public License v2.1. See the file LICENSE in the top level
+ * directory for more details.
+ */
+
+/**
+ * @{
+ *
+ * @file
+ * @brief Transparent-box unit tests of vfs functions stubs used when the file
+ * system does not implement the requested function.
+ */
+#include <errno.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "embUnit/embUnit.h"
+
+#include "vfs.h"
+
+#include "tests-vfs.h"
+
+static const vfs_file_system_ops_t null_fs_ops = {
+    .mount   = NULL,
+    .umount  = NULL,
+    .unlink  = NULL,
+    .statvfs = NULL,
+    .stat    = NULL,
+};
+
+static const vfs_file_ops_t null_file_ops = {
+    .close = NULL,
+    .fstat = NULL,
+    .lseek = NULL,
+    .open  = NULL,
+    .read  = NULL,
+    .write = NULL,
+};
+
+static const vfs_dir_ops_t null_dir_ops = {
+    .opendir  = NULL,
+    .readdir  = NULL,
+    .closedir = NULL,
+};
+
+static const vfs_file_system_t null_file_system = {
+    .f_op  = &null_file_ops,
+    .fs_op = &null_fs_ops,
+    .d_op  = &null_dir_ops,
+};
+
+static vfs_mount_t _test_vfs_mount_null = {
+    .mount_point = "/test",
+    .fs = &null_file_system,
+    .private_data = NULL,
+};
+
+static int _test_vfs_dir_op_status = -1;
+static vfs_DIR _test_dir;
+
+static void setup(void)
+{
+    int res = vfs_mount(&_test_vfs_mount_null);
+    if (res < 0) {
+        _test_vfs_dir_op_status = -1;
+        return;
+    }
+    _test_vfs_dir_op_status = vfs_opendir(&_test_dir, "/test/mydir");
+}
+
+static void teardown(void)
+{
+    if (_test_vfs_dir_op_status >= 0) {
+        vfs_closedir(&_test_dir);
+        _test_vfs_dir_op_status = -1;
+    }
+    vfs_umount(&_test_vfs_mount_null);
+}
+
+static void test_vfs_null_dir_ops_opendir(void)
+{
+    TEST_ASSERT_EQUAL_INT(0, _test_vfs_dir_op_status);
+    int res = vfs_opendir(NULL, "/test/mydir2");
+    TEST_ASSERT_EQUAL_INT(-EINVAL, res);
+}
+
+static void test_vfs_null_dir_ops_closedir(void)
+{
+    TEST_ASSERT_EQUAL_INT(0, _test_vfs_dir_op_status);
+    int res = vfs_closedir(&_test_dir);
+    TEST_ASSERT_EQUAL_INT(0, res);
+    res = vfs_closedir(&_test_dir);
+    TEST_ASSERT_EQUAL_INT(-EBADF, res);
+}
+
+static void test_vfs_null_dir_ops_readdir(void)
+{
+    TEST_ASSERT_EQUAL_INT(0, _test_vfs_dir_op_status);
+    vfs_dirent_t buf;
+    int res = vfs_readdir(&_test_dir, &buf);
+    TEST_ASSERT_EQUAL_INT(-EINVAL, res);
+}
+
+Test *tests_vfs_null_dir_ops_tests(void)
+{
+    EMB_UNIT_TESTFIXTURES(fixtures) {
+        new_TestFixture(test_vfs_null_dir_ops_opendir),
+        new_TestFixture(test_vfs_null_dir_ops_closedir),
+        new_TestFixture(test_vfs_null_dir_ops_readdir),
+    };
+
+    EMB_UNIT_TESTCALLER(vfs_dir_op_tests, setup, teardown, fixtures);
+
+    return (Test *)&vfs_dir_op_tests;
+}
+
+/** @} */
diff --git a/tests/unittests/tests-vfs/tests-vfs-file-ops.c b/tests/unittests/tests-vfs/tests-vfs-file-ops.c
new file mode 100644
index 0000000000..4b5ae05ea5
--- /dev/null
+++ b/tests/unittests/tests-vfs/tests-vfs-file-ops.c
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2016 Eistec AB
+ *
+ * This file is subject to the terms and conditions of the GNU Lesser
+ * General Public License v2.1. See the file LICENSE in the top level
+ * directory for more details.
+ */
+
+/**
+ * @{
+ *
+ * @file
+ * @brief Transparent-box unit tests of vfs functions stubs used when the file
+ * system does not implement the requested function.
+ */
+#include <errno.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "embUnit/embUnit.h"
+
+#include "vfs.h"
+
+#include "tests-vfs.h"
+
+static const vfs_file_system_ops_t null_fs_ops = {
+    .mount   = NULL,
+    .umount  = NULL,
+    .unlink  = NULL,
+    .statvfs = NULL,
+    .stat    = NULL,
+};
+
+static const vfs_file_ops_t null_file_ops = {
+    .close = NULL,
+    .fstat = NULL,
+    .lseek = NULL,
+    .open  = NULL,
+    .read  = NULL,
+    .write = NULL,
+};
+
+static const vfs_dir_ops_t null_dir_ops = {
+    .opendir  = NULL,
+    .readdir  = NULL,
+    .closedir = NULL,
+};
+
+static const vfs_file_system_t null_file_system = {
+    .f_op  = &null_file_ops,
+    .fs_op = &null_fs_ops,
+    .d_op  = &null_dir_ops,
+};
+
+static vfs_mount_t _test_vfs_mount_null = {
+    .mount_point = "/test",
+    .fs = &null_file_system,
+    .private_data = NULL,
+};
+
+static int _test_vfs_file_op_my_fd = -1;
+
+static void setup(void)
+{
+    int res = vfs_mount(&_test_vfs_mount_null);
+    if (res < 0) {
+        _test_vfs_file_op_my_fd = -1;
+        return;
+    }
+    _test_vfs_file_op_my_fd = vfs_open("/test/somefile", O_RDONLY, 0);
+}
+
+static void teardown(void)
+{
+    if (_test_vfs_file_op_my_fd >= 0) {
+        vfs_close(_test_vfs_file_op_my_fd);
+        _test_vfs_file_op_my_fd = -1;
+    }
+    vfs_umount(&_test_vfs_mount_null);
+}
+
+static void test_vfs_null_file_ops_close(void)
+{
+    TEST_ASSERT(_test_vfs_file_op_my_fd >= 0);
+    int res = vfs_close(_test_vfs_file_op_my_fd);
+    TEST_ASSERT_EQUAL_INT(0, res);
+    _test_vfs_file_op_my_fd = -1; /* prevent double close */
+}
+
+static void test_vfs_null_file_ops_fcntl(void)
+{
+    TEST_ASSERT(_test_vfs_file_op_my_fd >= 0);
+    int res = vfs_fcntl(_test_vfs_file_op_my_fd, F_GETFL, 0);
+    TEST_ASSERT_EQUAL_INT(O_RDONLY, res);
+    res = vfs_fcntl(_test_vfs_file_op_my_fd, F_GETFD, 0);
+    TEST_ASSERT_EQUAL_INT(-EINVAL, res);
+}
+
+static void test_vfs_null_file_ops_lseek(void)
+{
+    TEST_ASSERT(_test_vfs_file_op_my_fd >= 0);
+    off_t pos;
+    pos = vfs_lseek(_test_vfs_file_op_my_fd, 4, SEEK_SET);
+    TEST_ASSERT_EQUAL_INT(4, pos);
+    pos = vfs_lseek(_test_vfs_file_op_my_fd, 3, SEEK_CUR);
+    TEST_ASSERT_EQUAL_INT(7, pos);
+    pos = vfs_lseek(_test_vfs_file_op_my_fd, -7, SEEK_CUR);
+    TEST_ASSERT_EQUAL_INT(0, pos);
+    pos = vfs_lseek(_test_vfs_file_op_my_fd, -1, SEEK_CUR);
+    TEST_ASSERT_EQUAL_INT(-EINVAL, pos);
+    pos = vfs_lseek(_test_vfs_file_op_my_fd, 12345, SEEK_SET);
+    TEST_ASSERT_EQUAL_INT(12345, pos);
+    pos = vfs_lseek(_test_vfs_file_op_my_fd, -1, SEEK_SET);
+    TEST_ASSERT_EQUAL_INT(-EINVAL, pos);
+    pos = vfs_lseek(_test_vfs_file_op_my_fd, 0, SEEK_END); /* not implemented in "file system" */
+    TEST_ASSERT_EQUAL_INT(-EINVAL, pos);
+    pos = vfs_lseek(_test_vfs_file_op_my_fd, 0, SEEK_CUR);
+    TEST_ASSERT_EQUAL_INT(12345, pos);
+}
+
+static void test_vfs_null_file_ops_fstat(void)
+{
+    TEST_ASSERT(_test_vfs_file_op_my_fd >= 0);
+    struct stat buf;
+    int res = vfs_fstat(_test_vfs_file_op_my_fd, &buf);
+    TEST_ASSERT_EQUAL_INT(-EINVAL, res);
+}
+
+static void test_vfs_null_file_ops_read(void)
+{
+    TEST_ASSERT(_test_vfs_file_op_my_fd >= 0);
+    uint8_t buf[8];
+    int res = vfs_read(_test_vfs_file_op_my_fd, buf, sizeof(buf));
+    TEST_ASSERT_EQUAL_INT(-EINVAL, res);
+    res = vfs_read(_test_vfs_file_op_my_fd, NULL, sizeof(buf));
+    TEST_ASSERT_EQUAL_INT(-EFAULT, res);
+}
+
+static void test_vfs_null_file_ops_write(void)
+{
+    TEST_ASSERT(_test_vfs_file_op_my_fd >= 0);
+    static const char buf[] = "Unit test";
+    int res = vfs_write(_test_vfs_file_op_my_fd, buf, sizeof(buf));
+    TEST_ASSERT_EQUAL_INT(-EBADF, res);
+    res = vfs_write(_test_vfs_file_op_my_fd, NULL, sizeof(buf));
+    TEST_ASSERT_EQUAL_INT(-EFAULT, res);
+}
+
+Test *tests_vfs_null_file_ops_tests(void)
+{
+    EMB_UNIT_TESTFIXTURES(fixtures) {
+        new_TestFixture(test_vfs_null_file_ops_close),
+        new_TestFixture(test_vfs_null_file_ops_fcntl),
+        new_TestFixture(test_vfs_null_file_ops_lseek),
+        new_TestFixture(test_vfs_null_file_ops_fstat),
+        new_TestFixture(test_vfs_null_file_ops_read),
+        new_TestFixture(test_vfs_null_file_ops_write),
+    };
+
+    EMB_UNIT_TESTCALLER(vfs_file_op_tests, setup, teardown, fixtures);
+
+    return (Test *)&vfs_file_op_tests;
+}
+
+/** @} */
diff --git a/tests/unittests/tests-vfs/tests-vfs-file-system-ops.c b/tests/unittests/tests-vfs/tests-vfs-file-system-ops.c
new file mode 100644
index 0000000000..7443e8577c
--- /dev/null
+++ b/tests/unittests/tests-vfs/tests-vfs-file-system-ops.c
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2016 Eistec AB
+ *
+ * This file is subject to the terms and conditions of the GNU Lesser
+ * General Public License v2.1. See the file LICENSE in the top level
+ * directory for more details.
+ */
+
+/**
+ * @{
+ *
+ * @file
+ * @brief Transparent-box unit tests of vfs functions stubs used when the file
+ * system does not implement the requested function.
+ */
+#include <errno.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "embUnit/embUnit.h"
+
+#include "vfs.h"
+
+#include "tests-vfs.h"
+
+static const vfs_file_system_ops_t null_fs_ops = {
+    .mount   = NULL,
+    .umount  = NULL,
+    .unlink  = NULL,
+    .statvfs = NULL,
+    .stat    = NULL,
+};
+
+static const vfs_file_ops_t null_file_ops = {
+    .close = NULL,
+    .fstat = NULL,
+    .lseek = NULL,
+    .open  = NULL,
+    .read  = NULL,
+    .write = NULL,
+};
+
+static const vfs_dir_ops_t null_dir_ops = {
+    .opendir  = NULL,
+    .readdir  = NULL,
+    .closedir = NULL,
+};
+
+static const vfs_file_system_t null_file_system = {
+    .f_op  = &null_file_ops,
+    .fs_op = &null_fs_ops,
+    .d_op  = &null_dir_ops,
+};
+
+static vfs_mount_t _test_vfs_mount_null = {
+    .mount_point = "/test",
+    .fs = &null_file_system,
+    .private_data = NULL,
+};
+
+static int _test_vfs_fs_op_mount_res = -1;
+
+static void setup(void)
+{
+    _test_vfs_fs_op_mount_res = vfs_mount(&_test_vfs_mount_null);
+}
+
+static void teardown(void)
+{
+    vfs_umount(&_test_vfs_mount_null);
+}
+
+static void test_vfs_null_fs_ops_mount(void)
+{
+    TEST_ASSERT_EQUAL_INT(0, _test_vfs_fs_op_mount_res);
+    int res = vfs_mount(&_test_vfs_mount_null);
+    /* Already mounted */
+    TEST_ASSERT_EQUAL_INT(-EBUSY, res);
+}
+
+static void test_vfs_null_fs_ops_umount(void)
+{
+    TEST_ASSERT_EQUAL_INT(0, _test_vfs_fs_op_mount_res);
+    int res = vfs_umount(&_test_vfs_mount_null);
+    TEST_ASSERT_EQUAL_INT(0, res);
+    res = vfs_umount(&_test_vfs_mount_null);
+    /* Not mounted */
+    TEST_ASSERT_EQUAL_INT(-EINVAL, res);
+}
+
+static void test_vfs_null_fs_ops_rename(void)
+{
+    TEST_ASSERT_EQUAL_INT(0, _test_vfs_fs_op_mount_res);
+    int res = vfs_rename("/test/foo", "/test/bar");
+    TEST_ASSERT_EQUAL_INT(-EPERM, res);
+}
+
+static void test_vfs_null_fs_ops_unlink(void)
+{
+    TEST_ASSERT_EQUAL_INT(0, _test_vfs_fs_op_mount_res);
+    int res = vfs_unlink("/test/foo");
+    TEST_ASSERT_EQUAL_INT(-EPERM, res);
+}
+
+static void test_vfs_null_fs_ops_mkdir(void)
+{
+    TEST_ASSERT_EQUAL_INT(0, _test_vfs_fs_op_mount_res);
+    int res = vfs_mkdir("/test/foodir", 0);
+    TEST_ASSERT_EQUAL_INT(-EPERM, res);
+}
+
+static void test_vfs_null_fs_ops_rmdir(void)
+{
+    TEST_ASSERT_EQUAL_INT(0, _test_vfs_fs_op_mount_res);
+    int res = vfs_rmdir("/test/foodir");
+    TEST_ASSERT_EQUAL_INT(-EPERM, res);
+}
+
+static void test_vfs_null_fs_ops_stat(void)
+{
+    TEST_ASSERT_EQUAL_INT(0, _test_vfs_fs_op_mount_res);
+    struct stat buf;
+    int res = vfs_stat("/test/foo", &buf);
+    TEST_ASSERT_EQUAL_INT(-EPERM, res);
+}
+
+static void test_vfs_null_fs_ops_statvfs(void)
+{
+    TEST_ASSERT_EQUAL_INT(0, _test_vfs_fs_op_mount_res);
+    struct statvfs buf;
+    int res = vfs_statvfs("/test", &buf);
+    TEST_ASSERT_EQUAL_INT(-EPERM, res);
+}
+
+static void test_vfs_null_fs_ops_fstatvfs(void)
+{
+    TEST_ASSERT_EQUAL_INT(0, _test_vfs_fs_op_mount_res);
+    int fd = vfs_open("/test/baz", O_RDONLY, 0);
+    TEST_ASSERT(fd >= 0);
+    struct statvfs buf;
+    int res = vfs_fstatvfs(fd, &buf);
+    TEST_ASSERT_EQUAL_INT(-EINVAL, res);
+}
+
+
+Test *tests_vfs_null_file_system_ops_tests(void)
+{
+    EMB_UNIT_TESTFIXTURES(fixtures) {
+        new_TestFixture(test_vfs_null_fs_ops_mount),
+        new_TestFixture(test_vfs_null_fs_ops_umount),
+        new_TestFixture(test_vfs_null_fs_ops_rename),
+        new_TestFixture(test_vfs_null_fs_ops_unlink),
+        new_TestFixture(test_vfs_null_fs_ops_mkdir),
+        new_TestFixture(test_vfs_null_fs_ops_rmdir),
+        new_TestFixture(test_vfs_null_fs_ops_stat),
+        new_TestFixture(test_vfs_null_fs_ops_statvfs),
+        new_TestFixture(test_vfs_null_fs_ops_fstatvfs),
+    };
+
+    EMB_UNIT_TESTCALLER(vfs_fs_op_tests, setup, teardown, fixtures);
+
+    return (Test *)&vfs_fs_op_tests;
+}
+
+/** @} */
diff --git a/tests/unittests/tests-vfs/tests-vfs-mount-constfs.c b/tests/unittests/tests-vfs/tests-vfs-mount-constfs.c
new file mode 100644
index 0000000000..88a96b510d
--- /dev/null
+++ b/tests/unittests/tests-vfs/tests-vfs-mount-constfs.c
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2016 Eistec AB
+ *
+ * This file is subject to the terms and conditions of the GNU Lesser
+ * General Public License v2.1. See the file LICENSE in the top level
+ * directory for more details.
+ */
+
+/**
+ * @{
+ *
+ * @file
+ * @brief       Unittests for vfs_mount, vfs_umount, ConstFS, VFS POSIX wrappers
+ *
+ * @author      Joakim Nohlgård <joakim.nohlgard@eistec.se>
+ */
+#include <errno.h>
+#include <stdint.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "embUnit/embUnit.h"
+
+#include "vfs.h"
+#include "fs/constfs.h"
+
+#include "tests-vfs.h"
+
+static const uint8_t bin_data[] = {
+    0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+    0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
+    0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7,
+    0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF,
+};
+static const uint8_t str_data[] = "This is a test file";
+                                /* 01234567890123456789 */
+                                /* 0         1          */
+static const constfs_file_t _files[] = {
+    {
+        .path = "/test.txt",
+        .data = str_data,
+        .size = sizeof(str_data),
+    },
+    {
+        .path = "/data.bin",
+        .data = bin_data,
+        .size = sizeof(bin_data),
+    },
+};
+
+static const constfs_t fs_data = {
+    .files = _files,
+    .nfiles = sizeof(_files) / sizeof(_files[0]),
+};
+
+static vfs_mount_t _test_vfs_mount_invalid_mount = {
+    .mount_point = "test",
+    .fs = &constfs_file_system,
+    .private_data = (void *)&fs_data,
+};
+
+static vfs_mount_t _test_vfs_mount = {
+    .mount_point = "/test",
+    .fs = &constfs_file_system,
+    .private_data = (void *)&fs_data,
+};
+
+static void test_vfs_mount_umount(void)
+{
+    int res;
+    res = vfs_mount(&_test_vfs_mount);
+    TEST_ASSERT_EQUAL_INT(0, res);
+    res = vfs_umount(&_test_vfs_mount);
+    TEST_ASSERT_EQUAL_INT(0, res);
+}
+
+static void test_vfs_mount__invalid(void)
+{
+    int res;
+    res = vfs_mount(NULL);
+    TEST_ASSERT(res < 0);
+
+    res = vfs_mount(&_test_vfs_mount_invalid_mount);
+    TEST_ASSERT(res < 0);
+}
+
+static void test_vfs_umount__invalid_mount(void)
+{
+    int res;
+    res = vfs_umount(NULL);
+    TEST_ASSERT(res < 0);
+    res = vfs_umount(&_test_vfs_mount);
+    TEST_ASSERT(res < 0);
+}
+
+static void test_vfs_constfs_open(void)
+{
+    int res;
+    res = vfs_mount(&_test_vfs_mount);
+    TEST_ASSERT_EQUAL_INT(0, res);
+
+    int fd;
+    fd = vfs_open("/test/notfound", O_RDONLY, 0);
+    TEST_ASSERT(fd == -ENOENT);
+    if (fd >= 0) {
+        vfs_close(fd);
+    }
+    fd = vfs_open("/test/test.txt", O_WRONLY, 0);
+    TEST_ASSERT(fd == -EROFS);
+    if (fd >= 0) {
+        vfs_close(fd);
+    }
+    fd = vfs_open("/test/test.txt", O_RDWR, 0);
+    TEST_ASSERT(fd == -EROFS);
+    if (fd >= 0) {
+        vfs_close(fd);
+    }
+    fd = vfs_open("/test/test.txt", O_RDONLY, 0);
+    TEST_ASSERT(fd >= 0);
+    if (fd >= 0) {
+        res = vfs_close(fd);
+        TEST_ASSERT_EQUAL_INT(0, res);
+    }
+
+    res = vfs_umount(&_test_vfs_mount);
+    TEST_ASSERT_EQUAL_INT(0, res);
+}
+
+static void test_vfs_constfs_read_lseek(void)
+{
+    int res;
+    res = vfs_mount(&_test_vfs_mount);
+    TEST_ASSERT_EQUAL_INT(0, res);
+
+    int fd = vfs_open("/test/test.txt", O_RDONLY, 0);
+    TEST_ASSERT(fd >= 0);
+
+    char strbuf[64];
+    memset(strbuf, '\0', sizeof(strbuf));
+    ssize_t nbytes;
+    nbytes = vfs_read(fd, strbuf, sizeof(strbuf));
+    TEST_ASSERT_EQUAL_INT(sizeof(str_data), nbytes);
+    TEST_ASSERT_EQUAL_STRING((const char *)&str_data[0], (const char *)&strbuf[0]);
+
+    off_t pos;
+    /* lseek to the middle */
+    memset(strbuf, '\0', sizeof(strbuf));
+    pos = vfs_lseek(fd, sizeof(str_data) / 2, SEEK_SET);
+    TEST_ASSERT_EQUAL_INT(sizeof(str_data) / 2, pos);
+    nbytes = vfs_read(fd, strbuf, sizeof(strbuf));
+    TEST_ASSERT_EQUAL_INT((sizeof(str_data) + 1) / 2, nbytes); /* + 1 for rounding up */
+    TEST_ASSERT_EQUAL_STRING((const char *)&str_data[sizeof(str_data) / 2], (const char *)&strbuf[0]);
+
+    /* lseek to near the end */
+    memset(strbuf, '\0', sizeof(strbuf));
+    pos = vfs_lseek(fd, -1, SEEK_END);
+    TEST_ASSERT_EQUAL_INT(sizeof(str_data) - 1, pos);
+    nbytes = vfs_read(fd, strbuf, sizeof(strbuf));
+    TEST_ASSERT_EQUAL_INT(1, nbytes);
+    TEST_ASSERT_EQUAL_STRING((const char *)&str_data[sizeof(str_data) - 1], (const char *)&strbuf[0]);
+
+    res = vfs_fcntl(fd, F_GETFL, 0);
+    TEST_ASSERT_EQUAL_INT(O_RDONLY, res);
+
+    res = vfs_close(fd);
+    TEST_ASSERT_EQUAL_INT(0, res);
+
+    res = vfs_umount(&_test_vfs_mount);
+    TEST_ASSERT_EQUAL_INT(0, res);
+}
+
+#if MODULE_NEWLIB
+static void test_vfs_constfs__posix(void)
+{
+    int res;
+    res = vfs_mount(&_test_vfs_mount);
+    TEST_ASSERT_EQUAL_INT(0, res);
+
+    int fd = open("/test/test.txt", O_RDONLY, 0);
+    TEST_ASSERT(fd >= 0);
+
+    char strbuf[64];
+    memset(strbuf, '\0', sizeof(strbuf));
+    ssize_t nbytes;
+    nbytes = read(fd, strbuf, sizeof(strbuf));
+    TEST_ASSERT_EQUAL_INT(sizeof(str_data), nbytes);
+    TEST_ASSERT_EQUAL_STRING((const char *)&str_data[0], (const char *)&strbuf[0]);
+
+#if HAVE_FCNTL
+    /* fcntl support is optional in newlib */
+    res = fcntl(fd, F_GETFL, 0);
+    TEST_ASSERT_EQUAL_INT(O_RDONLY, res);
+#endif
+
+    res = close(fd);
+    TEST_ASSERT_EQUAL_INT(0, res);
+
+    res = vfs_umount(&_test_vfs_mount);
+    TEST_ASSERT_EQUAL_INT(0, res);
+}
+#endif
+
+Test *tests_vfs_mount_constfs_tests(void)
+{
+    EMB_UNIT_TESTFIXTURES(fixtures) {
+        new_TestFixture(test_vfs_mount_umount),
+        new_TestFixture(test_vfs_mount__invalid),
+        new_TestFixture(test_vfs_umount__invalid_mount),
+        new_TestFixture(test_vfs_constfs_open),
+        new_TestFixture(test_vfs_constfs_read_lseek),
+#if MODULE_NEWLIB
+        new_TestFixture(test_vfs_constfs__posix),
+#endif
+    };
+
+    EMB_UNIT_TESTCALLER(vfs_mount_tests, NULL, NULL, fixtures);
+
+    return (Test *)&vfs_mount_tests;
+}
+
+/** @} */
diff --git a/tests/unittests/tests-vfs/tests-vfs-normalize_path.c b/tests/unittests/tests-vfs/tests-vfs-normalize_path.c
new file mode 100644
index 0000000000..6e6d6fb8e9
--- /dev/null
+++ b/tests/unittests/tests-vfs/tests-vfs-normalize_path.c
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2016 Eistec AB
+ *
+ * This file is subject to the terms and conditions of the GNU Lesser
+ * General Public License v2.1. See the file LICENSE in the top level
+ * directory for more details.
+ */
+
+/**
+ * @{
+ *
+ * @file
+ * @brief       Unittests for vfs_normalize_path
+ *
+ * @author      Joakim Nohlgård <joakim.nohlgard@eistec.se>
+ */
+#include <errno.h>
+#include <stdint.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+#include "embUnit/embUnit.h"
+
+#include "vfs.h"
+
+#include "tests-vfs.h"
+
+static void test_vfs_normalize_path__noop(void)
+{
+    static const char path[] = "/this/is/a/test";
+    char buf[16];
+    int res = vfs_normalize_path(buf, path, sizeof(buf));
+    TEST_ASSERT_EQUAL_INT(4, res);
+    TEST_ASSERT_EQUAL_STRING((const char *)&path[0], (const char *)&buf[0]);
+}
+
+static void test_vfs_normalize_path__slashes(void)
+{
+    static const char path[] = "///////////////////////////////";
+    static const char expected[] = "/";
+    char buf[4];
+    int res = vfs_normalize_path(buf, path, sizeof(buf));
+    TEST_ASSERT_EQUAL_INT(1, res);
+    TEST_ASSERT_EQUAL_STRING((const char *)&expected[0], (const char *)&buf[0]);
+}
+
+static void test_vfs_normalize_path__dot(void)
+{
+    static const char path[] = "/abc/./def/././zxcv././.";
+    static const char expected[] = "/abc/def/zxcv.";
+    char buf[16];
+    int res = vfs_normalize_path(buf, path, sizeof(buf));
+    TEST_ASSERT_EQUAL_INT(3, res);
+    TEST_ASSERT_EQUAL_STRING((const char *)&expected[0], (const char *)&buf[0]);
+}
+
+static void test_vfs_normalize_path__reduce(void)
+{
+    static const char path[] = "/abc/../def";
+    static const char expected[] = "/def";
+    char buf[16];
+    int res = vfs_normalize_path(buf, path, sizeof(buf));
+    TEST_ASSERT_EQUAL_INT(1, res);
+    TEST_ASSERT_EQUAL_STRING((const char *)&expected[0], (const char *)&buf[0]);
+}
+
+static void test_vfs_normalize_path__trailing(void)
+{
+    static const char path[] = "/mydir/";
+    static const char expected[] = "/mydir/";
+    char buf[16];
+    int res = vfs_normalize_path(buf, path, sizeof(buf));
+    TEST_ASSERT_EQUAL_INT(1, res);
+    TEST_ASSERT_EQUAL_STRING((const char *)&expected[0], (const char *)&buf[0]);
+}
+
+static void test_vfs_normalize_path__outside(void)
+{
+    static const char path[] = "/somewhere/../..";
+    static const char path2[] = "/../abdgh";
+    char buf[16];
+    int res = vfs_normalize_path(buf, path, sizeof(buf));
+    TEST_ASSERT(res < 0);
+    res = vfs_normalize_path(buf, path2, sizeof(buf));
+    TEST_ASSERT(res < 0);
+}
+
+static void test_vfs_normalize_path__toolong(void)
+{
+    static const char path[] = "/abc";
+    char buf[4];
+    int res = vfs_normalize_path(buf, path, sizeof(buf));
+    TEST_ASSERT(res < 0);
+}
+
+static void test_vfs_normalize_path__shorten(void)
+{
+#if 0
+    /* Not supported by the current implementation */
+    /* The current implementation needs enough buffer space to store the longest
+     * prefix path before each ../ reduction */
+    static const char path[] = "/qwerty/asdfghjkl/..";
+    static const char expected[] = "/qwerty";
+    char buf[8];
+#endif
+    static const char path[] = "/12345/6789/..";
+    static const char expected[] = "/12345";
+    char buf[12];
+    int res = vfs_normalize_path(buf, path, sizeof(buf));
+    TEST_ASSERT_EQUAL_INT(1, res);
+    TEST_ASSERT_EQUAL_STRING((const char *)&expected[0], (const char *)&buf[0]);
+}
+
+static void test_vfs_normalize_path__shorten_inplace(void)
+{
+    char path[] = "/qwerty/asdfghjkl/..";
+    static const char expected[] = "/qwerty";
+    int res = vfs_normalize_path(path, path, sizeof(path));
+    TEST_ASSERT_EQUAL_INT(1, res);
+    TEST_ASSERT_EQUAL_STRING((const char *)&expected[0], (const char *)&path[0]);
+}
+
+static void test_vfs_normalize_path__empty(void)
+{
+    char path[] = "";
+    static const char expected[] = "";
+    char buf[4];
+    int res = vfs_normalize_path(buf, path, sizeof(buf));
+    TEST_ASSERT_EQUAL_INT(0, res);
+    TEST_ASSERT_EQUAL_STRING((const char *)&expected[0], (const char *)&path[0]);
+}
+
+Test *tests_vfs_normalize_path_tests(void)
+{
+    EMB_UNIT_TESTFIXTURES(fixtures) {
+        new_TestFixture(test_vfs_normalize_path__noop),
+        new_TestFixture(test_vfs_normalize_path__slashes),
+        new_TestFixture(test_vfs_normalize_path__dot),
+        new_TestFixture(test_vfs_normalize_path__reduce),
+        new_TestFixture(test_vfs_normalize_path__trailing),
+        new_TestFixture(test_vfs_normalize_path__outside),
+        new_TestFixture(test_vfs_normalize_path__toolong),
+        new_TestFixture(test_vfs_normalize_path__shorten),
+        new_TestFixture(test_vfs_normalize_path__shorten_inplace),
+        new_TestFixture(test_vfs_normalize_path__empty),
+    };
+
+    EMB_UNIT_TESTCALLER(vfs_normalize_path_tests, NULL, NULL, fixtures);
+
+    return (Test *)&vfs_normalize_path_tests;
+}
+/** @} */
diff --git a/tests/unittests/tests-vfs/tests-vfs-open-close.c b/tests/unittests/tests-vfs/tests-vfs-open-close.c
new file mode 100644
index 0000000000..4b14365fd5
--- /dev/null
+++ b/tests/unittests/tests-vfs/tests-vfs-open-close.c
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2016 Eistec AB
+ *
+ * This file is subject to the terms and conditions of the GNU Lesser
+ * General Public License v2.1. See the file LICENSE in the top level
+ * directory for more details.
+ */
+
+/**
+ * @{
+ *
+ * @file
+ * @brief       Unittests for vfs_open, vfs_close
+ *
+ * @author      Joakim Nohlgård <joakim.nohlgard@eistec.se>
+ */
+#include <errno.h>
+#include <stdint.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+#include "embUnit/embUnit.h"
+
+#include "vfs.h"
+
+#include "tests-vfs.h"
+
+static void test_vfs_close__invalid_fd(void)
+{
+    int res = vfs_close(-1);
+    TEST_ASSERT(res < 0);
+}
+
+static void test_vfs_open__notfound(void)
+{
+    int fd = vfs_open("/notfound/path", O_RDONLY, 0);
+    TEST_ASSERT(fd < 0);
+}
+
+Test *tests_vfs_open_close_tests(void)
+{
+    EMB_UNIT_TESTFIXTURES(fixtures) {
+        new_TestFixture(test_vfs_open__notfound),
+        new_TestFixture(test_vfs_close__invalid_fd),
+    };
+
+    EMB_UNIT_TESTCALLER(vfs_open_close_tests, NULL, NULL, fixtures);
+
+    return (Test *)&vfs_open_close_tests;
+}
+/** @} */
diff --git a/tests/unittests/tests-vfs/tests-vfs.c b/tests/unittests/tests-vfs/tests-vfs.c
new file mode 100644
index 0000000000..c8c91489f5
--- /dev/null
+++ b/tests/unittests/tests-vfs/tests-vfs.c
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2016 Eistec AB
+ *
+ * This file is subject to the terms and conditions of the GNU Lesser
+ * General Public License v2.1. See the file LICENSE in the top level
+ * directory for more details.
+ */
+
+/**
+ * @{
+ *
+ * @file
+ * @brief       Unittest entry point for the VFS test group
+ *
+ * @author      Joakim Nohlgård <joakim.nohlgard@eistec.se>
+ */
+
+#include "embUnit/embUnit.h"
+
+#include "tests-vfs.h"
+
+Test *tests_vfs_bind_tests(void);
+Test *tests_vfs_mount_constfs_tests(void);
+Test *tests_vfs_open_close_tests(void);
+Test *tests_vfs_normalize_path_tests(void);
+Test *tests_vfs_null_file_ops_tests(void);
+Test *tests_vfs_null_file_system_ops_tests(void);
+Test *tests_vfs_null_dir_ops_tests(void);
+
+void tests_vfs(void)
+{
+    TESTS_RUN(tests_vfs_open_close_tests());
+    TESTS_RUN(tests_vfs_bind_tests());
+    TESTS_RUN(tests_vfs_mount_constfs_tests());
+    TESTS_RUN(tests_vfs_normalize_path_tests());
+    TESTS_RUN(tests_vfs_null_file_ops_tests());
+    TESTS_RUN(tests_vfs_null_file_system_ops_tests());
+    TESTS_RUN(tests_vfs_null_dir_ops_tests());
+}
+/** @} */
diff --git a/tests/unittests/tests-vfs/tests-vfs.h b/tests/unittests/tests-vfs/tests-vfs.h
new file mode 100644
index 0000000000..edd6457537
--- /dev/null
+++ b/tests/unittests/tests-vfs/tests-vfs.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2016 Eistec AB
+ *
+ * This file is subject to the terms and conditions of the GNU Lesser
+ * General Public License v2.1. See the file LICENSE in the top level
+ * directory for more details.
+ */
+
+/**
+ * @addtogroup  unittests
+ * @{
+ *
+ * @file
+ * @brief       Unittests for the ``vfs`` module
+ *
+ * @author      Joakim Nohlgård <joakim.nohlgard@eistec.se>
+ */
+#ifndef TESTS_VFS_H
+#define TESTS_VFS_H
+
+#include "embUnit.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief   The entry point of this test suite.
+ */
+void tests_vfs(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* TESTS_VFS_H */
+/** @} */
-- 
GitLab