changeset 640:9850089c9671

core: resurrect VFS
author David Demelier <markand@malikania.fr>
date Wed, 27 Sep 2023 22:03:17 +0200
parents 19d19f644b5e
children fcd124e513ea
files CMakeLists.txt cmake/FindZIP.cmake cmake/MlkOptions.cmake cmake/MlkOptions.install.cmake libmlk-core/CMakeLists.txt libmlk-core/libmlk-core-config.cmake libmlk-core/mlk/core/vfs-dir.c libmlk-core/mlk/core/vfs-dir.h libmlk-core/mlk/core/vfs-zip.c libmlk-core/mlk/core/vfs-zip.h libmlk-core/mlk/core/vfs.c libmlk-core/mlk/core/vfs.h libmlk-core/mlk/core/vfs_p.h libmlk-util/mlk/util/sysconfig.cmake.h tests/CMakeLists.txt tests/assets/vfs/data.zip tests/assets/vfs/directory/hello.txt tests/test-vfs-dir.c tests/test-vfs-zip.c
diffstat 19 files changed, 1046 insertions(+), 4 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Sat Sep 23 21:04:16 2023 +0200
+++ b/CMakeLists.txt	Wed Sep 27 22:03:17 2023 +0200
@@ -85,6 +85,10 @@
 	find_package(Intl REQUIRED)
 endif ()
 
+if (MLK_WITH_ZIP)
+	find_package(ZIP REQUIRED)
+endif ()
+
 add_subdirectory(extern/libsqlite)
 add_subdirectory(extern/libdt)
 add_subdirectory(extern/libutlist)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cmake/FindZIP.cmake	Wed Sep 27 22:03:17 2023 +0200
@@ -0,0 +1,66 @@
+# FindZIP
+# -------
+#
+# Find libzip library, this modules defines:
+#
+# ZIP_INCLUDE_DIRS, where to find zip.h
+# ZIP_LIBRARIES, where to find library
+# ZIP_FOUND, if it is found
+#
+# The following imported targets will be available:
+#
+# ZIP::ZIP, if found.
+
+find_package(ZLIB QUIET)
+
+find_path(
+	ZIP_INCLUDE_DIR
+	NAMES zip.h
+)
+
+find_library(
+	ZIP_LIBRARY
+	NAMES zip libzip
+)
+
+find_path(
+	ZIPCONF_INCLUDE_DIR
+	NAMES zipconf.h
+)
+
+if (NOT ZIPCONF_INCLUDE_DIR)
+	# zipconf.h is sometimes directly in the include/ folder but on some systems
+	# like Windows, it is installed in the lib/ directory.
+	get_filename_component(_ZIP_PRIVATE_LIBRARY "${ZIP_LIBRARY}" DIRECTORY)
+
+	find_path(
+		ZIPCONF_INCLUDE_DIR
+		NAMES zipconf.h
+		PATHS "${_ZIP_PRIVATE_LIBRARY}/libzip/include"
+	)
+endif ()
+
+include(FindPackageHandleStandardArgs)
+
+find_package_handle_standard_args(
+	ZIP
+	REQUIRED_VARS ZLIB_LIBRARIES ZLIB_INCLUDE_DIRS ZIP_LIBRARY ZIP_INCLUDE_DIR ZIPCONF_INCLUDE_DIR
+)
+
+if (ZIP_FOUND)
+	set(ZIP_LIBRARIES ${ZIP_LIBRARY} ${ZLIB_LIBRARIES})
+	set(ZIP_INCLUDE_DIRS ${ZIP_INCLUDE_DIR} ${ZIPCONF_INCLUDE_DIR} ${ZLIB_INCLUDE_DIRS})
+
+	if (NOT TARGET ZIP::ZIP)
+		add_library(ZIP::ZIP UNKNOWN IMPORTED)
+		set_target_properties(
+			ZIP::ZIP
+			PROPERTIES
+				IMPORTED_LINK_INTERFACE_LANGUAGES "C"
+				IMPORTED_LOCATION "${ZIP_LIBRARY}"
+				INTERFACE_INCLUDE_DIRECTORIES "${ZIP_INCLUDE_DIRS}"
+		)
+	endif ()
+endif ()
+
+mark_as_advanced(ZIP_LIBRARY ZIP_INCLUDE_DIR ZIPCONF_INCLUDE_DIR)
--- a/cmake/MlkOptions.cmake	Sat Sep 23 21:04:16 2023 +0200
+++ b/cmake/MlkOptions.cmake	Wed Sep 27 22:03:17 2023 +0200
@@ -23,3 +23,4 @@
 mlk_option(TESTS_GRAPHICAL On BOOL "Enable unit tests that requires graphical context")
 mlk_option(CMAKEDIR "${CMAKE_INSTALL_LIBDIR}/cmake" STRING "Destination for CMake files")
 mlk_option(JAVASCRIPT On BOOL "Enable Javascript bindings")
+mlk_option(ZIP On BOOL "Enable zip file support in VFS")
--- a/cmake/MlkOptions.install.cmake	Sat Sep 23 21:04:16 2023 +0200
+++ b/cmake/MlkOptions.install.cmake	Wed Sep 27 22:03:17 2023 +0200
@@ -18,3 +18,4 @@
 
 set(MLK_WITH_NLS @MLK_WITH_NLS@)
 set(MLK_WITH_CMAKEDIR @MLK_WITH_CMAKEDIR@)
+set(MLK_WITH_ZIP @MLK_WITH_ZIP@)
--- a/libmlk-core/CMakeLists.txt	Sat Sep 23 21:04:16 2023 +0200
+++ b/libmlk-core/CMakeLists.txt	Wed Sep 27 22:03:17 2023 +0200
@@ -53,6 +53,10 @@
 	${libmlk-core_SOURCE_DIR}/mlk/core/texture.c
 	${libmlk-core_SOURCE_DIR}/mlk/core/trace.c
 	${libmlk-core_SOURCE_DIR}/mlk/core/util.c
+	${libmlk-core_SOURCE_DIR}/mlk/core/vfs-dir.c
+	${libmlk-core_SOURCE_DIR}/mlk/core/vfs-zip.c
+	${libmlk-core_SOURCE_DIR}/mlk/core/vfs.c
+	${libmlk-core_SOURCE_DIR}/mlk/core/vfs_p.h
 	${libmlk-core_SOURCE_DIR}/mlk/core/window.c
 )
 
@@ -89,6 +93,9 @@
 	${libmlk-core_SOURCE_DIR}/mlk/core/texture_p.h
 	${libmlk-core_SOURCE_DIR}/mlk/core/trace.h
 	${libmlk-core_SOURCE_DIR}/mlk/core/util.h
+	${libmlk-core_SOURCE_DIR}/mlk/core/vfs-dir.h
+	${libmlk-core_SOURCE_DIR}/mlk/core/vfs-zip.h
+	${libmlk-core_SOURCE_DIR}/mlk/core/vfs.h
 	${libmlk-core_SOURCE_DIR}/mlk/core/window.h
 	${libmlk-core_SOURCE_DIR}/mlk/core/window_p.h
 )
@@ -114,6 +121,10 @@
 	list(APPEND LIBRARIES Intl::Intl)
 endif ()
 
+if (MLK_WITH_ZIP)
+	list(APPEND LIBRARIES ZIP::ZIP)
+endif ()
+
 mlk_library(
 	NAME libmlk-core
 	ASSETS ${ASSETS}
--- a/libmlk-core/libmlk-core-config.cmake	Sat Sep 23 21:04:16 2023 +0200
+++ b/libmlk-core/libmlk-core-config.cmake	Wed Sep 27 22:03:17 2023 +0200
@@ -20,6 +20,7 @@
 
 find_dependency(libmlk-util)
 find_dependency(Intl)
+find_dependency(ZIP)
 
 include("${CMAKE_CURRENT_LIST_DIR}/libmlk-core-targets.cmake")
 include("${CMAKE_CURRENT_LIST_DIR}/../mlk/MlkBcc.cmake")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-core/mlk/core/vfs-dir.c	Wed Sep 27 22:03:17 2023 +0200
@@ -0,0 +1,134 @@
+/*
+ * vfs-dir.c -- VFS subsystem for directories
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "alloc.h"
+#include "err.h"
+#include "util.h"
+#include "vfs-dir.h"
+#include "vfs.h"
+
+#define MLK_VFS_DIR_FILE(self) \
+	MLK_CONTAINER_OF(self, struct mlk_vfs_dir_file, file)
+
+#define MLK_VFS_DIR(self) \
+	MLK_CONTAINER_OF(self, struct mlk_vfs_dir, vfs)
+
+static inline void
+normalize(char *path)
+{
+	size_t len = strlen(path);
+
+	for (char *p = path; *p; ++p)
+		if (*p == '\\')
+			*p = '/';
+
+	while (len && path[len - 1] == '/')
+		path[--len] = 0;
+}
+
+static size_t
+file_read(struct mlk_vfs_file *self, void *buf, size_t bufsz)
+{
+	struct mlk_vfs_dir_file *file = MLK_VFS_DIR_FILE(self);
+	size_t rv;
+
+	rv = fread(buf, 1, bufsz, file->handle);
+
+	if (ferror(file->handle))
+		return -1;
+
+	return rv;
+}
+
+static size_t
+file_write(struct mlk_vfs_file *self, const void *buf, size_t bufsz)
+{
+	struct mlk_vfs_dir_file *file = MLK_VFS_DIR_FILE(self);
+	size_t rv;
+
+	rv = fwrite(buf, 1, bufsz, file->handle);
+
+	if (ferror(file->handle))
+		return -1;
+
+	return rv;
+}
+
+static int
+file_flush(struct mlk_vfs_file *self)
+{
+	struct mlk_vfs_dir_file *file = MLK_VFS_DIR_FILE(self);
+
+	return fflush(file->handle) == EOF ? -1 : 0;
+}
+
+static void
+file_free(struct mlk_vfs_file *self)
+{
+	struct mlk_vfs_dir_file *file = MLK_VFS_DIR_FILE(self);
+
+	fclose(file->handle);
+	mlk_alloc_free(file);
+}
+
+static struct mlk_vfs_file *
+vfs_open(struct mlk_vfs *self, const char *entry, const char *mode)
+{
+	struct mlk_vfs_dir *dir = MLK_VFS_DIR(self);
+	struct mlk_vfs_dir_file *file;
+
+	file = mlk_alloc_new0(1, sizeof (*file));
+
+	snprintf(file->path, sizeof (file->path), "%s/%s", dir->path, entry);
+
+	if (!(file->handle = fopen(file->path, mode))) {
+		mlk_errf("%s: %s", file->path, strerror(errno));
+		mlk_alloc_free(file);
+		return NULL;
+	}
+
+	file->file.read = file_read;
+	file->file.write = file_write;
+	file->file.flush = file_flush;
+	file->file.free = file_free;
+
+	return &file->file;
+}
+
+void
+mlk_vfs_dir_init(struct mlk_vfs_dir *dir, const char *path)
+{
+	assert(dir);
+	assert(path);
+
+	mlk_util_strlcpy(dir->path, path, sizeof (dir->path));
+
+	/* Remove terminator and switch to UNIX paths. */
+	normalize(dir->path);
+
+	dir->vfs.data = NULL;
+	dir->vfs.open = vfs_open;
+	dir->vfs.finish = NULL;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-core/mlk/core/vfs-dir.h	Wed Sep 27 22:03:17 2023 +0200
@@ -0,0 +1,58 @@
+/*
+ * vfs-dir.h -- VFS subsystem for directories
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef MLK_CORE_VFS_DIR_H
+#define MLK_CORE_VFS_DIR_H
+
+#include <mlk/util/util.h>
+
+#include "vfs.h"
+
+struct mlk_vfs_dir_file {
+	char path[MLK_PATH_MAX];
+
+	struct mlk_vfs_file file;
+
+	/** \cond MLK_PRIVATE_DECLS */
+	void *handle;
+	/** \endcond MLK_PRIVATE_DECLS */
+};
+
+struct mlk_vfs_dir {
+	/**
+	 * (read-only)
+	 *
+	 * Path to the directory.
+	 */
+	char path[MLK_PATH_MAX];
+
+	struct mlk_vfs vfs;
+};
+
+#if defined(__cplusplus)
+extern "C" {
+#endif
+
+void
+mlk_vfs_dir_init(struct mlk_vfs_dir *dir, const char *path);
+
+#if defined(__cplusplus)
+}
+#endif
+
+#endif /* !MLK_CORE_VFS_DIR_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-core/mlk/core/vfs-zip.c	Wed Sep 27 22:03:17 2023 +0200
@@ -0,0 +1,143 @@
+/*
+ * vfs-zip.c -- VFS subsystem for zip archives
+ *
+ * Copyright (c) 2020-2022 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "sysconfig.h"
+
+#if defined(MLK_WITH_ZIP)
+
+#include <assert.h>
+#include <string.h>
+
+#include <zip.h>
+
+#include "alloc.h"
+#include "err.h"
+#include "util.h"
+#include "vfs-zip.h"
+#include "vfs.h"
+
+#define MLK_VFS_ZIP_FILE(self) \
+	MLK_CONTAINER_OF(self, struct mlk_vfs_zip_file, file)
+
+#define MLK_VFS_ZIP(self) \
+	MLK_CONTAINER_OF(self, struct mlk_vfs_zip, vfs)
+
+static inline int
+mkflags(const char *mode)
+{
+	/* TODO: this should check for mutual exclusions. */
+	int flags = 0;
+
+	for (; *mode; ++mode) {
+		switch (*mode) {
+		case 'w':
+			flags |= ZIP_CREATE;
+			break;
+		case 'x':
+			flags |= ZIP_EXCL;
+			break;
+		case '+':
+			flags |= ZIP_TRUNCATE;
+			break;
+		case 'r':
+			flags |= ZIP_RDONLY;
+			break;
+		default:
+			break;
+		}
+	}
+
+	return flags;
+}
+
+static size_t
+file_read(struct mlk_vfs_file *self, void *buf, size_t bufsz)
+{
+	struct mlk_vfs_zip_file *file = MLK_VFS_ZIP_FILE(self);
+
+	return zip_fread(file->handle, buf, bufsz);
+}
+
+static int
+file_flush(struct mlk_vfs_file *self)
+{
+	(void)self;
+
+	return 0;
+}
+
+static void
+file_free(struct mlk_vfs_file *self)
+{
+	struct mlk_vfs_zip_file *file = MLK_VFS_ZIP_FILE(self);
+
+	zip_fclose(file->handle);
+	mlk_alloc_free(self);
+}
+
+static struct mlk_vfs_file *
+vfs_open(struct mlk_vfs *self, const char *entry, const char *mode)
+{
+	(void)mode;
+
+	struct mlk_vfs_zip *zip = MLK_VFS_ZIP(self);
+	struct mlk_vfs_zip_file *file;
+
+	file = mlk_alloc_new0(1, sizeof (*file));
+
+	if (!(file->handle = zip_fopen(zip->handle, entry, 0))) {
+		mlk_errf("unable to open file in archive");
+		mlk_alloc_free(file);
+		return NULL;
+	}
+
+	file->file.read = file_read;
+	file->file.flush = file_flush;
+	file->file.free = file_free;
+
+	return &file->file;
+}
+
+static void
+vfs_finish(struct mlk_vfs *self)
+{
+	struct mlk_vfs_zip *zip = MLK_VFS_ZIP(self);
+
+	zip_close(zip->handle);
+}
+
+int
+mlk_vfs_zip_init(struct mlk_vfs_zip *zip, const char *file, const char *mode)
+{
+	assert(zip);
+	assert(file);
+	assert(mode);
+
+	if (!(zip->handle = zip_open(file, mkflags(mode), NULL))) {
+		mlk_errf("%s: unable to open zip file", file);
+		return -1;
+	}
+
+	zip->vfs.data = NULL;
+	zip->vfs.open = vfs_open;
+	zip->vfs.finish = vfs_finish;
+
+	return 0;
+}
+
+#endif /* !MLK_WITH_ZIP */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-core/mlk/core/vfs-zip.h	Wed Sep 27 22:03:17 2023 +0200
@@ -0,0 +1,56 @@
+/*
+ * vfs-zip.h -- VFS subsystem for zip archives
+ *
+ * Copyright (c) 2020-2022 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef MLK_CORE_VFS_ZIP_H
+#define MLK_CORE_VFS_ZIP_H
+
+#include "sysconfig.h"
+#include "vfs.h"
+
+#if defined(MLK_WITH_ZIP)
+
+#if defined(__cplusplus)
+extern "C" {
+#endif
+
+struct mlk_vfs_zip_file {
+	struct mlk_vfs_file file;
+	
+	/** \cond MLK_PRIVATE_DECLS */
+	void *handle;
+	/** \endcond MLK_PRIVATE_DECLS */
+};
+
+struct mlk_vfs_zip {
+	struct mlk_vfs vfs;
+
+	/** \cond MLK_PRIVATE_DECLS */
+	void *handle;
+	/** \endcond MLK_PRIVATE_DECLS */
+};
+
+int
+mlk_vfs_zip_init(struct mlk_vfs_zip *zip, const char *file, const char *mode);
+
+#if defined(__cplusplus)
+}
+#endif
+
+#endif /* !MLK_WITH_ZIP */
+
+#endif /* !MLK_CORE_VFS_ZIP_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-core/mlk/core/vfs.c	Wed Sep 27 22:03:17 2023 +0200
@@ -0,0 +1,147 @@
+/*
+ * vfs.c -- virtual file system abstraction
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "alloc.h"
+#include "err.h"
+#include "vfs.h"
+#include "vfs_p.h"
+
+struct mlk_vfs_file *
+mlk_vfs_open(struct mlk_vfs *vfs,
+             const char *entry,
+             const char *mode)
+{
+	assert(vfs);
+	assert(entry);
+	assert(mode);
+
+	return vfs->open(vfs, entry, mode);
+}
+
+void
+mlk_vfs_finish(struct mlk_vfs *vfs)
+{
+	assert(vfs);
+
+	if (vfs->finish)
+		vfs->finish(vfs);
+}
+
+size_t
+mlk_vfs_file_read(struct mlk_vfs_file *file, void *buf, size_t bufsz)
+{
+	assert(file);
+	assert(buf);
+
+	return file->read(file, buf, bufsz);
+}
+
+char *
+mlk_vfs_file_aread(struct mlk_vfs_file *file, size_t *outlen)
+{
+	char data[BUFSIZ], *str;
+	size_t nr, len = 0, cap = 128;
+
+	/* Initial allocation. */
+	str = mlk_alloc_new0(cap, 1);
+
+	while ((nr = mlk_vfs_file_read(file, data, sizeof (data))) > 0) {
+		if (nr >= cap - len) {
+			cap *= 2;
+			str  = mlk_alloc_resize(str, cap);
+		}
+
+		sprintf(&str[len], "%.*s", (int)nr, data);
+		len += nr;
+	}
+
+	if (nr == (size_t)-1) {
+		mlk_alloc_free(str);
+		return NULL;
+	}
+
+	if (outlen)
+		*outlen = len;
+
+	return str;
+}
+
+size_t
+mlk_vfs_file_write(struct mlk_vfs_file *file, void *buf, size_t bufsz)
+{
+	assert(file);
+	assert(buf);
+
+	return file->write(file, buf, bufsz);
+}
+
+int
+mlk_vfs_file_flush(struct mlk_vfs_file *file)
+{
+	assert(file);
+
+	if (file->flush)
+		return file->flush(file);
+
+	return 0;
+}
+
+void
+mlk_vfs_file_free(struct mlk_vfs_file *file)
+{
+	assert(file);
+
+	if (file->free)
+		file->free(file);
+}
+
+/* private */
+
+static int
+rw_vfs_file_close(SDL_RWops *context)
+{
+	free(context->hidden.mem.base);
+	free(context);
+
+	return 0;
+}
+
+SDL_RWops *
+mlk__vfs_to_rw(struct mlk_vfs_file *file)
+{
+	SDL_RWops *ops;
+	char *data;
+	size_t datasz;
+
+	if (!(data = mlk_vfs_file_aread(file, &datasz)))
+		return NULL;
+	if (!(ops = SDL_RWFromConstMem(data, datasz))) {
+		free(data);
+		return mlk_errf("%s", SDL_GetError()), NULL;
+	}
+
+	ops->close = rw_vfs_file_close;
+
+	return ops;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-core/mlk/core/vfs.h	Wed Sep 27 22:03:17 2023 +0200
@@ -0,0 +1,273 @@
+/*
+ * vfs.h -- virtual file system abstraction
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef MLK_CORE_VFS_H
+#define MLK_CORE_VFS_H
+
+/**
+ * \file mlk/core/vfs.h
+ * \brief Virtual file system abstraction.
+ *
+ * This module offers an abstract interface to load files and perform read/write
+ * operation on them. This can be useful for games that are designed to load
+ * large assets from compressed archives.
+ *
+ * The molko frameworks comes with two implementations:
+ *
+ * | module             | support     | remarks                            |
+ * |--------------------|-------------|------------------------------------|
+ * | mlk/core/vfs-dir.h | read, write | opens file relative to a directory |
+ * | mlk/core/vfs-zip.h | read        | zip archive files extractor        |
+ *
+ * ## Initialize a VFS
+ *
+ * To use this module, you must first open a VFS interface. It is usually implemented
+ * using the ::MLK_CONTAINER_OF macro.
+ *
+ * Example with mlk/core/vfs-dir.h
+ *
+ * ```c
+ * struct mlk_vfs_dir dir;
+ *
+ * mlk_vfs_directory_init(&dir, "/usr/share/mario");
+ * ```
+ *
+ * The abstract interface will be located in the `vfs` field for each
+ * implementation.
+ *
+ * ## Opening files
+ *
+ * Once your VFS module is initialized, you can call ::mlk_vfs_open to load an
+ * entry from it:
+ *
+ * ```c
+ * struct mlk_vfs_file *file;
+ * char content[1024];
+ * size_t len;
+ *
+ * file = mlk_vfs_open(vfs, "block.png");
+ *
+ * if (!file)
+ *     handle_failure();
+ *
+ * // The function does not append a NUL terminator.
+ * len = mlk_vfs_file_read(file, content, sizeof (content) - 1);
+ *
+ * if (len == (size_t)-1)
+ *     handle_failure();
+ *
+ * // Use content freely...
+ * ```
+ *
+ * ## Cleaning up resources
+ *
+ * Opened files SHOULD be destroyed before the VFS itself because depending on
+ * the underlying implementation, it is possible that those opened files have
+ * direct references to the VFS module.
+ *
+ * ```c
+ * mlk_vfs_file_free(file);
+ * mlk_vfs_finish(&dir.vfs);
+ * ```
+ */
+
+#include <stddef.h>
+
+struct mlk_vfs_file;
+
+/**
+ * \struct mlk_vfs
+ * \brief Abstract VFS loader.
+ */
+struct mlk_vfs {
+	/**
+	 * (read-write, borrowed, optional)
+	 *
+	 * Arbitrary user data for callbacks.
+	 */
+	void *data;
+
+	/**
+	 * (read-write)
+	 *
+	 * Open the file from the VFS.
+	 *
+	 * The mode argument must contain the following letters:
+	 *
+	 * - `+`: truncate file if exists
+	 * - `r`: open for reading
+	 * - `w`: open for writing
+	 * - `x`: open exclusive mode
+	 *
+	 * \pre self != NULL
+	 * \pre entry != NULL
+	 * \pre mode != NULL
+	 * \param self this vfs
+	 * \param entry the entry filename
+	 * \param mode open mode as described above
+	 * \return a VFS file or NULL on failure
+	 */
+	struct mlk_vfs_file * (*open)(struct mlk_vfs *self,
+	                              const char *entry,
+	                              const char *mode);
+
+	/**
+	 * (read-write)
+	 *
+	 * Cleanup resources.
+	 *
+	 * \pre self != NULL
+	 * \param self this vfs
+	 */
+	void (*finish)(struct mlk_vfs *self);
+};
+
+/**
+ * \struct mlk_vfs_file
+ * \brief Abstract VFS file.
+ */
+struct mlk_vfs_file {
+	/**
+	 * (read-write, borrowed, optional)
+	 *
+	 * Arbitrary user data for callbacks.
+	 */
+	void *data;
+
+	/**
+	 * (read-write, optional)
+	 *
+	 * Attempt to read file entry into the buffer destination. The function
+	 * can read less bytes than requested.
+	 *
+	 * If the function is NULL, an error is returned with an error string
+	 * telling that the operation is not supported.
+	 *
+	 * \pre self != NULL
+	 * \pre buf != NULL
+	 * \param self this VFS file
+	 * \param buf the buffer destination
+	 * \param bufsz maximum buffer size to read
+	 * \return the number of bytes read or -1 on error
+	 */
+	size_t (*read)(struct mlk_vfs_file *self, void *buf, size_t bufsz);
+
+	/**
+	 * (read-write, optional)
+	 *
+	 * Attempt to write file entry from the buffer source. The function can
+	 * write less bytes than requested.
+	 *
+	 * If the function is NULL, an error is returned with an error string
+	 * telling that the operation is not supported.
+	 *
+	 * \pre self != NULL
+	 * \pre buf != NULL
+	 * \param self this VFS file
+	 * \param buf the buffer source
+	 * \param bufsz the buffer length
+	 * \return the number of bytes written or -1 on error
+	 */
+	size_t (*write)(struct mlk_vfs_file *self, const void *buf, size_t bufsz);
+
+	/**
+	 * (read-write, optional)
+	 *
+	 * Attempt to flush pending input/output on the underlying interface.
+	 *
+	 * If the function is NULL, it is considered success.
+	 *
+	 * \pre self != NULL
+	 * \param self this VFS file
+	 * \return 0 on success or -1 on error
+	 */
+	int (*flush)(struct mlk_vfs_file *self);
+
+	/**
+	 * (read-write, optional)
+	 *
+	 * Cleanup resources for this VFS file.
+	 *
+	 * \pre self != NULL
+	 * \param self this VFS file
+	 */
+	void (*free)(struct mlk_vfs_file *self);
+};
+
+#if defined(__cplusplus)
+extern "C"
+#endif
+
+/**
+ * Wrapper of ::mlk_vfs::open if not NULL.
+ */
+struct mlk_vfs_file *
+mlk_vfs_open(struct mlk_vfs *vfs,
+             const char *entry,
+             const char *mode);
+
+/**
+ * Wrapper of ::mlk_vfs::finish if not NULL.
+ */
+void
+mlk_vfs_finish(struct mlk_vfs *vfs);
+
+/**
+ * Wrapper of ::mlk_vfs_file::read if not NULL.
+ */
+size_t
+mlk_vfs_file_read(struct mlk_vfs_file *file, void *buf, size_t bufsz);
+
+/**
+ * Convenient function to read an entire file content using repeated calls to
+ * ::mlk_vfs_file_read function until end of file is reached.
+ *
+ * The returned string is dynamically allocated and must be free'd using
+ * ::mlk_alloc_free function.
+ *
+ * \pre file != NULL
+ * \param file this VFS file
+ * \param len pointer receiving the number of bytes read (can be NULL)
+ * \return the string content
+ */
+char *
+mlk_vfs_file_aread(struct mlk_vfs_file *file, size_t *len);
+
+/**
+ * Wrapper of ::mlk_vfs::finish if not NULL.
+ */
+size_t
+mlk_vfs_file_write(struct mlk_vfs_file *file, void *buf, size_t bufsz);
+
+/**
+ * Wrapper of ::mlk_vfs::finish if not NULL.
+ */
+int
+mlk_vfs_file_flush(struct mlk_vfs_file *file);
+
+/**
+ * Wrapper of ::mlk_vfs::finish if not NULL.
+ */
+void
+mlk_vfs_file_free(struct mlk_vfs_file *file);
+
+#if defined(__cplusplus)
+}
+#endif
+
+#endif /* MLK_CORE_VFS_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-core/mlk/core/vfs_p.h	Wed Sep 27 22:03:17 2023 +0200
@@ -0,0 +1,29 @@
+/*
+ * vfs.h -- (PRIVATE) virtual file system abstraction
+ *
+ * Copyright (c) 2020-2022 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef MLK_CORE_VFS_P_H
+#define MLK_CORE_VFS_P_H
+
+#include <SDL.h>
+
+struct mlk_vfs_file;
+
+SDL_RWops *
+mlk__vfs_to_rw(struct mlk_vfs_file *);
+
+#endif /* !MLK_CORE_VFS_P_H */
--- a/libmlk-util/mlk/util/sysconfig.cmake.h	Sat Sep 23 21:04:16 2023 +0200
+++ b/libmlk-util/mlk/util/sysconfig.cmake.h	Wed Sep 27 22:03:17 2023 +0200
@@ -25,6 +25,7 @@
 #define MLK_LOCALEDIR   "@CMAKE_INSTALL_LOCALEDIR@"
 
 #cmakedefine MLK_WITH_NLS
+#cmakedefine MLK_WITH_ZIP
 
 #cmakedefine MLK_HAVE_PATH_MAX
 
--- a/tests/CMakeLists.txt	Sat Sep 23 21:04:16 2023 +0200
+++ b/tests/CMakeLists.txt	Wed Sep 27 22:03:17 2023 +0200
@@ -30,13 +30,15 @@
 	save-quest
 	state
 	util
+	vfs-dir
 )
 
+if (MLK_WITH_ZIP)
+	list(APPEND TESTS vfs-zip)
+endif ()
+
 if (MLK_WITH_TESTS_GRAPHICAL)
-	list(
-		APPEND TESTS
-		tileset
-	)
+	list(APPEND TESTS tileset)
 endif ()
 
 foreach (t ${TESTS})
Binary file tests/assets/vfs/data.zip has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/assets/vfs/directory/hello.txt	Wed Sep 27 22:03:17 2023 +0200
@@ -0,0 +1,1 @@
+Hello World!
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-vfs-dir.c	Wed Sep 27 22:03:17 2023 +0200
@@ -0,0 +1,58 @@
+/*
+ * test-vfs-directory.c -- test VFS directory
+ *
+ * Copyright (c) 2020-2022 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <mlk/core/vfs-dir.h>
+
+#include <dt.h>
+
+static void
+test_basics_read(void)
+{
+	struct mlk_vfs_dir dir;
+	struct mlk_vfs_file *file;
+	char data[256] = {};
+
+	mlk_vfs_dir_init(&dir, DIRECTORY "/vfs/directory");
+
+	DT_ASSERT(file = mlk_vfs_open(&dir.vfs, "hello.txt", "r"));
+	DT_EQ_UINT(mlk_vfs_file_read(file, data, sizeof (data)), 13U);
+	DT_EQ_STR(data, "Hello World!\n");
+
+	mlk_vfs_file_free(file);
+	mlk_vfs_finish(&dir.vfs);
+}
+
+static void
+test_error_notfound(void)
+{
+	struct mlk_vfs_dir dir;
+
+	mlk_vfs_dir_init(&dir, DIRECTORY "/vfs/directory");
+
+	DT_ASSERT(!mlk_vfs_open(&dir.vfs, "notfound.txt", "r"));
+
+	mlk_vfs_finish(&dir.vfs);
+}
+
+int
+main(void)
+{
+	DT_RUN(test_basics_read);
+	DT_RUN(test_error_notfound);
+	DT_SUMMARY();
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-vfs-zip.c	Wed Sep 27 22:03:17 2023 +0200
@@ -0,0 +1,56 @@
+/*
+ * test-zip-directory.c -- test VFS zip
+ *
+ * Copyright (c) 2020-2022 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <mlk/core/vfs-zip.h>
+
+#include <dt.h>
+
+static void
+test_basics_read(void)
+{
+	struct mlk_vfs_zip zip;
+	struct mlk_vfs_file *file;
+	char data[256] = {};
+
+	DT_EQ_INT(mlk_vfs_zip_init(&zip, DIRECTORY "/vfs/data.zip", "r"), 0);
+	DT_ASSERT(file = mlk_vfs_open(&zip.vfs, "texts/hello.txt", "r"));
+	DT_EQ_UINT(mlk_vfs_file_read(file, data, sizeof (data)), 21U);
+	DT_EQ_STR(data, "Hello from zip file!\n");
+
+	mlk_vfs_file_free(file);
+	mlk_vfs_finish(&zip.vfs);
+}
+
+static void
+test_error_notfound(void)
+{
+	struct mlk_vfs_zip zip;
+
+	DT_EQ_INT(mlk_vfs_zip_init(&zip, DIRECTORY "/vfs/data.zip", "r"), 0);
+	DT_ASSERT(!mlk_vfs_open(&zip.vfs, "notfound.txt", "r"));
+
+	mlk_vfs_finish(&zip.vfs);
+}
+
+int
+main(void)
+{
+	DT_RUN(test_basics_read);
+	DT_RUN(test_error_notfound);
+	DT_SUMMARY();
+}