changeset 641:fcd124e513ea

core: reintroduce VFS
author David Demelier <markand@malikania.fr>
date Sun, 01 Oct 2023 09:18:01 +0200
parents 9850089c9671
children 05b585720d3b
files CMakeLists.txt doc/Doxyfile libmlk-core/mlk/core/font.c libmlk-core/mlk/core/font.h libmlk-core/mlk/core/image.c libmlk-core/mlk/core/image.h libmlk-core/mlk/core/music.c libmlk-core/mlk/core/music.h libmlk-core/mlk/core/sound.c libmlk-core/mlk/core/sound.h 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 tests/test-vfs-dir.c tests/test-vfs-zip.c
diffstat 18 files changed, 632 insertions(+), 162 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Wed Sep 27 22:03:17 2023 +0200
+++ b/CMakeLists.txt	Sun Oct 01 09:18:01 2023 +0200
@@ -128,6 +128,7 @@
 	FILES
 		${molko_BINARY_DIR}/cmake/MlkOptions.cmake
 		${molko_SOURCE_DIR}/cmake/FindSndFile.cmake
+		${molko_SOURCE_DIR}/cmake/FindZIP.cmake
 		${molko_SOURCE_DIR}/cmake/MlkBcc.cmake
 		${molko_SOURCE_DIR}/cmake/MlkMap.cmake
 		${molko_SOURCE_DIR}/cmake/MlkTileset.cmake
--- a/doc/Doxyfile	Wed Sep 27 22:03:17 2023 +0200
+++ b/doc/Doxyfile	Sun Oct 01 09:18:01 2023 +0200
@@ -40,6 +40,7 @@
                          libmlk-core/mlk/core/core_p.h          \
                          libmlk-core/mlk/core/sys_p.h           \
                          libmlk-core/mlk/core/texture_p.h       \
+                         libmlk-core/mlk/core/vfs_p.h           \
                          libmlk-core/mlk/core/window_p.h        \
                          libmlk-ui/mlk/ui/ui_p.h
 
--- a/libmlk-core/mlk/core/font.c	Wed Sep 27 22:03:17 2023 +0200
+++ b/libmlk-core/mlk/core/font.c	Sun Oct 01 09:18:01 2023 +0200
@@ -26,6 +26,8 @@
 #include "font.h"
 #include "texture_p.h"
 #include "util.h"
+#include "vfs.h"
+#include "vfs_p.h"
 
 int
 mlk_font_open(struct mlk_font *font, const char *path, unsigned int size)
@@ -58,6 +60,22 @@
 }
 
 int
+mlk_font_openvfs(struct mlk_font *font, struct mlk_vfs_file *file, unsigned int size)
+{
+	assert(font);
+	assert(file);
+
+	SDL_RWops *ops;
+
+	if (!(ops = mlk__vfs_to_rw(file)))
+		return -1;
+	if (!(font->handle = TTF_OpenFontRW(ops, 1, size)))
+		return mlk_errf("%s", SDL_GetError());
+
+	return 0;
+}
+
+int
 mlk_font_render(struct mlk_font *font, struct mlk_texture *tex, const char *text, unsigned long color)
 {
 	assert(font);
--- a/libmlk-core/mlk/core/font.h	Wed Sep 27 22:03:17 2023 +0200
+++ b/libmlk-core/mlk/core/font.h	Sun Oct 01 09:18:01 2023 +0200
@@ -29,6 +29,7 @@
 #include <stddef.h>
 
 struct mlk_texture;
+struct mlk_vfs_file;
 
 /**
  * \enum mlk_font_style
@@ -83,7 +84,7 @@
  * \param font the font to open
  * \param path path to the font file (e.g. .ttf, .otf, etc)
  * \param size desired font height in pixels
- * \return 0 on success or an error code on failure
+ * \return 0 on success or -1 on error
  */
 int
 mlk_font_open(struct mlk_font *font, const char *path, unsigned int size);
@@ -98,7 +99,7 @@
  * \param data the font content
  * \param datasz the font content length
  * \param size desired font height in pixels
- * \return 0 on success or an error code on failure
+ * \return 0 on success or -1 on error
  */
 int
 mlk_font_openmem(struct mlk_font *font,
@@ -107,19 +108,34 @@
                  unsigned int size);
 
 /**
+ * Open a font from a virtual file system.
+ *
+ * The VFS file can be discarded after loading the font.
+ *
+ * \pre font != NULL
+ * \pre file != NULL
+ * \param font the font to initialize
+ * \param file the VFS file
+ * \param size desired font height in pixels
+ * \return 0 on success or -1 on error
+ */
+int
+mlk_font_openvfs(struct mlk_font *font, struct mlk_vfs_file *file, unsigned int size);
+
+/**
  * Render some text using the font and generate a texture.
  *
  * The texture destination must be deallocated once no longer used using
  * ::mlk_texture_finish.
  *
- * \pre mlk_font_ok(font)
+ * \pre font != NULL
  * \pre texture != NULL
  * \pre text != NULL && strlen(text) > 0
  * \param font the font to use
  * \param texture the texture to initialize
  * \param text the non NULL and non empty UTF-8 text
  * \param color foreground color
- * \return 0 on success or an error code on failure
+ * \return 0 on success or -1 on error
  */
 int
 mlk_font_render(struct mlk_font *font,
@@ -130,7 +146,7 @@
 /**
  * Return the font height in pixels
  *
- * \pre mlk_font_ok(font)
+ * \pre font != NULL
  * \param font the font to use
  * \return the font height
  */
@@ -144,12 +160,12 @@
  *
  * If the function fails, *w and *h are set to 0.
  *
- * \pre mlk_font_ok(font)
+ * \pre font != NULL
  * \param font the font to use
  * \param text the non NULL and non empty UTF-8 text
  * \param w pointer receiving width (or NULL)
  * \param h pointer receiving height (or NULL)
- * \return 0 on success or an error code on failure
+ * \return 0 on success or -1 on error
  */
 int
 mlk_font_query(const struct mlk_font *font,
--- a/libmlk-core/mlk/core/image.c	Wed Sep 27 22:03:17 2023 +0200
+++ b/libmlk-core/mlk/core/image.c	Sun Oct 01 09:18:01 2023 +0200
@@ -22,6 +22,8 @@
 
 #include "err.h"
 #include "texture.h"
+#include "vfs.h"
+#include "vfs_p.h"
 #include "window.h"
 #include "window_p.h"
 
@@ -55,6 +57,7 @@
 int
 mlk_image_openmem(struct mlk_texture *tex, const void *buffer, size_t size)
 {
+	assert(tex);
 	assert(buffer);
 
 	SDL_RWops *ops = SDL_RWFromConstMem(buffer, size);
@@ -66,3 +69,21 @@
 
 	return 0;
 }
+
+int
+mlk_image_openvfs(struct mlk_texture *tex, struct mlk_vfs_file *file)
+{
+	assert(tex);
+	assert(file);
+
+	SDL_RWops *ops;
+
+	if (!(ops = mlk__vfs_to_rw(file)))
+		return -1;
+	if (!(tex->handle = IMG_LoadTexture_RW(MLK__RENDERER(), ops, 1)))
+		return mlk_errf("%s", SDL_GetError());
+
+	dimensions(tex);
+
+	return 0;
+}
--- a/libmlk-core/mlk/core/image.h	Wed Sep 27 22:03:17 2023 +0200
+++ b/libmlk-core/mlk/core/image.h	Sun Oct 01 09:18:01 2023 +0200
@@ -27,6 +27,7 @@
 #include <stddef.h>
 
 struct mlk_texture;
+struct mlk_vfs_file;
 
 #if defined(__cplusplus)
 extern "C" {
@@ -60,6 +61,20 @@
 int
 mlk_image_openmem(struct mlk_texture *texture, const void *data, size_t datasz);
 
+/**
+ * Open an image from a virtual file system.
+ *
+ * The VFS file can be discarded after loading the image.
+ *
+ * \pre texture != NULL
+ * \pre file != NULL
+ * \param texture the texture to initialize
+ * \param file the VFS file
+ * \return 0 on success or -1 on error
+ */
+int
+mlk_image_openvfs(struct mlk_texture *texture, struct mlk_vfs_file *file);
+
 #if defined(__cplusplus)
 }
 #endif
--- a/libmlk-core/mlk/core/music.c	Wed Sep 27 22:03:17 2023 +0200
+++ b/libmlk-core/mlk/core/music.c	Sun Oct 01 09:18:01 2023 +0200
@@ -19,9 +19,12 @@
 #include <assert.h>
 #include <string.h>
 
+#include "alloc.h"
 #include "err.h"
 #include "music.h"
 #include "sys_p.h"
+#include "vfs.h"
+#include "vfs_p.h"
 
 #define SOURCE(mus) ((const struct mlk__audiostream *)mus->handle)->source
 
@@ -44,6 +47,26 @@
 }
 
 int
+mlk_music_openvfs(struct mlk_music *music, struct mlk_vfs_file *file)
+{
+	assert(music);
+	assert(file);
+
+	char *data;
+	size_t datasz;
+	int ret = 0;
+
+	if (!(data = mlk_vfs_file_read_all(file, &datasz)))
+		return -1;
+	if (mlk__audiostream_openmem((struct mlk__audiostream **)&music->handle, data, datasz) < 0)
+		ret = -1;
+
+	mlk_alloc_free(data);
+
+	return ret;
+}
+
+int
 mlk_music_ok(const struct mlk_music *mus)
 {
 	return mus && mus->handle;
--- a/libmlk-core/mlk/core/music.h	Wed Sep 27 22:03:17 2023 +0200
+++ b/libmlk-core/mlk/core/music.h	Sun Oct 01 09:18:01 2023 +0200
@@ -29,6 +29,8 @@
 
 #include <stddef.h>
 
+struct mlk_vfs_file;
+
 /**
  * \enum mlk_music_flags
  * \brief Music flags
@@ -70,7 +72,7 @@
  * \pre path != NULL
  * \param music the music to initialize
  * \param path the path to the music file (e.g. .ogg, .wav, .mp3, etc)
- * \return 0 on success or an error code on failure
+ * \return 0 on success or -1 on error
  */
 int
 mlk_music_open(struct mlk_music *music, const char *path);
@@ -83,29 +85,34 @@
  * \pre music != NULL
  * \pre path != NULL
  * \param music the music to initialize
- * \param data the font content
- * \param datasz the font content length
- * \return 0 on success or an error code on failure
+ * \param data the music data
+ * \param datasz the music data length
+ * \return 0 on success or -1 on error
  */
 int
 mlk_music_openmem(struct mlk_music *music, const void *data, size_t datasz);
 
 /**
- * Tells if the music structure is usable.
+ * Open a music from a virtual file system.
+ *
+ * The VFS file can be discarded after loading the sound.
  *
- * \param music the music to check
- * \return non-zero if the music structure is usable
+ * \pre music != NULL
+ * \pre file != NULL
+ * \param music the music to initialize
+ * \param file the VFS file
+ * \return 0 on success or -1 on error
  */
 int
-mlk_music_ok(const struct mlk_music *music);
+mlk_music_openvfs(struct mlk_music *music, struct mlk_vfs_file *file);
 
 /**
  * Start playing the music.
  *
- * \pre mlk_music_ok(music)
+ * \pre music != NULL
  * \param music the music to play
  * \param flags optional flags to pass
- * \return 0 on success or an error code on failure
+ * \return 0 on success or -1 on error
  */
 int
 mlk_music_play(struct mlk_music *music, enum mlk_music_flags flags);
@@ -113,7 +120,7 @@
 /**
  * Pause the music playback.
  *
- * \pre mlk_music_ok(music)
+ * \pre music != NULL
  * \param music the music to pause
  * \sa ::mlk_music_resume
  */
@@ -123,7 +130,7 @@
 /**
  * Resume the music where it was stopped.
  *
- * \pre mlk_music_ok(music)
+ * \pre music != NULL
  * \param music the music to resume
  * \sa ::mlk_music_pause
  */
@@ -135,7 +142,7 @@
  *
  * Calling ::mlk_music_resume on it will restart from the beginning.
  *
- * \pre mlk_music_ok(music)
+ * \pre music != NULL
  * \param music the music to stop
  * \sa ::mlk_music_resume
  * \sa ::mlk_music_play
@@ -148,7 +155,7 @@
  *
  * If the music is being played, it is stopped immediately.
  *
- * \pre mlk_music_ok(music)
+ * \pre music != NULL
  * \param music the music to destroy
  */
 void
--- a/libmlk-core/mlk/core/sound.c	Wed Sep 27 22:03:17 2023 +0200
+++ b/libmlk-core/mlk/core/sound.c	Sun Oct 01 09:18:01 2023 +0200
@@ -20,8 +20,10 @@
 #include <stdio.h>
 #include <string.h>
 
+#include "alloc.h"
 #include "sound.h"
 #include "sys_p.h"
+#include "vfs.h"
 
 #define SOURCE(snd) ((const struct mlk__audiostream *)snd->handle)->source
 
@@ -44,15 +46,29 @@
 }
 
 int
-mlk_sound_ok(const struct mlk_sound *snd)
+mlk_sound_openvfs(struct mlk_sound *snd, struct mlk_vfs_file *file)
 {
-	return snd && snd->handle;
+	assert(snd);
+	assert(file);
+
+	char *data;
+	size_t datasz;
+	int ret = 0;
+
+	if (!(data = mlk_vfs_file_read_all(file, &datasz)))
+		return -1;
+	if (mlk__audiostream_openmem((struct mlk__audiostream **)&snd->handle, data, datasz) < 0)
+		ret = -1;
+
+	mlk_alloc_free(data);
+
+	return ret;
 }
 
 int
 mlk_sound_play(struct mlk_sound *snd)
 {
-	assert(mlk_sound_ok(snd));
+	assert(snd);
 
 	alSourcePlay(SOURCE(snd));
 
@@ -62,7 +78,7 @@
 void
 mlk_sound_pause(struct mlk_sound *snd)
 {
-	assert(mlk_sound_ok(snd));
+	assert(snd);
 
 	alSourcePause(SOURCE(snd));
 }
@@ -70,7 +86,7 @@
 void
 mlk_sound_resume(struct mlk_sound *snd)
 {
-	assert(mlk_sound_ok(snd));
+	assert(snd);
 
 	alSourcePlay(SOURCE(snd));
 }
@@ -78,7 +94,7 @@
 void
 mlk_sound_stop(struct mlk_sound *snd)
 {
-	assert(mlk_sound_ok(snd));
+	assert(snd);
 
 	alSourceStop(SOURCE(snd));
 }
--- a/libmlk-core/mlk/core/sound.h	Wed Sep 27 22:03:17 2023 +0200
+++ b/libmlk-core/mlk/core/sound.h	Sun Oct 01 09:18:01 2023 +0200
@@ -31,6 +31,8 @@
 
 #include <stddef.h>
 
+struct mlk_vfs_file;
+
 /**
  * \struct mlk_music
  * \brief Music structure
@@ -54,7 +56,7 @@
  * \pre path != NULL
  * \param sound the sound to initialize
  * \param path the path to the music file (e.g. .ogg, .wav, .mp3, etc)
- * \return 0 on success or an error code on failure
+ * \return 0 on success or -1 on error
  */
 int
 mlk_sound_open(struct mlk_sound *sound, const char *path);
@@ -67,28 +69,33 @@
  * \pre music != NULL
  * \pre path != NULL
  * \param sound the sound to initialize
- * \param data the font content
- * \param datasz the font content length
- * \return 0 on success or an error code on failure
+ * \param data the sound data
+ * \param datasz the sound data length
+ * \return 0 on success or -1 on error
  */
 int
 mlk_sound_openmem(struct mlk_sound *sound, const void *data, size_t datasz);
 
 /**
- * Tells if the sound structure is usable.
+ * Open a sound from a virtual file system.
+ *
+ * The VFS file can be discarded after loading the sound.
  *
- * \param sound the sound to check
- * \return non-zero if the sound structure is usable
+ * \pre sound != NULL
+ * \pre file != NULL
+ * \param sound the sound to initialize
+ * \param file the VFS file
+ * \return 0 on success or -1 on error
  */
 int
-mlk_sound_ok(const struct mlk_sound *sound);
+mlk_sound_openvfs(struct mlk_sound *snd, struct mlk_vfs_file *file);
 
 /**
  * Start playing the sound.
  *
- * \pre mlk_sound_ok(sound)
+ * \pre sound != NULL
  * \param sound the sound to play
- * \return 0 on success or an error code on failure
+ * \return 0 on success or -1 on error
  */
 int
 mlk_sound_play(struct mlk_sound *sound);
@@ -96,7 +103,7 @@
 /**
  * Pause the sound playback.
  *
- * \pre mlk_sound_ok(sound)
+ * \pre sound != NULL
  * \param sound the sound to pause
  * \sa ::mlk_sound_resume
  */
@@ -106,7 +113,7 @@
 /**
  * Resume the sound where it was stopped.
  *
- * \pre mlk_sound_ok(sound)
+ * \pre sound != NULL
  * \param sound the sound to resume
  * \sa ::mlk_sound_pause
  */
@@ -118,7 +125,7 @@
  *
  * Calling ::mlk_sound_resume on it will restart from the beginning.
  *
- * \pre mlk_sound_ok(sound)
+ * \pre sound != NULL
  * \param sound the sound to stop
  * \sa ::mlk_sound_resume
  * \sa ::mlk_sound_play
--- a/libmlk-core/mlk/core/vfs-dir.c	Wed Sep 27 22:03:17 2023 +0200
+++ b/libmlk-core/mlk/core/vfs-dir.c	Sun Oct 01 09:18:01 2023 +0200
@@ -38,7 +38,9 @@
 static inline void
 normalize(char *path)
 {
-	size_t len = strlen(path);
+	size_t len;
+
+	len = strlen(path);
 
 	for (char *p = path; *p; ++p)
 		if (*p == '\\')
@@ -48,73 +50,129 @@
 		path[--len] = 0;
 }
 
+/*
+ * TODO
+ *
+ * Add an optional POSIX implementation based on open(2) if function is
+ * available.
+ */
+static FILE *
+makefile(const char *path, const char *mode)
+{
+	FILE *fp;
+	const char *fmode = NULL;
+	int r = 0, w = 0, e = 0, t = 0, seek = 0, flags, whence;
+	long offset;
+	size_t len;
+
+	/*
+	 * Determine which read-write mode we need, if unset by the user assumes
+	 * read-only for convenience.
+	 *
+	 * Conversion as following:
+	 *
+	 * | mode  | fopen mode | fseek?  | tewr        |
+	 * |-------+------------+---------+-------------|
+	 * | r     | rb         | no      | 0001 (0x01) |
+	 * | w     | r+b        | yes, 0  | 0010 (0x02) |
+	 * | we    | r+b        | yes, -1 | 0110 (0x06) |
+	 * | rw    | r+b        | yes, 0  | 0011 (0x03) |
+	 * | rwe   | r+b        | yes, -1 | 0111 (0x07) |
+	 * | wt    | wb         | no      | 1010 (0x0a) |
+	 * | rwt   | w+b        | no      | 1011 (0x0b) |
+	 *
+	 * Notes:
+	 *
+	 * Standard C function fopen does not have support to open a file for
+	 * writing only at the beginning (without destroying content) so we have
+	 * to read-write with a call to fseek if at-end is requested. The mode
+	 * 'a' is never applicable because it always append to the file no
+	 * matter fseek position.
+	 *
+	 * We could use open(2) POSIX function but that would make the code less
+	 * portable.
+	 */
+	len = strlen(mode);
+
+	r = strcspn(mode, "r") != len;
+	w = strcspn(mode, "w") != len;
+	e = strcspn(mode, "e") != len;
+	t = strcspn(mode, "t") != len;
+
+	/* For simplicity, convert the individual open mode into a bitmask. */
+	flags = r << 0 | w << 1 | e << 2 | t << 3;
+
+	switch (flags) {
+	case 0x01:
+		fmode = "rb";
+		break;
+	case 0x02:
+	case 0x03:
+		fmode = "r+b";
+		seek = 1;
+		offset = 0;
+		whence = SEEK_SET;
+		break;
+	case 0x06:
+	case 0x07:
+		fmode = "r+b";
+		seek = 1;
+		offset = 0;
+		whence = SEEK_END;
+		break;
+	case 0x0a:
+		fmode = "wb";
+		break;
+	case 0x0b:
+		fmode = "w+b";
+		break;
+	default:
+		mlk_errf("incompatible modes specified");
+		return NULL;
+	}
+
+	if (!(fp = fopen(path, fmode))) {
+		mlk_errf("%s", strerror(errno));
+		return NULL;
+	}
+
+	if (seek && fseek(fp, offset, whence) < 0) {
+		fclose(fp);
+		mlk_errf("%s", strerror(errno));
+		return NULL;
+	}
+
+	return fp;
+}
+
 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;
+	return mlk_vfs_dir_file_read(MLK_VFS_DIR_FILE(self), buf, bufsz);
 }
 
 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;
+	return mlk_vfs_dir_file_write(MLK_VFS_DIR_FILE(self), buf, bufsz);
 }
 
 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;
+	return mlk_vfs_dir_file_flush(MLK_VFS_DIR_FILE(self));
 }
 
 static void
-file_free(struct mlk_vfs_file *self)
+file_finish(struct mlk_vfs_file *self)
 {
-	struct mlk_vfs_dir_file *file = MLK_VFS_DIR_FILE(self);
-
-	fclose(file->handle);
-	mlk_alloc_free(file);
+	mlk_vfs_dir_file_finish(MLK_VFS_DIR_FILE(self));
 }
 
 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;
+	return mlk_vfs_dir_open(MLK_VFS_DIR(self), entry, mode);
 }
 
 void
@@ -132,3 +190,83 @@
 	dir->vfs.open = vfs_open;
 	dir->vfs.finish = NULL;
 }
+
+struct mlk_vfs_file *
+mlk_vfs_dir_open(struct mlk_vfs_dir *dir, const char *entry, const char *mode)
+{
+	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 = makefile(file->path, mode))) {
+		mlk_alloc_free(file);
+		return NULL;
+	}
+
+	file->file.read = file_read;
+	file->file.write = file_write;
+	file->file.flush = file_flush;
+	file->file.finish = file_finish;
+
+	return &file->file;
+}
+
+void
+mlk_vfs_dir_finish(struct mlk_vfs_dir *dir)
+{
+	assert(dir);
+
+	/* Kept for future use. */
+	(void)dir;
+}
+
+size_t
+mlk_vfs_dir_file_read(struct mlk_vfs_dir_file *file, void *buf, size_t bufsz)
+{
+	assert(file);
+	assert(buf);
+
+	size_t rv;
+
+	rv = fread(buf, 1, bufsz, file->handle);
+
+	if (ferror(file->handle))
+		return -1;
+
+	return rv;
+}
+
+size_t
+mlk_vfs_dir_file_write(struct mlk_vfs_dir_file *file, const void *buf, size_t bufsz)
+{
+	assert(file);
+	assert(buf);
+
+	size_t rv;
+
+	rv = fwrite(buf, 1, bufsz, file->handle);
+
+	if (ferror(file->handle))
+		return -1;
+
+	return rv;
+}
+
+int
+mlk_vfs_dir_file_flush(struct mlk_vfs_dir_file *file)
+{
+	assert(file);
+
+	return fflush(file->handle) == EOF ? -1 : 0;
+}
+
+void
+mlk_vfs_dir_file_finish(struct mlk_vfs_dir_file *file)
+{
+	assert(file);
+
+	fclose(file->handle);
+	mlk_alloc_free(file);
+}
--- a/libmlk-core/mlk/core/vfs-dir.h	Wed Sep 27 22:03:17 2023 +0200
+++ b/libmlk-core/mlk/core/vfs-dir.h	Sun Oct 01 09:18:01 2023 +0200
@@ -19,13 +19,52 @@
 #ifndef MLK_CORE_VFS_DIR_H
 #define MLK_CORE_VFS_DIR_H
 
+/**
+ * \file mlk/core/vfs-dir.h
+ * \brief VFS subsystem for directories.
+ *
+ * This module can be used to read files relative to a filesystem directory path
+ * using mlk/core/vfs.h abstract VFS module.
+ *
+ * It is implemented using the ::MLK_CONTAINER_OF macro which means you can
+ * use it and derive from it to add or modify its functions.
+ *
+ * ## Members used
+ *
+ * The following VFS members are used:
+ *
+ * - ::mlk_vfs::finish
+ * - ::mlk_vfs::open
+ *
+ * The following VFS file member are used:
+ *
+ * - ::mlk_vfs_file::finish
+ * - ::mlk_vfs_file::flush
+ * - ::mlk_vfs_file::read
+ * - ::mlk_vfs_file::write
+ */
+
 #include <mlk/util/util.h>
 
 #include "vfs.h"
 
+/**
+ * \struct mlk_vfs_dir_file
+ * \brief VFS file implementation for filesystem files.
+ */
 struct mlk_vfs_dir_file {
+	/**
+	 * (read-only)
+	 *
+	 * Path to the opened file.
+	 */
 	char path[MLK_PATH_MAX];
 
+	/**
+	 * (read-write)
+	 *
+	 * Abstract VFS file to implement.
+	 */
 	struct mlk_vfs_file file;
 
 	/** \cond MLK_PRIVATE_DECLS */
@@ -33,6 +72,10 @@
 	/** \endcond MLK_PRIVATE_DECLS */
 };
 
+/**
+ * \struct mlk_vfs_zip
+ * \brief VFS implementation for filesystem directories.
+ */
 struct mlk_vfs_dir {
 	/**
 	 * (read-only)
@@ -41,6 +84,11 @@
 	 */
 	char path[MLK_PATH_MAX];
 
+	/**
+	 * (read-write)
+	 *
+	 * Abstract VFS to implement.
+	 */
 	struct mlk_vfs vfs;
 };
 
@@ -48,9 +96,53 @@
 extern "C" {
 #endif
 
+/**
+ * Initialize the directory object and its underlying VFS module.
+ *
+ * \pre dir != NULL
+ * \pre path != NULL
+ * \param dir the dir implementation to initialize
+ * \param path the path to the directory
+ */
 void
 mlk_vfs_dir_init(struct mlk_vfs_dir *dir, const char *path);
 
+/**
+ * Implements ::mlk_vfs::open virtual function.
+ */
+struct mlk_vfs_file *
+mlk_vfs_dir_open(struct mlk_vfs_dir *dir, const char *entry, const char *mode);
+
+/**
+ * Implements ::mlk_vfs::finish virtual function.
+ */
+void
+mlk_vfs_dir_finish(struct mlk_vfs_dir *dir);
+
+/**
+ * Implements ::mlk_vfs_file::read virtual function.
+ */
+size_t
+mlk_vfs_dir_file_read(struct mlk_vfs_dir_file *file, void *buf, size_t bufsz);
+
+/**
+ * Implements ::mlk_vfs_file::write virtual function.
+ */
+size_t
+mlk_vfs_dir_file_write(struct mlk_vfs_dir_file *file, const void *buf, size_t bufsz);
+
+/**
+ * Implements ::mlk_vfs_file::flush virtual function.
+ */
+int
+mlk_vfs_dir_file_flush(struct mlk_vfs_dir_file *file);
+
+/**
+ * Implements ::mlk_vfs_file::finish virtual function.
+ */
+void
+mlk_vfs_dir_file_finish(struct mlk_vfs_dir_file *file);
+
 #if defined(__cplusplus)
 }
 #endif
--- a/libmlk-core/mlk/core/vfs-zip.c	Wed Sep 27 22:03:17 2023 +0200
+++ b/libmlk-core/mlk/core/vfs-zip.c	Sun Oct 01 09:18:01 2023 +0200
@@ -68,57 +68,25 @@
 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;
+	return mlk_vfs_zip_file_read(MLK_VFS_ZIP_FILE(self), buf, bufsz);
 }
 
 static void
-file_free(struct mlk_vfs_file *self)
+file_finish(struct mlk_vfs_file *self)
 {
-	struct mlk_vfs_zip_file *file = MLK_VFS_ZIP_FILE(self);
-
-	zip_fclose(file->handle);
-	mlk_alloc_free(self);
+	mlk_vfs_zip_file_finish(MLK_VFS_ZIP_FILE(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;
+	return mlk_vfs_zip_open(MLK_VFS_ZIP(self), entry, mode);
 }
 
 static void
 vfs_finish(struct mlk_vfs *self)
 {
-	struct mlk_vfs_zip *zip = MLK_VFS_ZIP(self);
-
-	zip_close(zip->handle);
+	mlk_vfs_zip_finish(MLK_VFS_ZIP(self));
 }
 
 int
@@ -140,4 +108,59 @@
 	return 0;
 }
 
+struct mlk_vfs_file *
+mlk_vfs_zip_open(struct mlk_vfs_zip *zip, const char *entry, const char *mode)
+{
+	(void)mode;
+
+	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.finish = file_finish;
+
+	return &file->file;
+}
+
+void
+mlk_vfs_zip_finish(struct mlk_vfs_zip *zip)
+{
+	assert(zip);
+
+	zip_close(zip->handle);
+	zip->handle = NULL;
+}
+
+size_t
+mlk_vfs_zip_file_read(struct mlk_vfs_zip_file *file, void *buf, size_t bufsz)
+{
+	assert(file);
+	assert(buf);
+
+	zip_int64_t rv;
+
+	if ((rv = zip_fread(file->handle, buf, bufsz)) < 0) {
+		mlk_errf("%s", zip_file_strerror(file->handle));
+		return -1;
+	}
+
+	return rv;
+}
+
+void
+mlk_vfs_zip_file_finish(struct mlk_vfs_zip_file *file)
+{
+	assert(file);
+
+	zip_fclose(file->handle);
+	mlk_alloc_free(file);
+}
+
 #endif /* !MLK_WITH_ZIP */
--- a/libmlk-core/mlk/core/vfs-zip.h	Wed Sep 27 22:03:17 2023 +0200
+++ b/libmlk-core/mlk/core/vfs-zip.h	Sun Oct 01 09:18:01 2023 +0200
@@ -19,16 +19,46 @@
 #ifndef MLK_CORE_VFS_ZIP_H
 #define MLK_CORE_VFS_ZIP_H
 
+/**
+ * \file mlk/core/vfs-zip.h
+ * \brief VFS subsystem for zip archives.
+ *
+ * This module can be used to read file from a ZIP archives using the
+ * mlk/core/vfs.h abstract VFS module.
+ *
+ * It is implemented using the ::MLK_CONTAINER_OF macro which means you can
+ * use it and derive from it to add or modify its functions.
+ *
+ * \note It currently supports reading files but not writing.
+ *
+ * ## Members used
+ *
+ * The following VFS members are used:
+ *
+ * - ::mlk_vfs::finish
+ * - ::mlk_vfs::open
+ *
+ * The following VFS file member are used:
+ *
+ * - ::mlk_vfs_file::finish
+ * - ::mlk_vfs_file::read
+ */
+
 #include "sysconfig.h"
 #include "vfs.h"
 
 #if defined(MLK_WITH_ZIP)
 
-#if defined(__cplusplus)
-extern "C" {
-#endif
-
+/**
+ * \struct mlk_vfs_zip_file
+ * \brief VFS file implementation for ZIP files.
+ */
 struct mlk_vfs_zip_file {
+	/**
+	 * (read-write)
+	 *
+	 * Abstract VFS file to implement.
+	 */
 	struct mlk_vfs_file file;
 	
 	/** \cond MLK_PRIVATE_DECLS */
@@ -36,7 +66,16 @@
 	/** \endcond MLK_PRIVATE_DECLS */
 };
 
+/**
+ * \struct mlk_vfs_zip
+ * \brief VFS implementation for ZIP files.
+ */
 struct mlk_vfs_zip {
+	/**
+	 * (read-write)
+	 *
+	 * Abstract VFS to implement.
+	 */
 	struct mlk_vfs vfs;
 
 	/** \cond MLK_PRIVATE_DECLS */
@@ -44,8 +83,47 @@
 	/** \endcond MLK_PRIVATE_DECLS */
 };
 
+#if defined(__cplusplus)
+extern "C" {
+#endif
+
+/**
+ * Initialize the ZIP object and its underlying VFS module.
+ *
+ * \pre zip != NULL
+ * \pre file != NULL
+ * \pre mode != NULL
+ * \param zip the zip implementation to initialize
+ * \param path the path to the ZIP file
+ * \param mode the open mode
+ * \return 0 on success or -1 on error
+ */
 int
-mlk_vfs_zip_init(struct mlk_vfs_zip *zip, const char *file, const char *mode);
+mlk_vfs_zip_init(struct mlk_vfs_zip *zip, const char *path, const char *mode);
+
+/**
+ * Implements ::mlk_vfs::open virtual function.
+ */
+struct mlk_vfs_file *
+mlk_vfs_zip_open(struct mlk_vfs_zip *zip, const char *entry, const char *mode);
+
+/**
+ * Implements ::mlk_vfs::open virtual function.
+ */
+void
+mlk_vfs_zip_finish(struct mlk_vfs_zip *zip);
+
+/**
+ * Implements ::mlk_vfs_file::read virtual function.
+ */
+size_t
+mlk_vfs_zip_file_read(struct mlk_vfs_zip_file *file, void *buf, size_t bufsz);
+
+/**
+ * Implements ::mlk_vfs_file::finish virtual function.
+ */
+void
+mlk_vfs_zip_file_finish(struct mlk_vfs_zip_file *file);
 
 #if defined(__cplusplus)
 }
--- a/libmlk-core/mlk/core/vfs.c	Wed Sep 27 22:03:17 2023 +0200
+++ b/libmlk-core/mlk/core/vfs.c	Sun Oct 01 09:18:01 2023 +0200
@@ -28,9 +28,7 @@
 #include "vfs_p.h"
 
 struct mlk_vfs_file *
-mlk_vfs_open(struct mlk_vfs *vfs,
-             const char *entry,
-             const char *mode)
+mlk_vfs_open(struct mlk_vfs *vfs, const char *entry, const char *mode)
 {
 	assert(vfs);
 	assert(entry);
@@ -58,21 +56,23 @@
 }
 
 char *
-mlk_vfs_file_aread(struct mlk_vfs_file *file, size_t *outlen)
+mlk_vfs_file_read_all(struct mlk_vfs_file *file, size_t *outlen)
 {
 	char data[BUFSIZ], *str;
-	size_t nr, len = 0, cap = 128;
+	size_t nr, len = 0, cap = sizeof (data);
 
 	/* 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);
+			while (nr >= cap - len)
+				cap *= 2;
+
+			str = mlk_alloc_resize(str, cap);
 		}
 
-		sprintf(&str[len], "%.*s", (int)nr, data);
+		memcpy(&str[len], data, nr);
 		len += nr;
 	}
 
@@ -108,12 +108,12 @@
 }
 
 void
-mlk_vfs_file_free(struct mlk_vfs_file *file)
+mlk_vfs_file_finish(struct mlk_vfs_file *file)
 {
 	assert(file);
 
-	if (file->free)
-		file->free(file);
+	if (file->finish)
+		file->finish(file);
 }
 
 /* private */
@@ -134,7 +134,7 @@
 	char *data;
 	size_t datasz;
 
-	if (!(data = mlk_vfs_file_aread(file, &datasz)))
+	if (!(data = mlk_vfs_file_read_all(file, &datasz)))
 		return NULL;
 	if (!(ops = SDL_RWFromConstMem(data, datasz))) {
 		free(data);
--- a/libmlk-core/mlk/core/vfs.h	Wed Sep 27 22:03:17 2023 +0200
+++ b/libmlk-core/mlk/core/vfs.h	Sun Oct 01 09:18:01 2023 +0200
@@ -34,17 +34,33 @@
  * | mlk/core/vfs-dir.h | read, write | opens file relative to a directory |
  * | mlk/core/vfs-zip.h | read        | zip archive files extractor        |
  *
+ * ## Opening mode
+ *
+ * Both VFS interfaces and function can take a `const char *` mode argument to
+ * indicate user open mode. Each implementation can take specific interface
+ * options but the following are reserved and must be understood by each:
+ *
+ * - `e`: start writing at end
+ * - `r`: open for reading
+ * - `t`: truncate file (can't be used without `w`)
+ * - `w`: open for writing (create file but keep content if exists)
+ *
+ * Mode `e` is meaning less when combined with `t`.
+ *
+ * \note In contrast to C `fopen` function, modes can be mixed which means `r`,
+ *       `w` and `rw` means read-only, write-only and read-write respectively.
+ *
  * ## Initialize a VFS
  *
- * To use this module, you must first open a VFS interface. It is usually implemented
- * using the ::MLK_CONTAINER_OF macro.
+ * 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");
+ * mlk_vfs_dir_init(&dir, "/usr/share/mario");
  * ```
  *
  * The abstract interface will be located in the `vfs` field for each
@@ -60,16 +76,16 @@
  * char content[1024];
  * size_t len;
  *
- * file = mlk_vfs_open(vfs, "block.png");
+ * file = mlk_vfs_open(&dir.vfs, "block.png", "r");
  *
  * if (!file)
- *     handle_failure();
+ *     your_handle_failure_function();
  *
  * // 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();
+ *     your_handle_failure_function();
  *
  * // Use content freely...
  * ```
@@ -81,7 +97,7 @@
  * direct references to the VFS module.
  *
  * ```c
- * mlk_vfs_file_free(file);
+ * mlk_vfs_file_finish(file);
  * mlk_vfs_finish(&dir.vfs);
  * ```
  */
@@ -206,7 +222,7 @@
 	 * \pre self != NULL
 	 * \param self this VFS file
 	 */
-	void (*free)(struct mlk_vfs_file *self);
+	void (*finish)(struct mlk_vfs_file *self);
 };
 
 #if defined(__cplusplus)
@@ -214,21 +230,19 @@
 #endif
 
 /**
- * Wrapper of ::mlk_vfs::open if not NULL.
+ * Invoke ::mlk_vfs::open if not NULL.
  */
 struct mlk_vfs_file *
-mlk_vfs_open(struct mlk_vfs *vfs,
-             const char *entry,
-             const char *mode);
+mlk_vfs_open(struct mlk_vfs *vfs, const char *entry, const char *mode);
 
 /**
- * Wrapper of ::mlk_vfs::finish if not NULL.
+ * Invoke ::mlk_vfs::finish if not NULL.
  */
 void
 mlk_vfs_finish(struct mlk_vfs *vfs);
 
 /**
- * Wrapper of ::mlk_vfs_file::read if not NULL.
+ * Invoke ::mlk_vfs_file::read if not NULL.
  */
 size_t
 mlk_vfs_file_read(struct mlk_vfs_file *file, void *buf, size_t bufsz);
@@ -243,28 +257,28 @@
  * \pre file != NULL
  * \param file this VFS file
  * \param len pointer receiving the number of bytes read (can be NULL)
- * \return the string content
+ * \return the string content (user owned)
  */
 char *
-mlk_vfs_file_aread(struct mlk_vfs_file *file, size_t *len);
+mlk_vfs_file_read_all(struct mlk_vfs_file *file, size_t *len);
 
 /**
- * Wrapper of ::mlk_vfs::finish if not NULL.
+ * Invoke ::mlk_vfs_file::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.
+ * Invoke ::mlk_vfs_file::flush if not NULL.
  */
 int
 mlk_vfs_file_flush(struct mlk_vfs_file *file);
 
 /**
- * Wrapper of ::mlk_vfs::finish if not NULL.
+ * Invoke ::mlk_vfs_file::finish if not NULL.
  */
 void
-mlk_vfs_file_free(struct mlk_vfs_file *file);
+mlk_vfs_file_finish(struct mlk_vfs_file *file);
 
 #if defined(__cplusplus)
 }
--- a/tests/test-vfs-dir.c	Wed Sep 27 22:03:17 2023 +0200
+++ b/tests/test-vfs-dir.c	Sun Oct 01 09:18:01 2023 +0200
@@ -33,7 +33,7 @@
 	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_file_finish(file);
 	mlk_vfs_finish(&dir.vfs);
 }
 
--- a/tests/test-vfs-zip.c	Wed Sep 27 22:03:17 2023 +0200
+++ b/tests/test-vfs-zip.c	Sun Oct 01 09:18:01 2023 +0200
@@ -32,7 +32,7 @@
 	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_file_finish(file);
 	mlk_vfs_finish(&zip.vfs);
 }