8000 miniaudio/external/fs/fs.h at master · Fuxfork/miniaudio · GitHub
[go: up one dir, main page]

Skip to content

Latest commit

 

History

History
3527 lines (2601 loc) · 125 KB

fs.h

File metadata and controls

3527 lines (2601 loc) · 125 KB
/*
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);
```
Formatted writing is also supported:
```c
result = fs_file_writef(pFile, "Hello %s!\n", "World");
if (result != FS_SUCCESS) {
// Failed to write file.
}
va_list args;
va_start(args, format);
result = fs_file_writefv(pFile, "Hello %s!\n", args);
va_end(args);
```
The `FS_WRITE` option will default to overwrite mode. You can use `FS_TRUNCATE` if you want to
truncate the file instead of overwriting it.
```c
fs_file_open(pFS, "file.txt", FS_WRITE | FS_TRUNCATE, &pFile);
```
You can also open a file in append mode with `FS_APPEND`:
```c
fs_file_open(pFS, "file.txt", FS_WRITE | FS_APPEND, &pFile);
```
When using `FS_APPEND` mode, the file will always append to the end of the file and can never
overwrite existing content. This follows POSIX semantics. In this mode, it is not possible to
create sparse files.
To open a file in write mode, but fail if the file already exists, you can use `FS_EXCLUSIVE`:
```c
fs_file_open(pFS, "file.txt" 8000 , FS_WRITE | FS_EXCLUSIVE, &pFile);
```
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);
```
When seeking, you can seek beyond the end of the file. Attempting to read from beyond the end of
the file will return `FS_AT_END`. Attempting to write beyond the end of the file will create a
hole if supported by the file system, or fill the space with data (the filled data can be left
undefined). When seeking from the end of the file with a negative offset, it will seek backwards
from the end. Seeking to before the start of the file is not allowed and will return an error.
Retrieving information about a file is done with `fs_file_get_info()`:
```c
fs_file_info info;
fs_file_get_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", FS_READ, &info); // FS_READ tells it to check read-only mounts (explained later)
```
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,
including having its own separate read/write cursor position. The initial position of the cursor of
the new file handle is undefined and you should explicitly seek to the appropriate location.
Important: `fs_file_duplicate()` can fail or work incorrectly if you use relative paths for mounts
and something changes the working directory. The reason being that it reopens the file based on the
original path to do the duplication.
To delete a file, use `fs_remove()`:
```c
fs_remove(pFS, "file.txt");
```
Note that files are deleted permanently. There is no recycle bin or trash functionality.
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_default();
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 its
extension. You can use whatever extension you would like for a backend, and you can associate
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 virtual path in which case a
prioritization system will be used (only for reading - in write mode only a single mount is 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 "/".
```
Important: When using mount points that start with "/", if the file cannot be opened from the mount,
it will fall back to trying the actual absolute path. To prevent this and ensure files are only
loaded from the mount point, use the `FS_ONLY_MOUNTS` flag when opening files. Alternatively,
simply avoid using "/" prefixed mounts and instead use `FS_NO_ABOVE_ROOT_NAVIGATION` for security.
You can also mount an archive 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 and 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.
You can also skip mount points when opening a file by using the `FS_IGNORE_MOUNTS` flag:
```c
fs_file_open(pFS, "/absolute/path/to/file.txt", FS_READ | FS_IGNORE_MOUNTS, &pFile);
```
This can be useful when you want to access a file directly without going through the mount system,
such as when working with temporary files.
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.
}
```
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. Platform Considerations
============================
3.1. Windows
--------------
On Windows, Unicode support is determined by the `UNICODE` preprocessor define. When `UNICODE` is
defined, the library will use the wide character versions of Windows APIs. When not defined, it
will use the ANSI versions.
3.2. POSIX
------------
On POSIX platforms, `ftruncate()` is unavailable with `-std=c89` unless `_XOPEN_SOURCE` is defined
to >= 500. This may affect the availability of file truncation functionality when using strict C89
compilation.
4. Backends
===========
You can implement custom backends to support different file systems and archive formats. A POSIX
or Win32 backend is the default backend depending on the platform, 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.
4.1. Backend Functions
----------------------
alloc_size
This function should return the size of the backend-specific data to associate with the `fs`
object. If no additional data is required, return 0.
The main library will allocate the `fs` object, including any additional space specified by the
`alloc_size` function.
init
This function is called after `alloc_size()` and after the `fs` object has been allocated. This
is where you should 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 "5. 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.
uninit
This is where you should do any cleanup. Do not close the stream here.
ioctl
This function is optional. You can use this to implement custom IO control commands. Return
`FS_INVALID_OPERATION` 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`.
remove
This 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`.
rename
This 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`.
mkdir
This function is used to create a directory. This is not recursive. If the directory already
exists, the backend should return `FS_ALREADY_EXISTS`. If a parent directory does not exist,
the backend should return `FS_DOES_NOT_EXIST`. This function is optional and can be left as
`NULL` or return `FS_NOT_IMPLEMENTED`.
info
This 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.
file_alloc_size
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()`.
file_open
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, the stream will be
non-null. If your backends requires a stream, you should check that the stream is null, and if
so, return an error. The reason you need to check for this is that the application itself may
erroneously attempt to initialize a `fs` object for your backend without a stream, and since
the library cannot know whether or not a backend requires a stream, it cannot check this on
your behalf.
If the backend requires a stream, it should take a copy of only the pointer and store it for
later use. Do *not* make a duplicate of the stream with `fs_stream_duplicate()`.
Backends need only handle the following open mode flags:
FS_READ
FS_WRITE
FS_TRUNCATE
FS_APPEND
FS_EXCLUSIVE
All other flags are for use at a higher level and should be ignored.
When opening in write mode (`FS_WRITE`), the backend should default to overwrite mode. If
`FS_TRUNCATE` is specified, the file should be truncated to 0 length. If `FS_APPEND` is
specified, all writes should happen at the end of the file regardless of the position of the
write cursor. If `FS_EXCLUSIVE` is specified, opening should fail if the file already exists.
In all write modes, the file should be created if it does not already exist.
If any flags cannot be supported, the backend should return an error.
When opening in read mode, if the file does not exist, `FS_DOES_NOT_EXIST` should be returned.
Similarly, if the file is a directory, `FS_IS_DIRECTORY` should be returned.
Before calling into this function, the library will normalize all paths to use forward slashes.
Therefore, backends must support forward slashes ("/") as path separators.
file_close
This function is where the file should be closed. 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.
file_read
This 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.
file_write
This is used to write data to the file. If the file is opened in append mode, the backend
should always ensure writes are appended to the end, regardless of the position of the write
cursor. This is optional and need only be specified if the backend supports writing.
file_seek
The `file_seek` function is used to seek the read/write cursor. The backend should allow
seeking beyond the end of the file. If the file is opened in write mode and data is written
beyond the end of the file, the file should be extended, and if possible made into a sparse
file. If sparse files are not supported, the backend should fill the gap with data, preferably
with zeros if possible.
Attempting to seek to before the start of the file should return `FS_BAD_SEEK`.
file_tell
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.
file_flush
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`.
file_truncate
The `file_truncate` function is used to truncate a file to the current cursor position. This is
only useful for write mode, so is therefore optional and can be left as `NULL` or return
`FS_NOT_IMPLEMENTED`.
file_info
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.
file_duplicate
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 backend must ensure that the duplicated file is totally independent of the
original file and has its own independent read/write pointer. If the backend is unable to
support duplicated files having their own independent read/write pointer, it must return an
error.
If a backend cannot support duplication, it can leave this as `NULL` or return
`FS_NOT_IMPLEMENTED`. However, if this is not implemented, the backend will not be able to open
files within archives.
first, next, free_iterator
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 additional space for the name
immediately after the `fs_iterator` allocation.
4.2. Thread Safety
------------------
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.
5. 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. You can retrieve the stream associated with a `fs_file` using `fs_file_get_stream()`.
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
0