Commit 587bd83c authored by David Reid's avatar David Reid

Update fs and fix some build errors with `-std=c89`.

parent b7e5451e
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -39,49 +39,154 @@ if (result != FS_SUCCESS) {
}
```
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(pFS, pBuffer, bytesToRead, &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.
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:
To do more advanced stuff, such as opening from archives, you'll need to configure the `fs` object
with a config, which you pass into `fs_init()`:
```c
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_ZIP, "pac"}
{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`). Whenever a file with a
"zip" or "pac" extension is found, the library will be able to access the archive. The library will
determine whether or not a file is an archive based on it's extension. You can use whatever
extension you would like for a backend, and you can associated multiple extensions to the same
backend. You can also associated different backends to the same extension, in which case the
library will use the first one that works. If the extension of a file does not match with one of
the registered archive types it'll assume it's not an archive and will skip it. Below is an example
of one way you can read from an archive:
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);
......@@ -116,8 +221,8 @@ if (result != FS_SUCCESS) {
}
```
In the example above, `FS_OPAQUE` is telling the library to treat archives as if they're totally
opaque and that the files within cannot be accessed.
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
......@@ -170,34 +275,45 @@ In addition to the above, you can use `fs_open_archive()` to open an archive fro
```c
fs* pArchive;
fs_open_archive(pFS, "archive.zip", FS_READ, &pArchive);
result = 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);
```
When opening an archive like this, it will inherit the archive types from the parent `fs` object
and will therefore support archives within archives. Use caution when doing this because if both
archives are compressed you will get a big performance hit. Only the inner-most archive should be
compressed.
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.2. Mounting
1.3. Mounting
-------------
There is no notion of a "current directory" in this library. By default, relative paths will be
relative to whatever the backend deems appropriate. In practice, this means the "current" directory
for the default system backend, and the root directory for archives. There is still control over
how to load files from a relative path, however: mounting.
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:
You can mount a physical directory to virtual path, similar in concept to Unix operating systems.
The difference, however, is that you can mount multiple directories to the same mount point in
which case a prioritization system will be used. There are separate mount points for reading and
writing. Below is an example of mounting for reading:
```c
fs_mount(pFS, "/some/actual/path", NULL, FS_READ);
```
To unmount, you need to specify the actual path, not the virtual path:
```c
fs_mount(pFS, "/some/actual/path", NULL, FS_MOUNT_PRIORITY_HIGHEST);
fs_unmount(pFS, "/some/actual/path", FS_READ);
```
In the example above, `NULL` is equivalent to an empty path. If, for example, you have a file with
the path "/some/actual/path/file.txt", you can open it like the following:
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);
......@@ -207,76 +323,75 @@ You don't need to specify the "/some/actual/path" part because it's handled by t
specify a virtual path, you can do something like the following:
```c
fs_mount(pFS, "/some/actual/path", "assets", FS_MOUNT_PRIORITY_HIGHEST);
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
prexied with "assets":
prefixed with "assets":
```c
fs_file_open(pFS, "assets/file.txt", FS_READ, &pFile);
```
Archives can also be mounted:
You can mount multiple paths to the same virtual path in which case a prioritization system will be
used:
```c
fs_mount(pFS, "/game/data/base/assets.zip", "assets", FS_MOUNT_PRIORITY_HIGHEST);
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.
```
You can mount multiple paths to the same mount point:
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, "/game/data/base.zip", "assets", FS_MOUNT_PRIORITY_HIGHEST);
fs_mount(pFS, "/game/data/mod1.zip", "assets", FS_MOUNT_PRIORITY_HIGHEST);
fs_mount(pFS, "/game/data/mod2.zip", "assets", FS_MOUNT_PRIORITY_HIGHEST);
fs_mount(pFS, "/home/user/.config/mygame", "config", FS_WRITE);
fs_mount(pFS, "/home/user/.local/share/mygame/saves", "saves", FS_WRITE);
```
In the example above, the "base.zip" archive is mounted first. Then "mod1.zip" is mounted, which
takes higher priority over "base.zip". Then "mod2.zip" is mounted which takes higher priority
again. With this set up, any file that is loaded from the "assets" mount point will first be loaded
from "mod2.zip", and if it doesn't exist there, "mod1.zip", and if not there, finally "base.zip".
You could use this set up to support simple modding prioritization in a game, for example.
If the file cannot be opened from any mounts it will attempt to open the file from the backend's
default search path. Mounts always take priority. When opening in transparent mode with
`FS_TRANSPARENT` (default), it will first try opening the file as if it were not in an archive. If
that fails, it will look inside archives.
You can also mount directories for writing:
To open a file for writing, you need only prefix the path with the mount's virtual path, exactly
like read mode:
```c
fs_mount_write(pFS, "/home/user/.config/mygame", "config", FS_MOUNT_PRIORITY_HIGHEST);
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.
```
You can then open a file for writing like so:
If you want to mount a directory for reading and writing, you can use both `FS_READ` and
`FS_WRITE` together:
```c
fs_file_open(pFS, "config/game.cfg", FS_WRITE, &pFile);
fs_mount(pFS, "/home/user/.config/mygame", "config", FS_READ | FS_WRITE);
```
When opening a file in write mode, the prefix is what determines which write mount point to use.
You can therefore have multiple write mounts:
You can set up read and write mount points to the same virtual path:
```c
fs_mount_write(pFS, "/home/user/.config/mygame", "config", FS_MOUNT_PRIORITY_HIGHEST);
fs_mount_write(pFS, "/home/user/.local/share/mygame/saves", "saves", FS_MOUNT_PRIORITY_HIGHEST);
fs_mount(pFS, "/usr/share/mygame/config", "config", FS_READ);
fs_mount(pFS, "/home/user/.local/share/mygame/config", "config", FS_READ | FS_WRITE);
```
Now you can write out different types of files, with the prefix being used to determine where it'll
be saved:
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_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.
fs_mount(pFS, "/home/user/.config/mygame", "config", FS_WRITE | FS_NO_CREATE_DIRS);
```
When opening a file for writing, if you pass in NULL for the `pFS` parameter it will open the file
like normal using the standard file system. That is it'll work exactly as if you were using stdio
`fopen()` and you will not be able use mount points. Keep in mind that there is no notion of a
"current directory" in this library so you'll be stuck with the initial working directory.
By default, you can move outside the mount point with ".." segments. If you want to disable this
functionality, you can use the `FS_NO_ABOVE_ROOT_NAVIGATION` flag:
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);
......@@ -286,19 +401,75 @@ In addition, any mount points that start with a "/" will be considered absolute
any above-root navigation:
```c
fs_mount(pFS, "/game/data/base", "/gamedata", FS_MOUNT_PRIORITY_HIGHEST);
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 "/game/data/base" when using this mount
point.
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);
```
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.
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:
1.3. Enumeration
```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:
......@@ -321,8 +492,101 @@ Internally, `fs_first()` will gather all of the enumerated files. This means you
Enumerated entries will be sorted by name in terms of `strcmp()`.
Enumeration is not recursive. If you want to enumerate recursively you can inspect the `directory`
member of the `info` member in `fs_iterator`.
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.
......@@ -354,7 +618,8 @@ 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!).
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
......@@ -400,10 +665,15 @@ not ever close or otherwise take ownership of the stream - that will be handled
The `uninit` function is where you should do any cleanup. Do not close the stream here.
The `remove` function is used to remove a file. This is not recursive. If the path is a directory,
the backend should return an error if it is not empty. Backends do not need to implement this
function in which case they can leave the callback pointer as `NULL`, or have it return
`FS_NOT_IMPLEMENTED`.
The `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
......@@ -416,7 +686,8 @@ 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()`.
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
......@@ -426,14 +697,16 @@ not need any additional data, it can return 0. The backend can access this data
The `file_open` function is where the backend should open the file. If the `fs` object that owns
the file was initialized with a stream, i.e. it's an archive, the stream will be non-null. You
should store this pointer for later use in `file_read`, etc. The `openMode` parameter will be a
combination of `FS_READ`, `FS_WRITE`, `FS_TRUNCATE`, `FS_APPEND` and `FS_OVERWRITE`. When opening
in write mode (`FS_WRITE`), it will default to truncate mode. You should ignore the `FS_OPAQUE`,
`FS_VERBOSE` and `FS_TRANSPARENT` flags. If the file does not exist, the backend should return
`FS_DOES_NOT_EXIST`. If the file is a directory, it should return `FS_IS_DIRECTORY`.
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. The stream should not be closed here - it'll be cleaned up at a higher
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`
......@@ -446,13 +719,14 @@ specified if the backend supports writing.
The `file_seek` function is used to seek the cursor. The backend should return `FS_BAD_SEEK` if the
seek is out of bounds.
The `file_tell` function is used to get the cursor position.
The `file_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.
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
......@@ -469,7 +743,9 @@ to deal with this is to allocate the allocate additional space for the name imme
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.
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
......@@ -486,7 +762,7 @@ For `fs_file`, simply opening the file is enough. For `fs_memory_stream`, you ne
implement your own stream type you would need to implement a similar initialization function.
Use `fs_stream_read()` and `fs_stream_write()` to read and write data from a stream. If the stream
does not support reading or writing, the respective function should return `FS_NOT_IMPLEMENTED`.
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.
......@@ -500,8 +776,8 @@ uninitialize a duplicated stream - `fs_stream_delete_duplicate()` will deal with
Streams are not thread safe. If you want to use a stream across multiple threads, you will need to
synchronize access to it yourself. Using different stream objects across multiple threads is safe.
A duplicated stream is entirely independent of the original stream and can be used across on a
different thread to the original stream.
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
......@@ -521,14 +797,14 @@ see some random tags and stuff in this file. These are just used for doing a dum
#ifndef fs_h
#define fs_h
#include <stddef.h> /* For size_t. */
#include <stdarg.h> /* For va_list. */
#if defined(__cplusplus)
extern "C" {
#endif
/* BEG fs_compiler_compat.h */
#include <stddef.h> /* For size_t. */
#include <stdarg.h> /* For va_list. */
#if defined(SIZE_MAX)
#define FS_SIZE_MAX SIZE_MAX
#else
......@@ -665,11 +941,13 @@ typedef enum fs_result
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,
......@@ -710,8 +988,6 @@ The stream vtable can support both reading and writing, but it doesn't need to s
the same time. If one is not supported, simply leave the relevant `read` or `write` callback as
`NULL`, or have them return FS_NOT_IMPLEMENTED.
*/
/* Seek Origins. */
typedef enum fs_seek_origin
{
FS_SEEK_SET = 0,
......@@ -728,9 +1004,11 @@ struct fs_stream_vtable
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
......@@ -741,13 +1019,17 @@ struct fs_stream
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);
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);
/* END fs_stream_writef.h */
/* BEG fs_stream_duplicate.h */
/*
Duplicates a stream.
......@@ -762,8 +1044,9 @@ 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().
......@@ -781,9 +1064,32 @@ typedef enum fs_format
} 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. */
......@@ -792,6 +1098,7 @@ FS_API fs_result fs_stream_read_to_end(fs_stream* pStream, fs_format format, con
#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. */
......@@ -803,6 +1110,9 @@ FS_API fs_result fs_stream_read_to_end(fs_stream* pStream, fs_format format, con
#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. */
......@@ -816,11 +1126,12 @@ typedef struct fs_file_info fs_file_info;
typedef struct fs_iterator fs_iterator;
typedef struct fs_backend fs_backend;
typedef enum fs_mount_priority
{
FS_MOUNT_PRIORITY_HIGHEST = 0,
FS_MOUNT_PRIORITY_LOWEST = 1
} fs_mount_priority;
/*
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
{
......@@ -852,6 +1163,8 @@ struct fs_config
fs_stream* pStream;
const fs_archive_type* pArchiveTypes;
size_t archiveTypeCount;
fs_on_refcount_changed_proc onRefCountChanged;
void* pRefCountChangedUserData;
const fs_allocation_callbacks* pAllocationCallbacks;
};
......@@ -859,7 +1172,7 @@ FS_API fs_config fs_config_init_default(void);
FS_API fs_config fs_config_init(const fs_backend* pBackend, void* pBackendConfig, fs_stream* pStream);
typedef struct fs_backend
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. */
......@@ -868,9 +1181,9 @@ typedef struct fs_backend
fs_result (* remove )(fs* pFS, const char* pFilePath);
fs_result (* rename )(fs* pFS, const char* pOldName, const char* pNewName);
fs_result (* mkdir )(fs* pFS, const char* pPath); /* This is not recursive. Return FS_SUCCESS if directory already exists. */
fs_result (* info )(fs* pFS, const char* pPath, int openMode, fs_file_info* pInfo); /* openMode flags can be ignored by most backends. It's primarily used by proxy of passthrough style backends. */
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 ENOENT if the file does not exist. pStream will be null if the backend does not need a stream (the `pFS` object was not initialized with one). */
fs_result (* file_open )(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. */
......@@ -883,19 +1196,22 @@ typedef struct fs_backend
fs_iterator* (* first )(fs* pFS, const char* pDirectoryPath, size_t directoryPathLen);
fs_iterator* (* next )(fs_iterator* pIterator); /* <-- Must return null when there are no more files. In this case, free_iterator must be called internally. */
void (* free_iterator )(fs_iterator* pIterator); /* <-- Free the `fs_iterator` object here since `first` and `next` were the ones who allocated it. Also do any uninitialization routines. */
} fs_backend;
};
FS_API fs_result fs_init(const fs_config* pConfig, fs** ppFS);
FS_API void fs_uninit(fs* pFS);
FS_API fs_result fs_ioctl(fs* pFS, int op, void* pArg);
FS_API fs_result fs_remove(fs* pFS, const char* pFilePath);
FS_API fs_result fs_rename(fs* pFS, const char* pOldName, const char* pNewName);
FS_API fs_result fs_mkdir(fs* pFS, const char* pPath); /* Does not consider mounts. Returns FS_SUCCESS if directory already exists. */
FS_API fs_result fs_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);
......@@ -926,13 +1242,12 @@ FS_API fs_iterator* fs_first(fs* pFS, const char* pDirectoryPath, int mode);
FS_API fs_iterator* fs_next(fs_iterator* pIterator);
FS_API void fs_free_iterator(fs_iterator* pIterator);
FS_API fs_result fs_mount(fs* pFS, const char* pPathToMount, const char* pMountPoint, fs_mount_priority priority);
FS_API fs_result fs_unmount(fs* pFS, const char* pPathToMount_NotMountPoint);
FS_API fs_result fs_mount_fs(fs* pFS, fs* pOtherFS, const char* pMountPoint, fs_mount_priority priority);
FS_API fs_result fs_unmount_fs(fs* pFS, fs* pOtherFS); /* Must be matched up with fs_mount_fs(). */
FS_API fs_result fs_mount_write(fs* pFS, const char* pPathToMount, const char* pMountPoint, fs_mount_priority priority);
FS_API fs_result fs_unmount_write(fs* pFS, const char* pPathToMount_NotMountPoint);
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
......@@ -957,27 +1272,18 @@ extern const fs_backend* FS_STDIO; /* The default stdio backend. The handle for
/* END fs.h */
/* BEG fs_helpers.h */
/*
This section just contains various helper functions, mainly for custom backends.
*/
/* Converts an errno code to our own error code. */
/* BEG fs_errno.h */
FS_API fs_result fs_result_from_errno(int error);
/* END fs_helpers.h */
/* 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 just one or
the other, or a different separator, you'll need to use a different library. Likewise, this library
will treat paths as case sensitive. Again, you'll need to use a different library if this is not
suitable for you.
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
......@@ -1005,11 +1311,13 @@ 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 */
......@@ -1028,7 +1336,7 @@ also supports reading.
You can overwrite data by seeking to the required location and then just writing like normal. To
append data, just seek to the end:
fs_memory_stream_seek(pStream, 0, fs_SEEK_ORIGIN_END);
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
......@@ -1121,6 +1429,7 @@ FS_API void* fs_binary_search(const void* pKey, const void* pList, size_t count,
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 */
......
#include "../../external/fs/fs.c" /* <-- Must come first due to some (hopefully temporary) hacks to work around some `-std=c89` errors. */
#include "../../miniaudio.c"
#include "../../external/fs/fs.c"
#include <stdio.h>
......@@ -43,7 +43,7 @@ int ma_run_tests(int argc, char** argv)
ma_bool32 hasError = MA_FALSE;
size_t iTest;
fs_mkdir(NULL, TEST_OUTPUT_DIR);
fs_mkdir(NULL, TEST_OUTPUT_DIR, FS_IGNORE_MOUNTS);
for (iTest = 0; iTest < g_Tests.count; iTest += 1) {
printf("=== BEGIN %s ===\n", g_Tests.pTests[iTest].pName);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment