mirror of
https://github.com/mackron/miniaudio.git
synced 2026-04-22 00:06:59 +02:00
1499 lines
67 KiB
C
1499 lines
67 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.
|
|
}
|
|
```
|
|
|
|
If you don't need any of the advanced features of the library, you can just pass in NULL for the
|
|
`fs` object which will just use the native file system like normal:
|
|
|
|
```c
|
|
fs_file_open(NULL, "file.txt", FS_READ, &pFile);
|
|
```
|
|
|
|
From here on out, examples will use an `fs` object for the sake of consistency, but all basic IO
|
|
APIs that do not use things like mounting and archive registration will work with NULL.
|
|
|
|
To close a file, use `fs_file_close()`:
|
|
|
|
```c
|
|
fs_file_close(pFile);
|
|
```
|
|
|
|
Reading content from the file is very standard:
|
|
|
|
```c
|
|
size_t bytesRead;
|
|
|
|
result = fs_file_read(pFile, 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. You
|
|
can pass in null for the last parameter of `fs_file_read()` in which an error will be returned if
|
|
the exact number of bytes requested could not be read.
|
|
|
|
Writing works the same way as reading:
|
|
|
|
```c
|
|
fs_file* pFile;
|
|
|
|
result = fs_file_open(pFS, "file.txt", FS_WRITE, &pFile);
|
|
if (result != FS_SUCCESS) {
|
|
// Failed to open file.
|
|
}
|
|
|
|
result = fs_file_write(pFile, pBuffer, bytesToWrite, &bytesWritten);
|
|
```
|
|
|
|
The `FS_WRITE` option will default to `FS_TRUNCATE`. You can also use `FS_APPEND` and
|
|
`FS_OVERWRITE`:
|
|
|
|
```c
|
|
fs_file_open(pFS, "file.txt", FS_APPEND, &pFile); // You need not specify FS_WRITE when using FS_TRUNCATE, FS_APPEND or FS_OVERWRITE as it is implied.
|
|
```
|
|
|
|
Files can be opened for both reading and writing by simply combining the two:
|
|
|
|
```c
|
|
fs_file_open(pFS, "file.txt", FS_READ | FS_WRITE, &pFile);
|
|
```
|
|
|
|
Seeking and telling is very standard as well:
|
|
|
|
```c
|
|
fs_file_seek(pFile, 0, FS_SEEK_END);
|
|
|
|
fs_int64 cursorPos;
|
|
fs_file_tell(pFile, &cursorPos);
|
|
```
|
|
|
|
Retrieving information about a file is done with `fs_file_info()`:
|
|
|
|
```c
|
|
fs_file_info info;
|
|
fs_file_info(pFile, &info);
|
|
```
|
|
|
|
If you want to get information about a file without opening it, you can use `fs_info()`:
|
|
|
|
```c
|
|
fs_file_info info;
|
|
fs_info(pFS, "file.txt", &info);
|
|
```
|
|
|
|
A file handle can be duplicated with `fs_file_duplicate()`:
|
|
|
|
```c
|
|
fs_file* pFileDup;
|
|
fs_file_duplicate(pFile, &pFileDup);
|
|
```
|
|
|
|
Note that this will only duplicate the file handle. It does not make a copy of the file on the file
|
|
system itself. The duplicated file handle will be entirely independent of the original handle.
|
|
|
|
To delete a file, use `fs_remove()`:
|
|
|
|
```c
|
|
fs_remove(pFS, "file.txt");
|
|
```
|
|
|
|
Files can be renamed and moved with `fs_rename()`:
|
|
|
|
```c
|
|
fs_rename(pFS, "file.txt", "new-file.txt");
|
|
```
|
|
|
|
To create a directory, use `fs_mkdir()`:
|
|
|
|
```c
|
|
fs_mkdir(pFS, "new-directory", 0);
|
|
```
|
|
|
|
By default, `fs_mkdir()` will create the directory hierarchy for you. If you want to disable this
|
|
so it fails if the directory hierarchy doesn't exist, you can use `FS_NO_CREATE_DIRS`:
|
|
|
|
```c
|
|
fs_mkdir(pFS, "new-directory", FS_NO_CREATE_DIRS);
|
|
```
|
|
|
|
1.2. Archives
|
|
-------------
|
|
To enable support for archives, you need an `fs` object, and it must be initialized with a config:
|
|
|
|
```c
|
|
#include "extras/backends/zip/fs_zip.h" // <-- This is where FS_ZIP is declared.
|
|
#include "extras/backends/pak/fs_pak.h" // <-- This is where FS_PAK is declared.
|
|
|
|
...
|
|
|
|
fs_archive_type pArchiveTypes[] =
|
|
{
|
|
{FS_ZIP, "zip"},
|
|
{FS_PAK, "pak"}
|
|
};
|
|
|
|
fs_config fsConfig = fs_config_init(FS_STDIO, NULL, NULL);
|
|
fsConfig.pArchiveTypes = pArchiveTypes;
|
|
fsConfig.archiveTypeCount = sizeof(pArchiveTypes) / sizeof(pArchiveTypes[0]);
|
|
|
|
fs* pFS;
|
|
fs_init(&fsConfig, &pFS);
|
|
```
|
|
|
|
In the code above we are registering support for ZIP archives (`FS_ZIP`) and Quake PAK archives
|
|
(`FS_PAK`). Whenever a file with a "zip" or "pak" 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 associate 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, opening the file will fail because `FS_OPAQUE` is telling the library to
|
|
treat archives as if they're totally opaque which means 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;
|
|
fs_open_archive(pFS, "archive.zip", FS_READ, &pArchive);
|
|
|
|
...
|
|
|
|
// When tearing down, do *not* use `fs_uninit()`. Use `fs_close_archive()` instead.
|
|
fs_close_archive(pArchive);
|
|
```
|
|
|
|
Note that you need to use `fs_close_archive()` when opening an archive like this. The reason for
|
|
this is that there's some internal reference counting and memory management happening under the
|
|
hood. You should only call `fs_close_archive()` if `fs_open_archive()` succeeds.
|
|
|
|
When opening an archive with `fs_open_archive()`, 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.3. Mounting
|
|
-------------
|
|
There is no ability to change the working directory in this library. Instead you can mount a
|
|
physical directory to a 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_READ);
|
|
```
|
|
|
|
To unmount, you need to specify the actual path, not the virtual path:
|
|
|
|
```c
|
|
fs_unmount(pFS, "/some/actual/path", FS_READ);
|
|
```
|
|
|
|
In the example above, using `NULL` for the virtual path 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_READ);
|
|
```
|
|
|
|
In this case, loading files that are physically located in "/some/actual/path" would need to be
|
|
prefixed with "assets":
|
|
|
|
```c
|
|
fs_file_open(pFS, "assets/file.txt", FS_READ, &pFile);
|
|
```
|
|
|
|
You can mount multiple paths to the same virtual path in which case a prioritization system will be
|
|
used:
|
|
|
|
```c
|
|
fs_mount(pFS, "/usr/share/mygame/gamedata/base", "gamedata", FS_READ); // Base game. Lowest priority.
|
|
fs_mount(pFS, "/home/user/.local/share/mygame/gamedata/mod1", "gamedata", FS_READ); // Mod #1. Middle priority.
|
|
fs_mount(pFS, "/home/user/.local/share/mygame/gamedata/mod2", "gamedata", FS_READ); // Mod #2. Highest priority.
|
|
```
|
|
|
|
The example above shows a basic system for setting up some kind of modding support in a game. In
|
|
this case, attempting to load a file from the "gamedata" mount point will first check the "mod2"
|
|
directory, and if it cannot be opened from there, it will check "mod1", and finally it'll fall back
|
|
to the base game data.
|
|
|
|
Internally there are a separate set of mounts for reading and writing. To set up a mount point for
|
|
opening files in write mode, you need to specify the `FS_WRITE` option:
|
|
|
|
```c
|
|
fs_mount(pFS, "/home/user/.config/mygame", "config", FS_WRITE);
|
|
fs_mount(pFS, "/home/user/.local/share/mygame/saves", "saves", FS_WRITE);
|
|
```
|
|
|
|
To open a file for writing, you need only prefix the path with the mount's virtual path, exactly
|
|
like read mode:
|
|
|
|
```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.
|
|
```
|
|
|
|
If you want to mount a directory for reading and writing, you can use both `FS_READ` and
|
|
`FS_WRITE` together:
|
|
|
|
```c
|
|
fs_mount(pFS, "/home/user/.config/mygame", "config", FS_READ | FS_WRITE);
|
|
```
|
|
|
|
You can set up read and write mount points to the same virtual path:
|
|
|
|
```c
|
|
fs_mount(pFS, "/usr/share/mygame/config", "config", FS_READ);
|
|
fs_mount(pFS, "/home/user/.local/share/mygame/config", "config", FS_READ | FS_WRITE);
|
|
```
|
|
|
|
When opening a file for reading, it'll first try searching the second mount point, and if it's not
|
|
found will fall back to the first. When opening in write mode, it will only ever use the second
|
|
mount point as the output directory because that's the only one set up with `FS_WRITE`. With this
|
|
setup, the first mount point is essentially protected from modification.
|
|
|
|
When mounting a directory for writing, the library will create the directory hierarchy for you. If
|
|
you want to disable this functionality, you can use the `FS_NO_CREATE_DIRS` flag:
|
|
|
|
```c
|
|
fs_mount(pFS, "/home/user/.config/mygame", "config", FS_WRITE | FS_NO_CREATE_DIRS);
|
|
```
|
|
|
|
|
|
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 when opening the file:
|
|
|
|
```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, "/usr/share/mygame/gamedata/base", "/gamedata", FS_READ);
|
|
```
|
|
|
|
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 "/usr/share/mygame/gamedata/base". When
|
|
opening a file with this kind of mount point, you would need to specify the leading slash:
|
|
|
|
```c
|
|
fs_file_open(pFS, "/gamedata/file.txt", FS_READ, &pFile); // Note how the path starts with "/".
|
|
```
|
|
|
|
|
|
You can also mount a archives to a virtual path:
|
|
|
|
```c
|
|
fs_mount(pFS, "/usr/share/mygame/gamedata.zip", "gamedata", FS_READ);
|
|
```
|
|
|
|
In order to do this, the `fs` object must have been configured with support for the given archive
|
|
type. 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.
|
|
|
|
|
|
The examples above have been hard coding paths, but you can use `fs_mount_sysdir()` to mount a
|
|
system directory to a virtual path. This is just a convenience helper function, and you need not
|
|
use it if you'd rather deal with system directories yourself:
|
|
|
|
```c
|
|
fs_mount_sysdir(pFS, FS_SYSDIR_CONFIG, "myapp", "/config", FS_READ | FS_WRITE);
|
|
```
|
|
|
|
This function requires that you specify a sub-directory of the system directory to mount. The reason
|
|
for this is to encourage the application to use good practice to avoid cluttering the file system.
|
|
|
|
Use `fs_unmount_sysdir()` to unmount a system directory. When using this you must specify the
|
|
sub-directory you used when mounting it:
|
|
|
|
```c
|
|
fs_unmount_sysdir(pFS, FS_SYSDIR_CONFIG, "myapp", FS_READ | FS_WRITE);
|
|
```
|
|
|
|
|
|
Mounting a `fs` object to a virtual path is also supported.
|
|
|
|
```c
|
|
fs* pSomeOtherFS; // <-- This would have been initialized earlier.
|
|
|
|
fs_mount_fs(pFS, pSomeOtherFS, "assets.zip", FS_READ);
|
|
|
|
...
|
|
|
|
fs_unmount_fs(pFS, pSomeOtherFS, FS_READ);
|
|
```
|
|
|
|
|
|
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.
|
|
|
|
When opening a file, 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 have access to 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.
|
|
|
|
|
|
|
|
1.4. 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 will need to do it manually.
|
|
You can inspect the `directory` member of the `info` member in `fs_iterator` to determine if the
|
|
entry is a directory.
|
|
|
|
|
|
1.5. System Directories
|
|
-----------------------
|
|
It can often be useful to know the exact paths of known standard system directories, such as the
|
|
home directory. You can use the `fs_sysdir()` function for this:
|
|
|
|
```c
|
|
char pPath[256];
|
|
size_t pathLen = fs_sysdir(FS_SYSDIR_HOME, pPath, sizeof(pPath));
|
|
if (pathLen > 0) {
|
|
if (pathLen < sizeof(pPath)) {
|
|
// Success!
|
|
} else {
|
|
// The buffer was too small. Expand the buffer to at least `pathLen + 1` and try again.
|
|
}
|
|
} else {
|
|
// An error occurred.
|
|
}
|
|
```
|
|
|
|
`fs_sysdir()` will return the length of the path written to `pPath`, or 0 if an error occurred. If
|
|
the buffer is too small, it will return the required size, not including the null terminator.
|
|
|
|
Recognized system directories include the following:
|
|
|
|
- FS_SYSDIR_HOME
|
|
- FS_SYSDIR_TEMP
|
|
- FS_SYSDIR_CONFIG
|
|
- FS_SYSDIR_DATA
|
|
- FS_SYSDIR_CACHE
|
|
|
|
|
|
|
|
1.6. Temporary Files
|
|
--------------------
|
|
You can create a temporary file or folder with `fs_mktmp()`. To create a temporary folder, use the
|
|
`FS_MKTMP_DIR` option:
|
|
|
|
```c
|
|
char pTmpPath[256];
|
|
fs_result result = fs_mktmp("prefix", pTmpPath, sizeof(pTmpPath), FS_MKTMP_DIR);
|
|
if (result != FS_SUCCESS) {
|
|
// Failed to create temporary file.
|
|
}
|
|
```
|
|
|
|
Similarly, to create a temporary file, use the `FS_MKTMP_FILE` option:
|
|
|
|
```c
|
|
char pTmpPath[256];
|
|
fs_result result = fs_mktmp("prefix", pTmpPath, sizeof(pTmpPath), FS_MKTMP_FILE);
|
|
if (result != FS_SUCCESS) {
|
|
// Failed to create temporary file.
|
|
}
|
|
```
|
|
|
|
`fs_mktmp()` will create a temporary file or folder with a unique name based on the provided
|
|
prefix and will return the full path to the created file or folder in `pTmpPath`. To open the
|
|
temporary file, you can pass in the path to `fs_file_open()`, making sure to ignore mount points
|
|
with `FS_IGNORE_MOUNTS`:
|
|
|
|
```c
|
|
fs_file* pFile;
|
|
result = fs_file_open(pFS, pTmpPath, FS_WRITE | FS_IGNORE_MOUNTS, &pFile);
|
|
if (result != FS_SUCCESS) {
|
|
// Failed to open temporary file.
|
|
}
|
|
```
|
|
|
|
If you just want to create a temporary file and don't care about the name, you can use
|
|
`fs_file_open()` with the `FS_TEMP` flag. In this case, the library will treat the file path
|
|
as the prefix:
|
|
|
|
```c
|
|
fs_file* pFile;
|
|
result = fs_file_open(pFS, "prefix", FS_TEMP, &pFile);
|
|
if (result != FS_SUCCESS) {
|
|
// Failed to open temporary file.
|
|
}
|
|
```
|
|
|
|
The use of temporary files is only valid with `fs` objects that make use of the standard file
|
|
system, such as the stdio backend.
|
|
|
|
The prefix can include subdirectories, such as "myapp/subdir". In this case the library will create
|
|
the directory hierarchy for you, unless you pass in `FS_NO_CREATE_DIRS`. Note that not all
|
|
platforms treat the name portion of the prefix the same. In particular, Windows will only use up to
|
|
the first 3 characters of the name portion of the prefix.
|
|
|
|
If you don't like the behavior of `fs_mktmp()`, you can consider using `fs_sysdir()` with
|
|
`FS_SYSDIR_TEMP` and create the temporary file yourself.
|
|
|
|
|
|
|
|
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!). A PAK backend is also included in the "extras" folder, and is simpler than
|
|
the ZIP backend which might also be a good place to start.
|
|
|
|
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 `ioctl` function is optional. You can use this to implement custom IO control commands. Return
|
|
`FS_INVALID_COMMAND` if the command is not recognized. The format of the `pArg` parameter is
|
|
command specific. If the backend does not need to implement this function, it can be left as `NULL`
|
|
or return `FS_NOT_IMPLEMENTED`.
|
|
|
|
The `remove` function is used to delete a file or directory. 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()`. This
|
|
function is mandatory.
|
|
|
|
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. Do *not* make a duplicate of the
|
|
stream with `fs_stream_duplicate()`. Instead just take a copy of the pointer. 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. Do not uninitialize the stream 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 current cursor position. There is only one cursor, even
|
|
when the file is opened in read and write mode.
|
|
|
|
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. This is mandatory.
|
|
|
|
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. You need not worry about thread-safety of a single individual file handle.
|
|
But when you have two different file handles, they must be able to be used on two different threads
|
|
at the same time.
|
|
|
|
|
|
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 will 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 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
|
|
|
|
#if defined(__cplusplus)
|
|
extern "C" {
|
|
#endif
|
|
|
|
/* BEG fs_compiler_compat.h */
|
|
#include <stddef.h> /* For size_t. */
|
|
#include <stdarg.h> /* For va_list. */
|
|
|
|
#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_PATH_TOO_LONG = -12,
|
|
FS_NOT_DIRECTORY = -14,
|
|
FS_IS_DIRECTORY = -15,
|
|
FS_DIRECTORY_NOT_EMPTY = -16,
|
|
FS_AT_END = -17,
|
|
FS_BUSY = -19,
|
|
FS_INTERRUPT = -21,
|
|
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.
|
|
*/
|
|
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);
|
|
/* BEG fs_stream_vtable_duplicate */
|
|
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. */
|
|
/* END fs_stream_vtable_duplicate */
|
|
};
|
|
|
|
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_seek(fs_stream* pStream, fs_int64 offset, fs_seek_origin origin);
|
|
FS_API fs_result fs_stream_tell(fs_stream* pStream, fs_int64* pCursor);
|
|
|
|
/* BEG fs_stream_writef.h */
|
|
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);
|
|
/* END fs_stream_writef.h */
|
|
|
|
/* BEG fs_stream_duplicate.h */
|
|
/*
|
|
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);
|
|
/* END fs_stream_duplicate.h */
|
|
|
|
/* BEG fs_stream_helpers.h */
|
|
/*
|
|
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_helpers.h */
|
|
/* END fs_stream.h */
|
|
|
|
|
|
/* BEG fs_sysdir.h */
|
|
typedef enum fs_sysdir_type
|
|
{
|
|
FS_SYSDIR_HOME,
|
|
FS_SYSDIR_TEMP,
|
|
FS_SYSDIR_CONFIG,
|
|
FS_SYSDIR_DATA,
|
|
FS_SYSDIR_CACHE
|
|
} fs_sysdir_type;
|
|
|
|
FS_API size_t fs_sysdir(fs_sysdir_type type, char* pDst, size_t dstCap); /* Returns the length of the string, or 0 on failure. If the return value is >= to dstCap it means the output buffer was too small. Use the returned value to know how big to make the buffer. Set pDst to NULL to calculate the required length. */
|
|
/* END fs_sysdir.h */
|
|
|
|
|
|
/* BEG fs_mktmp.h */
|
|
/* Make sure these options do not conflict with FS_NO_CREATE_DIRS. */
|
|
#define FS_MKTMP_DIR 0x0800 /* Create a temporary directory. */
|
|
#define FS_MKTMP_FILE 0x1000 /* Create a temporary file. */
|
|
|
|
FS_API fs_result fs_mktmp(const char* pPrefix, char* pTmpPath, size_t tmpPathCap, int options); /* Returns FS_PATH_TOO_LONG if the output buffer is too small. Use FS_MKTMP_FILE to create a file and FS_MKTMP_DIR to create a directory. Use FS_MKTMP_BASE_DIR to query the system base temp folder. pPrefix should not include the name of the system's base temp directory. Do not include paths like "/tmp" in the prefix. The output path will include the system's base temp directory and the prefix. */
|
|
/* END fs_mktmp.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_TEMP (FS_TRUNCATE | 0x0010)
|
|
|
|
#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(). */
|
|
|
|
#define FS_LOWEST_PRIORITY 0x2000 /* Only used with mounting. When set will create the mount with a lower priority to existing mounts. */
|
|
|
|
#define FS_NO_INCREMENT_REFCOUNT 0x4000 /* Internal use only. Used with fs_open_archive_ex() internally. */
|
|
|
|
/* 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;
|
|
|
|
/*
|
|
This callback is fired when the reference count of a fs object changes. This is useful if you want
|
|
to do some kind of advanced memory management, such as garbage collection. If the new reference count
|
|
is 1, it means no other objects are referencing the fs object.
|
|
*/
|
|
typedef void (* fs_on_refcount_changed_proc)(void* pUserData, fs* pFS, fs_uint32 newRefCount, fs_uint32 oldRefCount);
|
|
|
|
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;
|
|
fs_on_refcount_changed_proc onRefCountChanged;
|
|
void* pRefCountChangedUserData;
|
|
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);
|
|
|
|
|
|
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 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 FS_DOES_NOT_EXIST 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_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); /* Does not consider mounts. */
|
|
FS_API fs_result fs_rename(fs* pFS, const char* pOldName, const char* pNewName); /* Does not consider mounts. */
|
|
FS_API fs_result fs_mkdir(fs* pFS, const char* pPath, int options); /* Recursive. Will consider mounts unless FS_IGNORE_MOUNTS is specified. 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* fs_ref(fs* pFS); /* Increments the reference count. Returns pFS. */
|
|
FS_API fs_uint32 fs_unref(fs* pFS); /* Decrements the reference count. Does not uninitialize. */
|
|
FS_API fs_uint32 fs_refcount(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* pActualPath, const char* pVirtualPath, int options);
|
|
FS_API fs_result fs_unmount(fs* pFS, const char* pActualPath, int options);
|
|
FS_API fs_result fs_mount_sysdir(fs* pFS, fs_sysdir_type type, const char* pSubDir, const char* pVirtualPath, int options);
|
|
FS_API fs_result fs_unmount_sysdir(fs* pFS, fs_sysdir_type type, const char* pSubDir, int options);
|
|
FS_API fs_result fs_mount_fs(fs* pFS, fs* pOtherFS, const char* pVirtualPath, int options);
|
|
FS_API fs_result fs_unmount_fs(fs* pFS, fs* pOtherFS, int options); /* Must be matched up with fs_mount_fs(). */
|
|
|
|
/*
|
|
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_errno.h */
|
|
FS_API fs_result fs_result_from_errno(int error);
|
|
/* END fs_errno.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 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 int fs_path_compare(const char* pPathA, size_t pathALen, const char* pPathB, size_t pathBLen);
|
|
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 fs_bool32 fs_path_begins_with(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_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_strncmp(const char* str1, const char* str2, size_t maxLen);
|
|
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.
|
|
*/
|