You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
miniaudio/external/fs/fs.h

1190 lines
56 KiB
C

/*
File system library. Choice of public domain or MIT-0. See license statements at the end of this file.
fs - v1.0.0 - Release Date TBD
David Reid - mackron@gmail.com
GitHub: https://github.com/mackron/fs
*/
/*
1. Introduction
===============
This library is used to abstract access to the regular file system and archives such as ZIP files.
1.1. Basic Usage
----------------
The main object in the library is the `fs` object. Below is the most basic way to initialize a `fs`
object:
```c
fs_result result;
fs* pFS;
result = fs_init(NULL, &pFS);
if (result != FS_SUCCESS) {
// Failed to initialize.
}
```
The above code will initialize a `fs` object representing the system's regular file system. It uses
stdio under the hood. Once this is set up you can load files:
```c
fs_file* pFile;
result = fs_file_open(pFS, "file.txt", FS_READ, &pFile);
if (result != FS_SUCCESS) {
// Failed to open file.
}
```
Reading content from the file is very standard:
```c
size_t bytesRead;
result = fs_file_read(pFS, pBuffer, bytesToRead, &bytesRead);
if (result != FS_SUCCESS) {
// Failed to read file. You can use FS_AT_END to check if reading failed due to being at EOF.
}
```
In the code above, the number of bytes actually read is output to a variable. You can use this to
determine if you've reached the end of the file. You can also check if the result is FS_AT_END.
To do more advanced stuff, such as opening from archives, you'll need to configure the `fs` object
with a config, which you pass into `fs_init()`:
```c
#include "extras/backends/zip/fs_zip.h" // <-- This is where FS_ZIP is declared.
...
fs_archive_type pArchiveTypes[] =
{
{FS_ZIP, "zip"},
{FS_ZIP, "pac"}
};
fs_config fsConfig = fs_config_init(FS_STDIO, NULL, NULL);
fsConfig.pArchiveTypes = pArchiveTypes;
fsConfig.archiveTypeCount = sizeof(pArchiveTypes) / sizeof(pArchiveTypes[0]);
fs_init(&fsConfig, &pFS);
```
In the code above we are registering support for ZIP archives (`FS_ZIP`). Whenever a file with a
"zip" or "pac" extension is found, the library will be able to access the archive. The library will
determine whether or not a file is an archive based on it's extension. You can use whatever
extension you would like for a backend, and you can associated multiple extensions to the same
backend. You can also associated different backends to the same extension, in which case the
library will use the first one that works. If the extension of a file does not match with one of
the registered archive types it'll assume it's not an archive and will skip it. Below is an example
of one way you can read from an archive:
```c
result = fs_file_open(pFS, "archive.zip/file-inside-archive.txt", FS_READ, &pFile);
if (result != FS_SUCCESS) {
// Failed to open file.
}
```
In the example above, we've explicitly specified the name of the archive in the file path. The
library also supports the ability to handle archives transparently, meaning you don't need to
explicitly specify the archive. The code below will also work:
```c
fs_file_open(pFS, "file-inside-archive.txt", FS_READ, &pFile);
```
Transparently handling archives like this has overhead because the library needs to scan the file
system and check every archive it finds. To avoid this, you can explicitly disable this feature:
```c
fs_file_open(pFS, "archive.zip/file-inside-archive.txt", FS_READ | FS_VERBOSE, &pFile);
```
In the code above, the `FS_VERBOSE` flag will require you to pass in a verbose file path, meaning
you need to explicitly specify the archive in the path. You can take this one step further by
disabling access to archives in this manner altogether via `FS_OPAQUE`:
```c
result = fs_file_open(pFS, "archive.zip/file-inside-archive.txt", FS_READ | FS_OPAQUE, &pFile);
if (result != FS_SUCCESS) {
// This example will always fail.
}
```
In the example above, `FS_OPAQUE` is telling the library to treat archives as if they're totally
opaque and that the files within cannot be accessed.
Up to this point the handling of archives has been done automatically via `fs_file_open()`, however
the library allows you to manage archives manually. To do this you just initialize a `fs` object to
represent the archive:
```c
// Open the archive file itself first.
fs_file* pArchiveFile;
result = fs_file_open(pFS, "archive.zip", FS_READ, &pArchiveFile);
if (result != FS_SUCCESS) {
// Failed to open archive file.
}
// Once we have the archive file we can create the `fs` object representing the archive.
fs* pArchive;
fs_config archiveConfig;
archiveConfig = fs_config_init(FS_ZIP, NULL, fs_file_get_stream(pArchiveFile));
result = fs_init(&archiveConfig, &pArchive);
if (result != FS_SUCCESS) {
// Failed to initialize archive.
}
...
// During teardown, make sure the archive `fs` object is uninitialized before the stream.
fs_uninit(pArchive);
fs_file_close(pArchiveFile);
```
To initialize an `fs` object for an archive you need a stream to provide the raw archive data to
the backend. Conveniently, the `fs_file` object itself is a stream. In the example above we're just
opening a file from a different `fs` object (usually one representing the default file system) to
gain access to a stream. The stream does not need to be a `fs_file`. You can implement your own
`fs_stream` object, and a `fs_memory_stream` is included as stock with the library for when you
want to store the contents of an archive in-memory. Once you have the `fs` object for the archive
you can use it just like any other:
```c
result = fs_file_open(pArchive, "file-inside-archive.txt", FS_READ, &pFile);
if (result != FS_SUCCESS) {
// Failed to open file.
}
```
In addition to the above, you can use `fs_open_archive()` to open an archive from a file:
```c
fs* pArchive;
result = fs_open_archive(pFS, "archive.zip", FS_READ, &pArchive);
```
When opening an archive like this, it will inherit the archive types from the parent `fs` object
and will therefore support archives within archives. Use caution when doing this because if both
archives are compressed you will get a big performance hit. Only the inner-most archive should be
compressed.
1.2. Mounting
-------------
There is no notion of a "current directory" in this library. By default, relative paths will be
relative to whatever the backend deems appropriate. In practice, this means the "current" directory
for the default system backend, and the root directory for archives. There is still control over
how to load files from a relative path, however: mounting.
You can mount a physical directory to virtual path, similar in concept to Unix operating systems.
The difference, however, is that you can mount multiple directories to the same mount point in
which case a prioritization system will be used. There are separate mount points for reading and
writing. Below is an example of mounting for reading:
```c
fs_mount(pFS, "/some/actual/path", NULL, FS_MOUNT_PRIORITY_HIGHEST);
```
In the example above, `NULL` is equivalent to an empty path. If, for example, you have a file with
the path "/some/actual/path/file.txt", you can open it like the following:
```c
fs_file_open(pFS, "file.txt", FS_READ, &pFile);
```
You don't need to specify the "/some/actual/path" part because it's handled by the mount. If you
specify a virtual path, you can do something like the following:
```c
fs_mount(pFS, "/some/actual/path", "assets", FS_MOUNT_PRIORITY_HIGHEST);
```
In this case, loading files that are physically located in "/some/actual/path" would need to be
prexied with "assets":
```c
fs_file_open(pFS, "assets/file.txt", FS_READ, &pFile);
```
Archives can also be mounted:
```c
fs_mount(pFS, "/game/data/base/assets.zip", "assets", FS_MOUNT_PRIORITY_HIGHEST);
```
You can mount multiple paths to the same mount point:
```c
fs_mount(pFS, "/game/data/base.zip", "assets", FS_MOUNT_PRIORITY_HIGHEST);
fs_mount(pFS, "/game/data/mod1.zip", "assets", FS_MOUNT_PRIORITY_HIGHEST);
fs_mount(pFS, "/game/data/mod2.zip", "assets", FS_MOUNT_PRIORITY_HIGHEST);
```
In the example above, the "base.zip" archive is mounted first. Then "mod1.zip" is mounted, which
takes higher priority over "base.zip". Then "mod2.zip" is mounted which takes higher priority
again. With this set up, any file that is loaded from the "assets" mount point will first be loaded
from "mod2.zip", and if it doesn't exist there, "mod1.zip", and if not there, finally "base.zip".
You could use this set up to support simple modding prioritization in a game, for example.
If the file cannot be opened from any mounts it will attempt to open the file from the backend's
default search path. Mounts always take priority. When opening in transparent mode with
`FS_TRANSPARENT` (default), it will first try opening the file as if it were not in an archive. If
that fails, it will look inside archives.
You can also mount directories for writing:
```c
fs_mount_write(pFS, "/home/user/.config/mygame", "config", FS_MOUNT_PRIORITY_HIGHEST);
```
You can then open a file for writing like so:
```c
fs_file_open(pFS, "config/game.cfg", FS_WRITE, &pFile);
```
When opening a file in write mode, the prefix is what determines which write mount point to use.
You can therefore have multiple write mounts:
```c
fs_mount_write(pFS, "/home/user/.config/mygame", "config", FS_MOUNT_PRIORITY_HIGHEST);
fs_mount_write(pFS, "/home/user/.local/share/mygame/saves", "saves", FS_MOUNT_PRIORITY_HIGHEST);
```
Now you can write out different types of files, with the prefix being used to determine where it'll
be saved:
```c
fs_file_open(pFS, "config/game.cfg", FS_WRITE, &pFile); // Prefixed with "config", so will use the "config" mount point.
fs_file_open(pFS, "saves/save0.sav", FS_WRITE, &pFile); // Prefixed with "saves", so will use the "saves" mount point.
```
When opening a file for writing, if you pass in NULL for the `pFS` parameter it will open the file
like normal using the standard file system. That is it'll work exactly as if you were using stdio
`fopen()` and you will not be able use mount points. Keep in mind that there is no notion of a
"current directory" in this library so you'll be stuck with the initial working directory.
By default, you can move outside the mount point with ".." segments. If you want to disable this
functionality, you can use the `FS_NO_ABOVE_ROOT_NAVIGATION` flag:
```c
fs_file_open(pFS, "../file.txt", FS_READ | FS_NO_ABOVE_ROOT_NAVIGATION, &pFile);
```
In addition, any mount points that start with a "/" will be considered absolute and will not allow
any above-root navigation:
```c
fs_mount(pFS, "/game/data/base", "/gamedata", FS_MOUNT_PRIORITY_HIGHEST);
```
In the example above, the "/gamedata" mount point starts with a "/", so it will not allow any
above-root navigation which means you cannot navigate above "/game/data/base" when using this mount
point.
Note that writing directly into an archive is not supported by this API. To write into an archive,
the backend itself must support writing, and you will need to manually initialize a `fs` object for
the archive an write into it directly.
1.3. Enumeration
----------------
You can enumerate over the contents of a directory like the following:
```c
for (fs_iterator* pIterator = fs_first(pFS, "directory/to/enumerate", FS_NULL_TERMINATED, 0); pIterator != NULL; pIterator = fs_next(pIterator)) {
printf("Name: %s\n", pIterator->pName);
printf("Size: %llu\n", pIterator->info.size);
}
```
If you want to terminate iteration early, use `fs_free_iterator()` to free the iterator object.
`fs_next()` will free the iterator for you when it reaches the end.
Like when opening a file, you can specify `FS_OPAQUE`, `FS_VERBOSE` or `FS_TRANSPARENT` (default)
in `fs_first()` to control which files are enumerated. Enumerated files will be consistent with
what would be opened when using the same option with `fs_file_open()`.
Internally, `fs_first()` will gather all of the enumerated files. This means you should expect
`fs_first()` to be slow compared to `fs_next()`.
Enumerated entries will be sorted by name in terms of `strcmp()`.
Enumeration is not recursive. If you want to enumerate recursively you can inspect the `directory`
member of the `info` member in `fs_iterator`.
2. Thread Safety
================
The following points apply regarding thread safety.
- Opening files across multiple threads is safe. Backends are responsible for ensuring thread
safety when opening files.
- An individual `fs_file` object is not thread safe. If you want to use a specific `fs_file`
object across multiple threads, you will need to synchronize access to it yourself. Using
different `fs_file` objects across multiple threads is safe.
- Mounting and unmounting is not thread safe. You must use your own synchronization if you
want to do this across multiple threads.
- Opening a file on one thread while simultaneously mounting or unmounting on another thread is
not safe. Again, you must use your own synchronization if you need to do this. The recommended
usage is to set up your mount points once during initialization before opening any files.
3. Backends
===========
You can implement custom backends to support different file systems and archive formats. A stdio
backend is the default backend and is built into the library. A backend implements the functions
in the `fs_backend` structure.
A ZIP backend is included in the "extras" folder of this library's repository. Refer to this for
a complete example for how to implement a backend (not including write support, but I'm sure
you'll figure it out!).
The backend abstraction is designed to relieve backends from having to worry about the
implementation details of the main library. Backends should only concern themselves with their
own local content and not worry about things like mount points, archives, etc. Those details will
be handled at a higher level in the library.
Instances of a `fs` object can be configured with backend-specific configuration data. This is
passed to the backend as a void pointer to the necessary functions. This data will point to a
backend-defined structure that the backend will know how to use.
In order for the library to know how much memory to allocate for the `fs` object, the backend
needs to implement the `alloc_size` function. This function should return the total size of the
backend-specific data to associate with the `fs` object. Internally, this memory will be stored
at the end of the `fs` object. The backend can access this data via `fs_get_backend_data()`:
```c
typedef struct my_fs_data
{
int someData;
} my_fs_data;
...
my_fs_data* pBackendData = (my_fs_data*)fs_get_backend_data(pFS);
assert(pBackendData != NULL);
do_something(pBackendData->someData);
```
This pattern will be a central part of how backends are implemented. If you don't have any
backend-specific data, you can just return 0 from `alloc_size()` and simply not reference the
backend data pointer.
The main library will allocate the `fs` object, including any additional space specified by the
`alloc_size` function. Once this is done, it'll call the `init` function to initialize the backend.
This function will take a pointer to the `fs` object, the backend-specific configuration data, and
a stream object. The stream is used to provide the backend with the raw data of an archive, which
will be required for archive backends like ZIP. If your backend requires this, you should check
for if the stream is null, and if so, return an error. See section "4. Streams" for more details
on how to use streams. You need not take a copy of the stream pointer for use outside of `init()`.
Instead you can just use `fs_get_stream()` to get the stream object when you need it. You should
not ever close or otherwise take ownership of the stream - that will be handled at a higher level.
The `uninit` function is where you should do any cleanup. Do not close the stream here.
The `remove` function is used to remove a file. This is not recursive. If the path is a directory,
the backend should return an error if it is not empty. Backends do not need to implement this
function in which case they can leave the callback pointer as `NULL`, or have it return
`FS_NOT_IMPLEMENTED`.
The `rename` function is used to rename a file. This will act as a move if the source and
destination are in different directories. If the destination already exists, it should be
overwritten. This function is optional and can be left as `NULL` or return `FS_NOT_IMPLEMENTED`.
The `mkdir` function is used to create a directory. This is not recursive. If the directory already
exists, the backend should return `FS_SUCCESS`. This function is optional and can be left as `NULL`
or return `FS_NOT_IMPLEMENTED`.
The `info` function is used to get information about a file. If the backend does not have the
notion of the last modified or access time, it can set those values to 0. Set `directory` to 1 (or
FS_TRUE) if it's a directory. Likewise, set `symlink` to 1 if it's a symbolic link. It is important
that this function return the info of the exact file that would be opened with `file_open()`.
Like when initializing a `fs` object, the library needs to know how much backend-specific data to
allocate for the `fs_file` object. This is done with the `file_alloc_size` function. This function
is basically the same as `alloc_size` for the `fs` object, but for `fs_file`. If the backend does
not need any additional data, it can return 0. The backend can access this data via
`fs_file_get_backend_data()`.
The `file_open` function is where the backend should open the file. If the `fs` object that owns
the file was initialized with a stream, i.e. it's an archive, the stream will be non-null. You
should store this pointer for later use in `file_read`, etc. The `openMode` parameter will be a
combination of `FS_READ`, `FS_WRITE`, `FS_TRUNCATE`, `FS_APPEND` and `FS_OVERWRITE`. When opening
in write mode (`FS_WRITE`), it will default to truncate mode. You should ignore the `FS_OPAQUE`,
`FS_VERBOSE` and `FS_TRANSPARENT` flags. If the file does not exist, the backend should return
`FS_DOES_NOT_EXIST`. If the file is a directory, it should return `FS_IS_DIRECTORY`.
The file should be closed with `file_close`. This is where the backend should release any resources
associated with the file. The stream should not be closed here - it'll be cleaned up at a higher
level.
The `file_read` function is used to read data from the file. The backend should return `FS_AT_END`
when the end of the file is reached, but only if the number of bytes read is 0.
The `file_write` function is used to write data to the file. If the file is opened in append mode,
the backend should seek to the end of the file before writing. This is optional and need only be
specified if the backend supports writing.
The `file_seek` function is used to seek the cursor. The backend should return `FS_BAD_SEEK` if the
seek is out of bounds.
The `file_tell` function is used to get the cursor position.
The `file_flush` function is used to flush any buffered data to the file. This is optional and can
be left as `NULL` or return `FS_NOT_IMPLEMENTED`.
The `file_info` function is used to get information about an opened file. It returns the same
information as `info` but for an opened file.
The `file_duplicate` function is used to duplicate a file. The destination file will be a new file
and already allocated. The backend need only copy the necessary backend-specific data to the new
file.
The `first`, `next` and `free_iterator` functions are used to enumerate the contents of a directory.
If the directory is empty, or an error occurs, `fs_first` should return `NULL`. The `next` function
should return `NULL` when there are no more entries. When `next` returns `NULL`, the backend needs
to free the iterator object. The `free_iterator` function is used to free the iterator object
explicitly. The backend is responsible for any memory management of the name string. A typical way
to deal with this is to allocate the allocate additional space for the name immediately after the
`fs_iterator` allocation.
Backends are responsible for guaranteeing thread-safety of different files across different
threads. This should typically be quite easy since most system backends, such as stdio, are already
thread-safe, and archive backends are typically read-only which should make thread-safety trivial
on that front as well.
4. Streams
==========
Streams are the data delivery mechanism for archive backends. You can implement custom streams, but
this should be uncommon because `fs_file` itself is a stream, and a memory stream is included in
the library called `fs_memory_stream`. Between these two the majority of use cases should be
covered.
A stream is initialized using a specialized initialization function depending on the stream type.
For `fs_file`, simply opening the file is enough. For `fs_memory_stream`, you need to call
`fs_memory_stream_init_readonly()` for a standard read-only stream, or
`fs_memory_stream_init_write()` for a stream with write (and read) support. If you want to
implement your own stream type you would need to implement a similar initialization function.
Use `fs_stream_read()` and `fs_stream_write()` to read and write data from a stream. If the stream
does not support reading or writing, the respective function should return `FS_NOT_IMPLEMENTED`.
The cursor can be set and retrieved with `fs_stream_seek()` and `fs_stream_tell()`. There is only
a single cursor which is shared between reading and writing.
Streams can be duplicated. A duplicated stream is a fully independent stream. This functionality
is used heavily internally by the library so if you build a custom stream you should support it
if you can. Without duplication support, you will not be able to open files within archives. To
duplicate a stream, use `fs_stream_duplicate()`. To delete a duplicated stream, use
`fs_stream_delete_duplicate()`. Do not use implementation-specific uninitialization routines to
uninitialize a duplicated stream - `fs_stream_delete_duplicate()` will deal with that for you.
Streams are not thread safe. If you want to use a stream across multiple threads, you will need to
synchronize access to it yourself. Using different stream objects across multiple threads is safe.
A duplicated stream is entirely independent of the original stream and can be used across on a
different thread to the original stream.
The `fs_stream` object is a base class. If you want to implement your own stream, you should make
the first member of your stream object a `fs_stream` object. This will allow you to cast between
`fs_stream*` and your custom stream type.
See `fs_stream_vtable` for a list of functions that need to be implemented for a custom stream. If
the stream does not support writing, the `write` callback can be left as `NULL` or return
`FS_NOT_IMPLEMENTED`.
See `fs_memory_stream` for an example of how to implement a custom stream.
*/
/*
This library has been designed to be amalgamated into other libraries of mine. You will probably
see some random tags and stuff in this file. These are just used for doing a dumb amalgamation.
*/
#ifndef fs_h
#define fs_h
#include <stddef.h> /* For size_t. */
#include <stdarg.h> /* For va_list. */
#if defined(__cplusplus)
extern "C" {
#endif
/* BEG fs_compiler_compat.h */
#if defined(SIZE_MAX)
#define FS_SIZE_MAX SIZE_MAX
#else
#define FS_SIZE_MAX 0xFFFFFFFF /* When SIZE_MAX is not defined by the standard library just default to the maximum 32-bit unsigned integer. */
#endif
#if defined(__LP64__) || defined(_WIN64) || (defined(__x86_64__) && !defined(__ILP32__)) || defined(_M_X64) || defined(__ia64) || defined(_M_IA64) || defined(__aarch64__) || defined(_M_ARM64) || defined(__powerpc64__)
#define FS_SIZEOF_PTR 8
#else
#define FS_SIZEOF_PTR 4
#endif
#if FS_SIZEOF_PTR == 8
#define FS_64BIT
#else
#define FS_32BIT
#endif
#if defined(FS_USE_STDINT)
#include <stdint.h>
typedef int8_t fs_int8;
typedef uint8_t fs_uint8;
typedef int16_t fs_int16;
typedef uint16_t fs_uint16;
typedef int32_t fs_int32;
typedef uint32_t fs_uint32;
typedef int64_t fs_int64;
typedef uint64_t fs_uint64;
#else
typedef signed char fs_int8;
typedef unsigned char fs_uint8;
typedef signed short fs_int16;
typedef unsigned short fs_uint16;
typedef signed int fs_int32;
typedef unsigned int fs_uint32;
#if defined(_MSC_VER) && !defined(__clang__)
typedef signed __int64 fs_int64;
typedef unsigned __int64 fs_uint64;
#else
#if defined(__clang__) || (defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6)))
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wlong-long"
#if defined(__clang__)
#pragma GCC diagnostic ignored "-Wc++11-long-long"
#endif
#endif
typedef signed long long fs_int64;
typedef unsigned long long fs_uint64;
#if defined(__clang__) || (defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6)))
#pragma GCC diagnostic pop
#endif
#endif
#endif /* FS_USE_STDINT */
#if FS_SIZEOF_PTR == 8
typedef fs_uint64 fs_uintptr;
typedef fs_int64 fs_intptr;
#else
typedef fs_uint32 fs_uintptr;
typedef fs_int32 fs_intptr;
#endif
typedef unsigned char fs_bool8;
typedef unsigned int fs_bool32;
#define FS_TRUE 1
#define FS_FALSE 0
#define FS_INT64_MAX ((fs_int64)(((fs_uint64)0x7FFFFFFF << 32) | 0xFFFFFFFF))
#ifndef FS_API
#define FS_API
#endif
#ifdef _MSC_VER
#define FS_INLINE __forceinline
#elif defined(__GNUC__)
/*
I've had a bug report where GCC is emitting warnings about functions possibly not being inlineable. This warning happens when
the __attribute__((always_inline)) attribute is defined without an "inline" statement. I think therefore there must be some
case where "__inline__" is not always defined, thus the compiler emitting these warnings. When using -std=c89 or -ansi on the
command line, we cannot use the "inline" keyword and instead need to use "__inline__". In an attempt to work around this issue
I am using "__inline__" only when we're compiling in strict ANSI mode.
*/
#if defined(__STRICT_ANSI__)
#define FS_GNUC_INLINE_HINT __inline__
#else
#define FS_GNUC_INLINE_HINT inline
#endif
#if (__GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ >= 2)) || defined(__clang__)
#define FS_INLINE FS_GNUC_INLINE_HINT __attribute__((always_inline))
#else
#define FS_INLINE FS_GNUC_INLINE_HINT
#endif
#elif defined(__WATCOMC__)
#define FS_INLINE __inline
#else
#define FS_INLINE
#endif
#if defined(__has_attribute)
#if __has_attribute(format)
#define FS_ATTRIBUTE_FORMAT(fmt, va) __attribute__((format(printf, fmt, va)))
#endif
#endif
#ifndef FS_ATTRIBUTE_FORMAT
#define FS_ATTRIBUTE_FORMAT(fmt, va)
#endif
#define FS_NULL_TERMINATED ((size_t)-1)
/* END fs_compiler_compat.h */
/* BEG fs_result.h */
typedef enum fs_result
{
/* Compression Non-Error Result Codes. */
FS_HAS_MORE_OUTPUT = 102, /* Some stream has more output data to be read, but there's not enough room in the output buffer. */
FS_NEEDS_MORE_INPUT = 100, /* Some stream needs more input data before it can be processed. */
/* Main Result Codes. */
FS_SUCCESS = 0,
FS_ERROR = -1, /* Generic, unknown error. */
FS_INVALID_ARGS = -2,
FS_INVALID_OPERATION = -3,
FS_OUT_OF_MEMORY = -4,
FS_OUT_OF_RANGE = -5,
FS_ACCESS_DENIED = -6,
FS_DOES_NOT_EXIST = -7,
FS_ALREADY_EXISTS = -8,
FS_INVALID_FILE = -10,
FS_TOO_BIG = -11,
FS_NOT_DIRECTORY = -14,
FS_IS_DIRECTORY = -15,
FS_DIRECTORY_NOT_EMPTY = -16,
FS_AT_END = -17,
FS_BUSY = -19,
FS_BAD_SEEK = -25,
FS_NOT_IMPLEMENTED = -29,
FS_TIMEOUT = -34,
FS_CHECKSUM_MISMATCH = -100,
FS_NO_BACKEND = -101
} fs_result;
/* END fs_result.h */
/* BEG fs_allocation_callbacks.h */
typedef struct fs_allocation_callbacks
{
void* pUserData;
void* (* onMalloc )(size_t sz, void* pUserData);
void* (* onRealloc)(void* p, size_t sz, void* pUserData);
void (* onFree )(void* p, void* pUserData);
} fs_allocation_callbacks;
FS_API void* fs_malloc(size_t sz, const fs_allocation_callbacks* pAllocationCallbacks);
FS_API void* fs_calloc(size_t sz, const fs_allocation_callbacks* pAllocationCallbacks);
FS_API void* fs_realloc(void* p, size_t sz, const fs_allocation_callbacks* pAllocationCallbacks);
FS_API void fs_free(void* p, const fs_allocation_callbacks* pAllocationCallbacks);
/* END fs_allocation_callbacks.h */
/* BEG fs_stream.h */
/*
Streams.
The feeding of input and output data is done via a stream.
To implement a custom stream, such as a memory stream, or a file stream, you need to extend from
`fs_stream` and implement `fs_stream_vtable`. You can access your custom data by casting the
`fs_stream` to your custom type.
The stream vtable can support both reading and writing, but it doesn't need to support both at
the same time. If one is not supported, simply leave the relevant `read` or `write` callback as
`NULL`, or have them return FS_NOT_IMPLEMENTED.
*/
/* Seek Origins. */
typedef enum fs_seek_origin
{
FS_SEEK_SET = 0,
FS_SEEK_CUR = 1,
FS_SEEK_END = 2
} fs_seek_origin;
typedef struct fs_stream_vtable fs_stream_vtable;
typedef struct fs_stream fs_stream;
struct fs_stream_vtable
{
fs_result (* read )(fs_stream* pStream, void* pDst, size_t bytesToRead, size_t* pBytesRead);
fs_result (* write )(fs_stream* pStream, const void* pSrc, size_t bytesToWrite, size_t* pBytesWritten);
fs_result (* seek )(fs_stream* pStream, fs_int64 offset, fs_seek_origin origin);
fs_result (* tell )(fs_stream* pStream, fs_int64* pCursor);
size_t (* duplicate_alloc_size)(fs_stream* pStream); /* Optional. Returns the allocation size of the stream. When not defined, duplicating is disabled. */
fs_result (* duplicate )(fs_stream* pStream, fs_stream* pDuplicatedStream); /* Optional. Duplicate the stream. */
void (* uninit )(fs_stream* pStream); /* Optional. Uninitialize the stream. */
};
struct fs_stream
{
const fs_stream_vtable* pVTable;
};
FS_API fs_result fs_stream_init(const fs_stream_vtable* pVTable, fs_stream* pStream);
FS_API fs_result fs_stream_read(fs_stream* pStream, void* pDst, size_t bytesToRead, size_t* pBytesRead);
FS_API fs_result fs_stream_write(fs_stream* pStream, const void* pSrc, size_t bytesToWrite, size_t* pBytesWritten);
FS_API fs_result fs_stream_writef(fs_stream* pStream, const char* fmt, ...) FS_ATTRIBUTE_FORMAT(2, 3);
FS_API fs_result fs_stream_writef_ex(fs_stream* pStream, const fs_allocation_callbacks* pAllocationCallbacks, const char* fmt, ...) FS_ATTRIBUTE_FORMAT(3, 4);
FS_API fs_result fs_stream_writefv(fs_stream* pStream, const char* fmt, va_list args);
FS_API fs_result fs_stream_writefv_ex(fs_stream* pStream, const fs_allocation_callbacks* pAllocationCallbacks, const char* fmt, va_list args);
FS_API fs_result fs_stream_seek(fs_stream* pStream, fs_int64 offset, fs_seek_origin origin);
FS_API fs_result fs_stream_tell(fs_stream* pStream, fs_int64* pCursor);
/*
Duplicates a stream.
This will allocate the new stream on the heap. The caller is responsible for freeing the stream
with `fs_stream_delete_duplicate()` when it's no longer needed.
*/
FS_API fs_result fs_stream_duplicate(fs_stream* pStream, const fs_allocation_callbacks* pAllocationCallbacks, fs_stream** ppDuplicatedStream);
/*
Deletes a duplicated stream.
Do not use this for a stream that was not duplicated with `fs_stream_duplicate()`.
*/
FS_API void fs_stream_delete_duplicate(fs_stream* pDuplicatedStream, const fs_allocation_callbacks* pAllocationCallbacks);
/*
Helper functions for reading the entire contents of a stream, starting from the current cursor position. Free
the returned pointer with fs_free().
The format (FS_FORMAT_TEXT or FS_FORMAT_BINARY) is used to determine whether or not a null terminator should be
appended to the end of the data.
For flexiblity in case the backend does not support cursor retrieval or positioning, the data will be read
in fixed sized chunks.
*/
typedef enum fs_format
{
FS_FORMAT_TEXT,
FS_FORMAT_BINARY
} fs_format;
FS_API fs_result fs_stream_read_to_end(fs_stream* pStream, fs_format format, const fs_allocation_callbacks* pAllocationCallbacks, void** ppData, size_t* pDataSize);
/* END fs_stream.h */
/* BEG fs.h */
/* Open mode flags. */
#define FS_READ 0x0001
#define FS_WRITE 0x0002
#define FS_APPEND (FS_WRITE | 0x0004)
#define FS_OVERWRITE (FS_WRITE | 0x0008)
#define FS_TRUNCATE (FS_WRITE)
#define FS_TRANSPARENT 0x0000 /* Default. Opens a file such that archives of a known type are handled transparently. For example, "somefolder/archive.zip/file.txt" can be opened with "somefolder/file.txt" (the "archive.zip" part need not be specified). This assumes the `fs` object has been initialized with support for the relevant archive types. */
#define FS_OPAQUE 0x0010 /* When used, files inside archives cannot be opened automatically. For example, "somefolder/archive.zip/file.txt" will fail. Mounted archives work fine. */
#define FS_VERBOSE 0x0020 /* When used, files inside archives can be opened, but the name of the archive must be specified explicitly in the path, such as "somefolder/archive.zip/file.txt" */
#define FS_NO_CREATE_DIRS 0x0040 /* When used, directories will not be created automatically when opening files for writing. */
#define FS_IGNORE_MOUNTS 0x0080 /* When used, mounted directories and archives will be ignored when opening and iterating files. */
#define FS_ONLY_MOUNTS 0x0100 /* When used, only mounted directories and archives will be considered when opening and iterating files. */
#define FS_NO_SPECIAL_DIRS 0x0200 /* When used, the presence of special directories like "." and ".." will be result in an error when opening files. */
#define FS_NO_ABOVE_ROOT_NAVIGATION 0x0400 /* When used, navigating above the mount point with leading ".." segments will result in an error. Can be also be used with fs_path_normalize(). */
/* Garbage collection policies.*/
#define FS_GC_POLICY_THRESHOLD 0x0001 /* Only garbage collect unreferenced opened archives until the count is below the configured threshold. */
#define FS_GC_POLICY_FULL 0x0002 /* Garbage collect every unreferenced opened archive, regardless of how many are open.*/
typedef struct fs_config fs_config;
typedef struct fs fs;
typedef struct fs_file fs_file;
typedef struct fs_file_info fs_file_info;
typedef struct fs_iterator fs_iterator;
typedef struct fs_backend fs_backend;
typedef enum fs_mount_priority
{
FS_MOUNT_PRIORITY_HIGHEST = 0,
FS_MOUNT_PRIORITY_LOWEST = 1
} fs_mount_priority;
typedef struct fs_archive_type
{
const fs_backend* pBackend;
const char* pExtension;
} fs_archive_type;
struct fs_file_info
{
fs_uint64 size;
fs_uint64 lastModifiedTime;
fs_uint64 lastAccessTime;
int directory;
int symlink;
};
struct fs_iterator
{
fs* pFS;
const char* pName; /* Must be null terminated. The FS implementation is responsible for manageing the memory allocation. */
size_t nameLen;
fs_file_info info;
};
struct fs_config
{
const fs_backend* pBackend;
const void* pBackendConfig;
fs_stream* pStream;
const fs_archive_type* pArchiveTypes;
size_t archiveTypeCount;
const fs_allocation_callbacks* pAllocationCallbacks;
};
FS_API fs_config fs_config_init_default(void);
FS_API fs_config fs_config_init(const fs_backend* pBackend, void* pBackendConfig, fs_stream* pStream);
typedef struct fs_backend
{
size_t (* alloc_size )(const void* pBackendConfig);
fs_result (* init )(fs* pFS, const void* pBackendConfig, fs_stream* pStream); /* Return 0 on success or an errno result code on error. pBackendConfig is a pointer to a backend-specific struct. The documentation for your backend will tell you how to use this. You can usually pass in NULL for this. */
void (* uninit )(fs* pFS);
fs_result (* ioctl )(fs* pFS, int op, void* pArg); /* Optional. */
fs_result (* remove )(fs* pFS, const char* pFilePath);
fs_result (* rename )(fs* pFS, const char* pOldName, const char* pNewName);
fs_result (* mkdir )(fs* pFS, const char* pPath); /* This is not recursive. Return FS_SUCCESS if directory already exists. */
fs_result (* info )(fs* pFS, const char* pPath, int openMode, fs_file_info* pInfo); /* openMode flags can be ignored by most backends. It's primarily used by proxy of passthrough style backends. */
size_t (* file_alloc_size )(fs* pFS);
fs_result (* file_open )(fs* pFS, fs_stream* pStream, const char* pFilePath, int openMode, fs_file* pFile); /* Return 0 on success or an errno result code on error. Return ENOENT if the file does not exist. pStream will be null if the backend does not need a stream (the `pFS` object was not initialized with one). */
fs_result (* file_open_handle)(fs* pFS, void* hBackendFile, fs_file* pFile); /* Optional. Open a file from a file handle. Backend-specific. The format of hBackendFile will be specified by the backend. */
void (* file_close )(fs_file* pFile);
fs_result (* file_read )(fs_file* pFile, void* pDst, size_t bytesToRead, size_t* pBytesRead); /* Return 0 on success, or FS_AT_END on end of file. Only return FS_AT_END if *pBytesRead is 0. Return an errno code on error. Implementations must support reading when already at EOF, in which case FS_AT_END should be returned and *pBytesRead should be 0. */
fs_result (* file_write )(fs_file* pFile, const void* pSrc, size_t bytesToWrite, size_t* pBytesWritten);
fs_result (* file_seek )(fs_file* pFile, fs_int64 offset, fs_seek_origin origin);
fs_result (* file_tell )(fs_file* pFile, fs_int64* pCursor);
fs_result (* file_flush )(fs_file* pFile);
fs_result (* file_info )(fs_file* pFile, fs_file_info* pInfo);
fs_result (* file_duplicate )(fs_file* pFile, fs_file* pDuplicate); /* Duplicate the file handle. */
fs_iterator* (* first )(fs* pFS, const char* pDirectoryPath, size_t directoryPathLen);
fs_iterator* (* next )(fs_iterator* pIterator); /* <-- Must return null when there are no more files. In this case, free_iterator must be called internally. */
void (* free_iterator )(fs_iterator* pIterator); /* <-- Free the `fs_iterator` object here since `first` and `next` were the ones who allocated it. Also do any uninitialization routines. */
} fs_backend;
FS_API fs_result fs_init(const fs_config* pConfig, fs** ppFS);
FS_API void fs_uninit(fs* pFS);
FS_API fs_result fs_ioctl(fs* pFS, int op, void* pArg);
FS_API fs_result fs_remove(fs* pFS, const char* pFilePath);
FS_API fs_result fs_rename(fs* pFS, const char* pOldName, const char* pNewName);
FS_API fs_result fs_mkdir(fs* pFS, const char* pPath); /* Does not consider mounts. Returns FS_SUCCESS if directory already exists. */
FS_API fs_result fs_info(fs* pFS, const char* pPath, int openMode, fs_file_info* pInfo); /* openMode flags specify same options as openMode in file_open(), but FS_READ, FS_WRITE, FS_TRUNCATE, FS_APPEND, and FS_OVERWRITE are ignored. */
FS_API fs_stream* fs_get_stream(fs* pFS);
FS_API const fs_allocation_callbacks* fs_get_allocation_callbacks(fs* pFS);
FS_API void* fs_get_backend_data(fs* pFS); /* For use by the backend. Will be the size returned by the alloc_size() function in the vtable. */
FS_API size_t fs_get_backend_data_size(fs* pFS);
FS_API fs_result fs_open_archive_ex(fs* pFS, const fs_backend* pBackend, void* pBackendConfig, const char* pArchivePath, size_t archivePathLen, int openMode, fs** ppArchive);
FS_API fs_result fs_open_archive(fs* pFS, const char* pArchivePath, int openMode, fs** ppArchive);
FS_API void fs_close_archive(fs* pArchive);
FS_API void fs_gc_archives(fs* pFS, int policy);
FS_API void fs_set_archive_gc_threshold(fs* pFS, size_t threshold);
FS_API size_t fs_get_archive_gc_threshold(fs* pFS);
FS_API fs_result fs_file_open(fs* pFS, const char* pFilePath, int openMode, fs_file** ppFile);
FS_API fs_result fs_file_open_from_handle(fs* pFS, void* hBackendFile, fs_file** ppFile);
FS_API void fs_file_close(fs_file* pFile);
FS_API fs_result fs_file_read(fs_file* pFile, void* pDst, size_t bytesToRead, size_t* pBytesRead); /* Returns 0 on success, FS_AT_END on end of file, or an errno result code on error. Will only return FS_AT_END if *pBytesRead is 0. */
FS_API fs_result fs_file_write(fs_file* pFile, const void* pSrc, size_t bytesToWrite, size_t* pBytesWritten);
FS_API fs_result fs_file_writef(fs_file* pFile, const char* fmt, ...) FS_ATTRIBUTE_FORMAT(2, 3);
FS_API fs_result fs_file_writefv(fs_file* pFile, const char* fmt, va_list args);
FS_API fs_result fs_file_seek(fs_file* pFile, fs_int64 offset, fs_seek_origin origin);
FS_API fs_result fs_file_tell(fs_file* pFile, fs_int64* pCursor);
FS_API fs_result fs_file_flush(fs_file* pFile);
FS_API fs_result fs_file_get_info(fs_file* pFile, fs_file_info* pInfo);
FS_API fs_result fs_file_duplicate(fs_file* pFile, fs_file** ppDuplicate); /* Duplicate the file handle. */
FS_API void* fs_file_get_backend_data(fs_file* pFile);
FS_API size_t fs_file_get_backend_data_size(fs_file* pFile);
FS_API fs_stream* fs_file_get_stream(fs_file* pFile); /* Files are streams. They can be cast directly to fs_stream*, but this function is here for people who prefer function style getters. */
FS_API fs* fs_file_get_fs(fs_file* pFile);
FS_API fs_iterator* fs_first_ex(fs* pFS, const char* pDirectoryPath, size_t directoryPathLen, int mode);
FS_API fs_iterator* fs_first(fs* pFS, const char* pDirectoryPath, int mode);
FS_API fs_iterator* fs_next(fs_iterator* pIterator);
FS_API void fs_free_iterator(fs_iterator* pIterator);
FS_API fs_result fs_mount(fs* pFS, const char* pPathToMount, const char* pMountPoint, fs_mount_priority priority);
FS_API fs_result fs_unmount(fs* pFS, const char* pPathToMount_NotMountPoint);
FS_API fs_result fs_mount_fs(fs* pFS, fs* pOtherFS, const char* pMountPoint, fs_mount_priority priority);
FS_API fs_result fs_unmount_fs(fs* pFS, fs* pOtherFS); /* Must be matched up with fs_mount_fs(). */
FS_API fs_result fs_mount_write(fs* pFS, const char* pPathToMount, const char* pMountPoint, fs_mount_priority priority);
FS_API fs_result fs_unmount_write(fs* pFS, const char* pPathToMount_NotMountPoint);
/*
Helper functions for reading the entire contents of a file, starting from the current cursor position. Free
the returned pointer with fs_free(), using the same allocation callbacks as the fs object. You can use
fs_get_allocation_callbacks() if necessary, like so:
fs_free(pFileData, fs_get_allocation_callbacks(pFS));
The format (FS_FORMAT_TEXT or FS_FORMAT_BINARY) is used to determine whether or not a null terminator should be
appended to the end of the data.
For flexiblity in case the backend does not support cursor retrieval or positioning, the data will be read
in fixed sized chunks.
*/
FS_API fs_result fs_file_read_to_end(fs_file* pFile, fs_format format, void** ppData, size_t* pDataSize);
FS_API fs_result fs_file_open_and_read(fs* pFS, const char* pFilePath, fs_format format, void** ppData, size_t* pDataSize);
FS_API fs_result fs_file_open_and_write(fs* pFS, const char* pFilePath, void* pData, size_t dataSize);
/* Default Backend. */
extern const fs_backend* FS_STDIO; /* The default stdio backend. The handle for fs_file_open_from_handle() is a FILE*. */
/* END fs.h */
/* BEG fs_helpers.h */
/*
This section just contains various helper functions, mainly for custom backends.
*/
/* Converts an errno code to our own error code. */
FS_API fs_result fs_result_from_errno(int error);
/* END fs_helpers.h */
/* BEG fs_path.h */
/*
These functions are low-level functions for working with paths. The most important part of this API
is probably the iteration functions. These functions are used for iterating over each of the
segments of a path. This library will recognize both '\' and '/'. If you want to use just one or
the other, or a different separator, you'll need to use a different library. Likewise, this library
will treat paths as case sensitive. Again, you'll need to use a different library if this is not
suitable for you.
Iteration will always return both sides of a separator. For example, if you iterate "abc/def",
you will get two items: "abc" and "def". Where this is of particular importance and where you must
be careful, is the handling of the root directory. If you iterate "/", it will also return two
items. The first will be length 0 with an offset of zero which represents the left side of the "/"
and the second will be length 0 with an offset of 1 which represents the right side. The reason for
this design is that it makes iteration unambiguous and makes it easier to reconstruct a path.
The path API does not do any kind of validation to check if it represents an actual path on the
file system. Likewise, it does not do any validation to check if the path contains invalid
characters. All it cares about is "/" and "\".
*/
typedef struct
{
const char* pFullPath;
size_t fullPathLength;
size_t segmentOffset;
size_t segmentLength;
} fs_path_iterator;
FS_API fs_result fs_path_first(const char* pPath, size_t pathLen, fs_path_iterator* pIterator);
FS_API fs_result fs_path_last(const char* pPath, size_t pathLen, fs_path_iterator* pIterator);
FS_API fs_result fs_path_next(fs_path_iterator* pIterator);
FS_API fs_result fs_path_prev(fs_path_iterator* pIterator);
FS_API fs_bool32 fs_path_is_first(const fs_path_iterator* pIterator);
FS_API fs_bool32 fs_path_is_last(const fs_path_iterator* pIterator);
FS_API int fs_path_iterators_compare(const fs_path_iterator* pIteratorA, const fs_path_iterator* pIteratorB);
FS_API const char* fs_path_file_name(const char* pPath, size_t pathLen); /* Does *not* include the null terminator. Returns an offset of pPath. Will only be null terminated if pPath is. Returns null if the path ends with a slash. */
FS_API int fs_path_directory(char* pDst, size_t dstCap, const char* pPath, size_t pathLen); /* Returns the length, or < 0 on error. pDst can be null in which case the required length will be returned. Will not include a trailing slash. */
FS_API const char* fs_path_extension(const char* pPath, size_t pathLen); /* Does *not* include the null terminator. Returns an offset of pPath. Will only be null terminated if pPath is. Returns null if the extension cannot be found. */
FS_API fs_bool32 fs_path_extension_equal(const char* pPath, size_t pathLen, const char* pExtension, size_t extensionLen); /* Returns true if the extension is equal to the given extension. Case insensitive. */
FS_API const char* fs_path_trim_base(const char* pPath, size_t pathLen, const char* pBasePath, size_t basePathLen);
FS_API int fs_path_append(char* pDst, size_t dstCap, const char* pBasePath, size_t basePathLen, const char* pPathToAppend, size_t pathToAppendLen); /* pDst can be equal to pBasePath in which case it will be appended in-place. pDst can be null in which case the function will return the required length. */
FS_API int fs_path_normalize(char* pDst, size_t dstCap, const char* pPath, size_t pathLen, unsigned int options); /* The only root component recognized is "/". The path cannot start with "C:", "//<address>", etc. This is not intended to be a general cross-platform path normalization routine. If the path starts with "/", this will fail with a negative result code if normalization would result in the path going above the root directory. Will convert all separators to "/". Will remove trailing slash. pDst can be null in which case the required length will be returned. */
/* END fs_path.h */
/* BEG fs_memory_stream.h */
/*
Memory streams support both reading and writing within the same stream. To only support read-only
mode, use fs_memory_stream_init_readonly(). With this you can pass in a standard data/size pair.
If you need writing support, use fs_memory_stream_init_write(). When writing data, the stream will
output to a buffer that is owned by the stream. When you need to access the data, do so by
inspecting the pointer directly with `stream.write.pData` and `stream.write.dataSize`. This mode
also supports reading.
You can overwrite data by seeking to the required location and then just writing like normal. To
append data, just seek to the end:
fs_memory_stream_seek(pStream, 0, fs_SEEK_ORIGIN_END);
The memory stream need not be uninitialized in read-only mode. In write mode you can use
`fs_memory_stream_uninit()` to free the data. Alternatively you can just take ownership of the
buffer and free it yourself with `fs_free()`.
Below is an example for write mode.
```c
fs_memory_stream stream;
fs_memory_stream_init_write(NULL, &stream);
// Write some data to the stream...
fs_memory_stream_write(&stream, pSomeData, someDataSize, NULL);
// Do something with the data.
do_something_with_my_data(stream.write.pData, stream.write.dataSize);
```
To free the data, you can use `fs_memory_stream_uninit()`, or you can take ownership of the data
and free it yourself with `fs_free()`:
```c
fs_memory_stream_uninit(&stream);
```
Or to take ownership:
```c
size_t dataSize;
void* pData = fs_memory_stream_take_ownership(&stream, &dataSize);
```
With the above, `pData` will be the pointer to the data and `dataSize` will be the size of the data
and you will be responsible for deleting the buffer with `fs_free()`.
Read mode is simpler:
```c
fs_memory_stream stream;
fs_memory_stream_init_readonly(pData, dataSize, &stream);
// Read some data.
fs_memory_stream_read(&stream, &myBuffer, bytesToRead, NULL);
```
There is only one cursor. As you read and write the cursor will move forward. If you need to
read and write from different locations from the same fs_memory_stream object, you need to
seek before doing your read or write. You cannot read and write at the same time across
multiple threads for the same fs_memory_stream object.
*/
typedef struct fs_memory_stream fs_memory_stream;
struct fs_memory_stream
{
fs_stream base;
void** ppData; /* Will be set to &readonly.pData in readonly mode. */
size_t* pDataSize; /* Will be set to &readonly.dataSize in readonly mode. */
struct
{
const void* pData;
size_t dataSize;
} readonly;
struct
{
void* pData; /* Will only be set in write mode. */
size_t dataSize;
size_t dataCap;
} write;
size_t cursor;
fs_allocation_callbacks allocationCallbacks; /* This is copied from the allocation callbacks passed in from e_memory_stream_init(). Only used in write mode. */
};
FS_API fs_result fs_memory_stream_init_write(const fs_allocation_callbacks* pAllocationCallbacks, fs_memory_stream* pStream);
FS_API fs_result fs_memory_stream_init_readonly(const void* pData, size_t dataSize, fs_memory_stream* pStream);
FS_API void fs_memory_stream_uninit(fs_memory_stream* pStream); /* Only needed for write mode. This will free the internal pointer so make sure you've done what you need to do with it. */
FS_API fs_result fs_memory_stream_read(fs_memory_stream* pStream, void* pDst, size_t bytesToRead, size_t* pBytesRead);
FS_API fs_result fs_memory_stream_write(fs_memory_stream* pStream, const void* pSrc, size_t bytesToWrite, size_t* pBytesWritten);
FS_API fs_result fs_memory_stream_seek(fs_memory_stream* pStream, fs_int64 offset, int origin);
FS_API fs_result fs_memory_stream_tell(fs_memory_stream* pStream, size_t* pCursor);
FS_API fs_result fs_memory_stream_remove(fs_memory_stream* pStream, size_t offset, size_t size);
FS_API fs_result fs_memory_stream_truncate(fs_memory_stream* pStream);
FS_API void* fs_memory_stream_take_ownership(fs_memory_stream* pStream, size_t* pSize); /* Takes ownership of the buffer. The caller is responsible for freeing the buffer with fs_free(). Only valid in write mode. */
/* END fs_memory_stream.h */
/* BEG fs_utils.h */
FS_API void fs_sort(void* pBase, size_t count, size_t stride, int (*compareProc)(void*, const void*, const void*), void* pUserData);
FS_API void* fs_binary_search(const void* pKey, const void* pList, size_t count, size_t stride, int (*compareProc)(void*, const void*, const void*), void* pUserData);
FS_API void* fs_linear_search(const void* pKey, const void* pList, size_t count, size_t stride, int (*compareProc)(void*, const void*, const void*), void* pUserData);
FS_API void* fs_sorted_search(const void* pKey, const void* pList, size_t count, size_t stride, int (*compareProc)(void*, const void*, const void*), void* pUserData);
FS_API int fs_strnicmp(const char* str1, const char* str2, size_t count);
/* END fs_utils.h */
/* BEG fs_snprintf.h */
FS_API int fs_vsprintf(char* buf, char const* fmt, va_list va);
FS_API int fs_vsnprintf(char* buf, size_t count, char const* fmt, va_list va);
FS_API int fs_sprintf(char* buf, char const* fmt, ...) FS_ATTRIBUTE_FORMAT(2, 3);
FS_API int fs_snprintf(char* buf, size_t count, char const* fmt, ...) FS_ATTRIBUTE_FORMAT(3, 4);
/* END fs_snprintf.h */
#if defined(__cplusplus)
}
#endif
#endif /* fs_h */
/*
This software is available as a choice of the following licenses. Choose
whichever you prefer.
===============================================================================
ALTERNATIVE 1 - Public Domain (www.unlicense.org)
===============================================================================
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
software, either in source code form or as a compiled binary, for any purpose,
commercial or non-commercial, and by any means.
In jurisdictions that recognize copyright laws, the author or authors of this
software dedicate any and all copyright interest in the software to the public
domain. We make this dedication for the benefit of the public at large and to
the detriment of our heirs and successors. We intend this dedication to be an
overt act of relinquishment in perpetuity of all present and future rights to
this software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>
===============================================================================
ALTERNATIVE 2 - MIT No Attribution
===============================================================================
Copyright 2025 David Reid
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/