├── .gitignore ├── CMakeLists.txt ├── README.md ├── examples ├── archives.c ├── hello_world.c └── mounting.c ├── extras └── backends │ ├── pak │ ├── fs_pak.c │ └── fs_pak.h │ ├── sub │ ├── fs_sub.c │ └── fs_sub.h │ └── zip │ ├── fs_zip.c │ └── fs_zip.h ├── fs.c ├── fs.h ├── tests └── fstest.c └── tools └── fsu.c /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | \#docs/ 3 | build/ 4 | testvectors/ 5 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | project(fs 3 | VERSION 1.0 4 | ) 5 | 6 | # Options 7 | option(FS_BUILD_EXAMPLES "Build typo examples" OFF) 8 | option(FS_BUILD_TESTS "Build typo tests" OFF) 9 | option(FS_BUILD_TOOLS "Build typo tools" OFF) 10 | option(FS_FORCE_CXX "Force compilation as C++" OFF) 11 | option(FS_FORCE_C89 "Force compilation as C89" OFF) 12 | option(FS_ENABLE_OPENED_FILES_ASSERT "Enable an assert for when a file is left open" OFF) 13 | 14 | 15 | # Construct compiler options. 16 | set(COMPILE_OPTIONS) 17 | 18 | if(FS_FORCE_CXX AND FS_FORCE_C89) 19 | message(FATAL_ERROR "FS_FORCE_CXX and FS_FORCE_C89 cannot be enabled at the same time.") 20 | endif() 21 | 22 | if(FS_FORCE_CXX) 23 | if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") 24 | message(STATUS "Compiling as C++ (GNU/Clang)") 25 | list(APPEND COMPILE_OPTIONS -x c++) 26 | elseif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") 27 | message(STATUS "Compiling as C++ (MSVC)") 28 | list(APPEND COMPILE_OPTIONS /TP) 29 | else() 30 | message(WARNING "FS_FORCE_CXX is enabled but the compiler does not support it. Ignoring.") 31 | endif() 32 | endif() 33 | 34 | if(FS_FORCE_C89) 35 | if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") 36 | message(STATUS "Compiling as C89") 37 | list(APPEND COMPILE_OPTIONS -std=c89) 38 | elseif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") 39 | message(WARNING "MSVC does not support forcing C89. FS_FORCE_C89 ignored.") 40 | else() 41 | message(WARNING "FS_FORCE_C89 is enabled but the compiler does not support it. Ignoring.") 42 | endif() 43 | endif() 44 | 45 | # Warnings 46 | if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") 47 | list(APPEND COMPILE_OPTIONS -Wall -Wextra -Wpedantic) 48 | elseif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") 49 | #list(APPEND COMPILE_OPTIONS /W4) 50 | endif() 51 | 52 | 53 | 54 | # Static Libraries 55 | add_library(fs STATIC 56 | fs.c 57 | fs.h 58 | ) 59 | 60 | target_include_directories(fs PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) 61 | 62 | target_compile_options(fs PRIVATE ${COMPILE_OPTIONS}) 63 | 64 | add_library(fszip STATIC 65 | extras/backends/zip/fs_zip.c 66 | extras/backends/zip/fs_zip.h 67 | ) 68 | target_compile_options(fszip PRIVATE ${COMPILE_OPTIONS}) 69 | 70 | add_library(fspak STATIC 71 | extras/backends/pak/fs_pak.c 72 | extras/backends/pak/fs_pak.h 73 | ) 74 | target_compile_options(fspak PRIVATE ${COMPILE_OPTIONS}) 75 | 76 | add_library(fssub STATIC 77 | extras/backends/sub/fs_sub.c 78 | extras/backends/sub/fs_sub.h 79 | ) 80 | target_compile_options(fssub PRIVATE ${COMPILE_OPTIONS}) 81 | 82 | 83 | # Tests 84 | if(FS_BUILD_TESTS) 85 | enable_testing() 86 | 87 | add_executable(fstest tests/fstest.c) 88 | target_link_libraries(fstest PRIVATE 89 | fs 90 | fszip 91 | fspak 92 | fssub 93 | ) 94 | target_compile_options(fstest PRIVATE ${COMPILE_OPTIONS}) 95 | 96 | add_test(NAME fstest COMMAND fstest) 97 | endif() 98 | 99 | 100 | # Examples 101 | if(FS_BUILD_EXAMPLES) 102 | # hello_world 103 | add_executable(hello_world examples/hello_world.c) 104 | target_link_libraries(hello_world PRIVATE 105 | fs 106 | ) 107 | target_compile_options(hello_world PRIVATE ${COMPILE_OPTIONS}) 108 | 109 | # archives 110 | add_executable(archives examples/archives.c) 111 | target_link_libraries(archives PRIVATE 112 | fs 113 | fszip 114 | fspak 115 | ) 116 | target_compile_options(archives PRIVATE ${COMPILE_OPTIONS}) 117 | 118 | # mounting 119 | add_executable(mounting examples/mounting.c) 120 | target_link_libraries(mounting PRIVATE 121 | fs 122 | fszip 123 | fspak 124 | ) 125 | target_compile_options(mounting PRIVATE ${COMPILE_OPTIONS}) 126 | endif() 127 | 128 | 129 | # Tools 130 | if(FS_BUILD_TOOLS) 131 | # fsu 132 | add_executable(fsu tools/fsu.c) 133 | target_link_libraries(fsu PRIVATE 134 | fs 135 | fszip 136 | fspak 137 | ) 138 | target_compile_options(fsu PRIVATE ${COMPILE_OPTIONS}) 139 | endif() 140 | 141 | 142 | # TODO: Always link with "-static" on Windows when compiling with GCC and Clang, and /MTd or /MT for MSVC. If MSVC, have option "STATIC_CRT" like what libgit2 does. 143 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a cross-platform library that provides a unified API for accessing the native file system 2 | and archives such as ZIP. It's written in C (compilable as C++), has no external dependencies, and 3 | is your choice of either public domain or [MIT No Attribution](https://github.com/aws/mit-0). 4 | 5 | 6 | About 7 | ===== 8 | See the documentation at the top of [fs.h](fs.h) for details on how to use the library. 9 | 10 | This library supports the ability to open files from the both the native file system and virtual 11 | file systems through a unified API. File systems are implemented as backends, examples of which can 12 | be found in the [extras/backends](extras/backends) folder. Custom backends can be implemented and 13 | plugged in without needing to modify the library. 14 | 15 | One common use of a backend, and the motivation for the creation of this library in the first 16 | place, is to support archive formats like ZIP. You can plug in archive backends and associate them 17 | with a file extension to enable seamless and transparent handling of archives. For example, you can 18 | associate the ZIP backend with the "zip" extension which will then let you load a file like this: 19 | 20 | ```c 21 | fs_file_open(pFS, "myapp/archive.zip/file.txt", FS_READ, &pFile); 22 | ``` 23 | 24 | The example above explicitly lists the "archive.zip" archive in the path, but you need not specify 25 | that if you don't want to. The following will also work: 26 | 27 | ```c 28 | fs_file_open(pFS, "myapp/file.txt", FS_READ, &pFile); 29 | ``` 30 | 31 | Here the archive is handled totally transparently. There are many options for opening a file, some 32 | of which are more efficient than others. See the documentation in [fs.h](fs.h) for details. 33 | 34 | When opening a file, the library searches through a list of mount points that you set up 35 | beforehand. Below is an example for setting up a simple mount point that will be used when opening 36 | a file for reading: 37 | 38 | ```c 39 | fs_mount(pFS, "/home/user/.local/share/myapp", NULL, FS_READ); 40 | ``` 41 | 42 | With this setup, opening a file in read-only mode will open the file relative to the path specified 43 | in `fs_mount()`. You can also specify a virtual path for a mount point: 44 | 45 | ```c 46 | fs_mount(pFS, "/home/user/.local/share/myapp", "data", FS_READ); 47 | ``` 48 | 49 | With the mount above you would need to prefix your path with "data" when opening a file: 50 | 51 | ```c 52 | fs_file_open(pFS, "data/file.txt", FS_READ, &pFile); 53 | ``` 54 | 55 | Since the path starts with "data", the paths mounted to the virtual path "data" will be searched 56 | when opening the file. 57 | 58 | You can also mount multiple paths to the same virtual path, in which case the later will take 59 | precedence. Below is an example you might see in a game: 60 | 61 | ```c 62 | fs_mount(pFS, "/usr/share/mygame/gamedata/base", "gamedata", FS_READ); // Base game. Lowest priority. 63 | fs_mount(pFS, "/home/user/.local/share/mygame/gamedata/mod1", "gamedata", FS_READ); // Mod #1. Middle priority. 64 | fs_mount(pFS, "/home/user/.local/share/mygame/gamedata/mod2", "gamedata", FS_READ); // Mod #2. Highest priority. 65 | ``` 66 | 67 | You could then load an asset like this: 68 | 69 | ```c 70 | fs_file_open(pFS, "gamedata/texture.png", FS_READ, &pFile); 71 | ``` 72 | 73 | Here there "mod2" directory would be searched first because it has the highest priority. If the 74 | file cannot be opened from there it'll fall back to "mod1", and then as a last resort it'll fall 75 | back to the base game. 76 | 77 | Internally there are a separate set of mounts for reading and writing. To set up a mount point for 78 | opening files in write mode, you need to specify the `FS_WRITE` option: 79 | 80 | ```c 81 | fs_mount(pFS, "/home/user/.config/mygame", "config", FS_WRITE); 82 | ``` 83 | 84 | To open a file for writing, you need only prefix the path with the mount's virtual path, exactly 85 | like read mode: 86 | 87 | ```c 88 | fs_file_open(pFS, "config/game.cfg", FS_WRITE, &pFile); 89 | ``` 90 | 91 | You can set up read and write mount points to the same virtual path: 92 | 93 | ```c 94 | fs_mount(pFS, "/usr/share/mygame/config", "config", FS_READ); 95 | fs_mount(pFS, "/home/user/.local/share/mygame/config", "config", FS_READ | FS_WRITE); 96 | ``` 97 | 98 | When opening a file for reading, it'll first try searching the second mount point, and if it's not 99 | found will fall back to the first. When opening in write mode, it will only ever use the second 100 | mount point as the output directory because that's the only one set up with `FS_WRITE`. With this 101 | setup, the first mount point is essentially protected from modification. 102 | 103 | There's more you can do with mounting, such as mounting an archive or `fs` object to a virtual 104 | path, helpers for mounting system directories, and preventing navigation outside the mount point. 105 | See the documentation at the top of [fs.h](fs.h) for more details. 106 | 107 | In addition to the aforementioned functionality, the library includes all of the standard 108 | functionality you would expect for file IO, such as file enumeration, stat-ing (referred to as 109 | "info" in this library), creating directories and deleting and renaming files. 110 | 111 | 112 | Usage 113 | ===== 114 | See [fs.h](fs.h) for documentation. Examples can be found in the [examples](examples) folder. Backends can 115 | be found in the [extras/backends](extras/backends) folder. 116 | 117 | 118 | Building 119 | ======== 120 | To build the library, just add the necessary source files to your source tree. The main library is 121 | contained within fs.c. Archive backends are each contained in their own separate file. Stock 122 | archive backends can be found in the [extras/backends](extras/backends) folder. These will have a 123 | .h file which you should include after fs.h: 124 | 125 | ```c 126 | #include "fs.h" 127 | #include "extras/backends/zip/fs_zip.h" 128 | #include "extras/backends/pak/fs_pak.h" 129 | ``` 130 | 131 | You need only include backends that you care about. See examples/archives.c for an example on how 132 | to use archives. 133 | 134 | You can also use CMake, but support for that is very basic. 135 | 136 | 137 | License 138 | ======= 139 | Your choice of either public domain or [MIT No Attribution](https://github.com/aws/mit-0). 140 | -------------------------------------------------------------------------------- /examples/archives.c: -------------------------------------------------------------------------------- 1 | /* 2 | Shows basic usage of how to make use of archives with the library. 3 | 4 | This examples depends on the ZIP backend which is located in the "extras" folder. 5 | 6 | Like the hello_world example, this will load a file and print it to the console. The difference is 7 | that this example will support the ability to load files from a ZIP archive. 8 | 9 | When loading from an archive, you need to specify the backend and map it to a file extension. The 10 | extension is what the library uses to determine which backend to use when opening a file. 11 | 12 | To see this in action, consider specifying a file path like "archive.zip/file.txt". This will 13 | ensure the file is loaded from the ZIP archive. 14 | */ 15 | #include "../fs.h" 16 | #include "../extras/backends/zip/fs_zip.h" /* <-- This is where FS_ZIP is declared. */ 17 | 18 | #include 19 | 20 | int main(int argc, char** argv) 21 | { 22 | fs_result result; 23 | fs* pFS; 24 | fs_config fsConfig; 25 | fs_file* pFile; 26 | char buffer[4096]; 27 | size_t bytesRead; 28 | fs_archive_type pArchiveTypes[] = 29 | { 30 | {FS_ZIP, "zip"} 31 | }; 32 | 33 | if (argc < 2) { 34 | printf("Usage: archives \n"); 35 | return 1; 36 | } 37 | 38 | fsConfig = fs_config_init(FS_STDIO, NULL, NULL); 39 | fsConfig.pArchiveTypes = pArchiveTypes; 40 | fsConfig.archiveTypeCount = sizeof(pArchiveTypes) / sizeof(pArchiveTypes[0]); 41 | 42 | result = fs_init(&fsConfig, &pFS); 43 | if (result != FS_SUCCESS) { 44 | printf("Failed to initialize file system: %d\n", result); 45 | return 1; 46 | } 47 | 48 | result = fs_file_open(pFS, argv[1], FS_READ, &pFile); 49 | if (result != FS_SUCCESS) { 50 | fs_uninit(pFS); 51 | printf("Failed to open file: %d\n", result); 52 | return 1; 53 | } 54 | 55 | while (fs_file_read(pFile, buffer, sizeof(buffer), &bytesRead) == FS_SUCCESS) { 56 | printf("%.*s", (int)bytesRead, buffer); 57 | } 58 | 59 | fs_file_close(pFile); 60 | fs_uninit(pFS); 61 | 62 | return 0; 63 | } 64 | -------------------------------------------------------------------------------- /examples/hello_world.c: -------------------------------------------------------------------------------- 1 | /* 2 | Shows the most basic of basic usage of the library. 3 | 4 | This will simply load a file from the default file system and print it to stdout. 5 | 6 | At the most basic level, a `fs` object is not required which is what this example demonstrates. 7 | */ 8 | #include "../fs.h" 9 | #include 10 | 11 | int main(int argc, char** argv) 12 | { 13 | fs_result result; 14 | fs_file* pFile; 15 | char buffer[4096]; 16 | size_t bytesRead; 17 | 18 | if (argc < 2) { 19 | printf("Usage: hello_world \n"); 20 | return 1; 21 | } 22 | 23 | result = fs_file_open(NULL, argv[1], FS_READ, &pFile); 24 | if (result != FS_SUCCESS) { 25 | printf("Failed to open file: %d\n", result); 26 | return 1; 27 | } 28 | 29 | while (fs_file_read(pFile, buffer, sizeof(buffer), &bytesRead) == FS_SUCCESS) { 30 | printf("%.*s", (int)bytesRead, buffer); 31 | } 32 | 33 | fs_file_close(pFile); 34 | 35 | return 0; 36 | } 37 | -------------------------------------------------------------------------------- /examples/mounting.c: -------------------------------------------------------------------------------- 1 | /* 2 | Basic demonstration of how to mount directories. 3 | 4 | Mounting a directory essentially adds it as a search path when opening files. You can mount 5 | multiple directories to the same mount point, in which case it's prioritized by the order in 6 | which they were mounted. 7 | 8 | In this example we will mount the following folders to the mount point "mnt": 9 | 10 | "testvectors/mounting/src1" 11 | "testvectors/mounting/src2" 12 | 13 | The latter will take precedence over the former, so if a file exists in both directories, the 14 | one in "src2" will be loaded. You can swap these around to see the difference. 15 | 16 | This example uses "mnt" as the mount point, but you can use anything you like, including an empty 17 | string. You just need to remember to use the same mount point when specifying the path of the file 18 | to open. 19 | 20 | This examples omits some cleanup for the sake of brevity. 21 | */ 22 | #include "../fs.h" 23 | #include 24 | 25 | int main(int argc, char** argv) 26 | { 27 | fs_result result; 28 | fs* pFS; 29 | fs_file* pFile; 30 | char buffer[4096]; 31 | size_t bytesRead; 32 | 33 | result = fs_init(NULL, &pFS); 34 | if (result != FS_SUCCESS) { 35 | printf("Failed to initialize file system: %d\n", result); 36 | return 1; 37 | } 38 | 39 | result = fs_mount(pFS, "testvectors/mounting/src1", "mnt", FS_READ); 40 | if (result != FS_SUCCESS) { 41 | printf("Failed to mount directory: %d\n", result); 42 | return 1; 43 | } 44 | 45 | result = fs_mount(pFS, "testvectors/mounting/src2", "mnt", FS_READ); 46 | if (result != FS_SUCCESS) { 47 | printf("Failed to mount directory: %d\n", result); 48 | return 1; 49 | } 50 | 51 | result = fs_file_open(pFS, "mnt/hello", FS_READ, &pFile); 52 | if (result != FS_SUCCESS) { 53 | printf("Failed to open file: %d\n", result); 54 | return 1; 55 | } 56 | 57 | while (fs_file_read(pFile, buffer, sizeof(buffer), &bytesRead) == FS_SUCCESS) { 58 | printf("%.*s", (int)bytesRead, buffer); 59 | } 60 | 61 | fs_file_close(pFile); 62 | fs_uninit(pFS); 63 | 64 | (void)argc; 65 | (void)argv; 66 | 67 | return 0; 68 | } 69 | -------------------------------------------------------------------------------- /extras/backends/pak/fs_pak.c: -------------------------------------------------------------------------------- 1 | #ifndef fs_pak_c 2 | #define fs_pak_c 3 | 4 | #include "../../../fs.h" 5 | #include "fs_pak.h" 6 | 7 | #include 8 | #include 9 | 10 | #ifndef FS_PAK_COPY_MEMORY 11 | #define FS_PAK_COPY_MEMORY(dst, src, sz) memcpy((dst), (src), (sz)) 12 | #endif 13 | 14 | #ifndef FS_PAK_ASSERT 15 | #define FS_PAK_ASSERT(x) assert(x) 16 | #endif 17 | 18 | #define FS_PAK_ABS(x) (((x) > 0) ? (x) : -(x)) 19 | 20 | 21 | /* BEG fs_pak.c */ 22 | static FS_INLINE fs_bool32 fs_pak_is_le(void) 23 | { 24 | #if defined(__x86_64__) || defined(_M_X64) || defined(__i386) || defined(_M_IX86) 25 | return FS_TRUE; 26 | #elif defined(__BYTE_ORDER) && defined(__LITTLE_ENDIAN) && __BYTE_ORDER == __LITTLE_ENDIAN 27 | return FS_TRUE; 28 | #else 29 | int n = 1; 30 | return (*(char*)&n) == 1; 31 | #endif 32 | } 33 | 34 | static FS_INLINE unsigned int fs_pak_swap_endian_32(unsigned int n) 35 | { 36 | return 37 | ((n & 0xFF000000) >> 24) | 38 | ((n & 0x00FF0000) >> 8) | 39 | ((n & 0x0000FF00) << 8) | 40 | ((n & 0x000000FF) << 24); 41 | } 42 | 43 | static FS_INLINE unsigned int fs_pak_le2ne_32(unsigned int n) 44 | { 45 | if (!fs_pak_is_le()) { 46 | return fs_pak_swap_endian_32(n); 47 | } 48 | 49 | return n; 50 | } 51 | 52 | 53 | typedef struct fs_pak_toc_entry 54 | { 55 | char name[56]; 56 | fs_uint32 offset; 57 | fs_uint32 size; 58 | } fs_pak_toc_entry; 59 | 60 | typedef struct fs_pak 61 | { 62 | fs_uint32 fileCount; 63 | fs_pak_toc_entry* pTOC; 64 | } fs_pak; 65 | 66 | 67 | static size_t fs_alloc_size_pak(const void* pBackendConfig) 68 | { 69 | (void)pBackendConfig; 70 | return sizeof(fs_pak); 71 | } 72 | 73 | static fs_result fs_init_pak(fs* pFS, const void* pBackendConfig, fs_stream* pStream) 74 | { 75 | fs_pak* pPak; 76 | fs_result result; 77 | char fourcc[4]; 78 | fs_uint32 tocOffset; 79 | fs_uint32 tocSize; 80 | 81 | /* No need for a backend config. */ 82 | (void)pBackendConfig; 83 | 84 | if (pStream == NULL) { 85 | return FS_INVALID_OPERATION; /* Most likely the FS is being opened without a stream. */ 86 | } 87 | 88 | pPak = (fs_pak*)fs_get_backend_data(pFS); 89 | FS_PAK_ASSERT(pPak != NULL); 90 | 91 | result = fs_stream_read(pStream, fourcc, sizeof(fourcc), NULL); 92 | if (result != FS_SUCCESS) { 93 | return result; 94 | } 95 | 96 | if (fourcc[0] != 'P' || fourcc[1] != 'A' || fourcc[2] != 'C' || fourcc[3] != 'K') { 97 | return FS_INVALID_FILE; /* Not a PAK file. */ 98 | } 99 | 100 | result = fs_stream_read(pStream, &tocOffset, 4, NULL); 101 | if (result != FS_SUCCESS) { 102 | return result; 103 | } 104 | 105 | result = fs_stream_read(pStream, &tocSize, 4, NULL); 106 | if (result != FS_SUCCESS) { 107 | return result; 108 | } 109 | 110 | tocOffset = fs_pak_le2ne_32(tocOffset); 111 | tocSize = fs_pak_le2ne_32(tocSize); 112 | 113 | /* Seek to the TOC so we can read it. */ 114 | result = fs_stream_seek(pStream, tocOffset, FS_SEEK_SET); 115 | if (result != FS_SUCCESS) { 116 | return result; 117 | } 118 | 119 | pPak->pTOC = (fs_pak_toc_entry*)fs_malloc(tocSize, fs_get_allocation_callbacks(pFS)); 120 | if (pPak->pTOC == NULL) { 121 | return FS_OUT_OF_MEMORY; 122 | } 123 | 124 | result = fs_stream_read(pStream, pPak->pTOC, tocSize, NULL); 125 | if (result != FS_SUCCESS) { 126 | fs_free(pPak->pTOC, fs_get_allocation_callbacks(pFS)); 127 | pPak->pTOC = NULL; 128 | return result; 129 | } 130 | 131 | pPak->fileCount = tocSize / sizeof(fs_pak_toc_entry); 132 | 133 | /* Swap the endianness of the TOC. */ 134 | if (!fs_pak_is_le()) { 135 | fs_uint32 i; 136 | for (i = 0; i < pPak->fileCount; i += 1) { 137 | pPak->pTOC[i].offset = fs_pak_swap_endian_32(pPak->pTOC[i].offset); 138 | pPak->pTOC[i].size = fs_pak_swap_endian_32(pPak->pTOC[i].size); 139 | } 140 | } 141 | 142 | return FS_SUCCESS; 143 | } 144 | 145 | static void fs_uninit_pak(fs* pFS) 146 | { 147 | fs_pak* pPak = (fs_pak*)fs_get_backend_data(pFS); 148 | FS_PAK_ASSERT(pPak != NULL); 149 | 150 | fs_free(pPak->pTOC, fs_get_allocation_callbacks(pFS)); 151 | return; 152 | } 153 | 154 | static fs_result fs_info_pak(fs* pFS, const char* pPath, int openMode, fs_file_info* pInfo) 155 | { 156 | fs_pak* pPak; 157 | fs_uint32 tocIndex; 158 | 159 | (void)openMode; 160 | 161 | pPak = (fs_pak*)fs_get_backend_data(pFS); 162 | FS_PAK_ASSERT(pPak != NULL); 163 | 164 | for (tocIndex = 0; tocIndex < pPak->fileCount; tocIndex += 1) { 165 | if (fs_path_compare(pPak->pTOC[tocIndex].name, FS_NULL_TERMINATED, pPath, FS_NULL_TERMINATED) == 0) { 166 | pInfo->size = pPak->pTOC[tocIndex].size; 167 | pInfo->directory = 0; 168 | 169 | return FS_SUCCESS; 170 | } 171 | } 172 | 173 | /* Getting here means we couldn't find a file with the given path, but it might be a folder. */ 174 | for (tocIndex = 0; tocIndex < pPak->fileCount; tocIndex += 1) { 175 | if (fs_path_begins_with(pPak->pTOC[tocIndex].name, FS_NULL_TERMINATED, pPath, FS_NULL_TERMINATED)) { 176 | pInfo->size = 0; 177 | pInfo->directory = 1; 178 | 179 | return FS_SUCCESS; 180 | } 181 | } 182 | 183 | /* Getting here means neither a file nor folder was found. */ 184 | return FS_DOES_NOT_EXIST; 185 | } 186 | 187 | 188 | typedef struct fs_file_pak 189 | { 190 | fs_stream* pStream; 191 | fs_uint32 tocIndex; 192 | fs_uint32 cursor; 193 | } fs_file_pak; 194 | 195 | static size_t fs_file_alloc_size_pak(fs* pFS) 196 | { 197 | (void)pFS; 198 | return sizeof(fs_file_pak); 199 | } 200 | 201 | static fs_result fs_file_open_pak(fs* pFS, fs_stream* pStream, const char* pPath, int openMode, fs_file* pFile) 202 | { 203 | fs_pak* pPak; 204 | fs_file_pak* pPakFile; 205 | fs_uint32 tocIndex; 206 | fs_result result; 207 | 208 | pPak = (fs_pak*)fs_get_backend_data(pFS); 209 | FS_PAK_ASSERT(pPak != NULL); 210 | 211 | pPakFile = (fs_file_pak*)fs_file_get_backend_data(pFile); 212 | FS_PAK_ASSERT(pPakFile != NULL); 213 | 214 | /* Write mode is currently unsupported. */ 215 | if ((openMode & FS_WRITE) != 0) { 216 | return FS_INVALID_OPERATION; 217 | } 218 | 219 | for (tocIndex = 0; tocIndex < pPak->fileCount; tocIndex += 1) { 220 | if (fs_path_compare(pPak->pTOC[tocIndex].name, FS_NULL_TERMINATED, pPath, FS_NULL_TERMINATED) == 0) { 221 | pPakFile->tocIndex = tocIndex; 222 | pPakFile->cursor = 0; 223 | pPakFile->pStream = pStream; 224 | 225 | result = fs_stream_seek(pPakFile->pStream, pPak->pTOC[tocIndex].offset, FS_SEEK_SET); 226 | if (result != FS_SUCCESS) { 227 | return FS_INVALID_FILE; /* Failed to seek. Archive is probably corrupt. */ 228 | } 229 | 230 | return FS_SUCCESS; 231 | } 232 | } 233 | 234 | /* Getting here means the file was not found. */ 235 | return FS_DOES_NOT_EXIST; 236 | } 237 | 238 | static void fs_file_close_pak(fs_file* pFile) 239 | { 240 | /* Nothing to do. */ 241 | (void)pFile; 242 | } 243 | 244 | static fs_result fs_file_read_pak(fs_file* pFile, void* pDst, size_t bytesToRead, size_t* pBytesRead) 245 | { 246 | fs_file_pak* pPakFile; 247 | fs_pak* pPak; 248 | fs_result result; 249 | fs_uint32 bytesRemainingInFile; 250 | 251 | pPakFile = (fs_file_pak*)fs_file_get_backend_data(pFile); 252 | FS_PAK_ASSERT(pPakFile != NULL); 253 | 254 | pPak = (fs_pak*)fs_get_backend_data(fs_file_get_fs(pFile)); 255 | FS_PAK_ASSERT(pPak != NULL); 256 | 257 | bytesRemainingInFile = pPak->pTOC[pPakFile->tocIndex].size - pPakFile->cursor; 258 | if (bytesRemainingInFile == 0) { 259 | return FS_AT_END; /* No more bytes remaining. Must return AT_END. */ 260 | } 261 | 262 | if (bytesToRead > bytesRemainingInFile) { 263 | bytesToRead = bytesRemainingInFile; 264 | } 265 | 266 | result = fs_stream_read(pPakFile->pStream, pDst, bytesToRead, pBytesRead); 267 | if (result != FS_SUCCESS) { 268 | return result; 269 | } 270 | 271 | pPakFile->cursor += (fs_uint32)*pBytesRead; 272 | FS_PAK_ASSERT(pPakFile->cursor <= pPak->pTOC[pPakFile->tocIndex].size); 273 | 274 | return FS_SUCCESS; 275 | } 276 | 277 | static fs_result fs_file_seek_pak(fs_file* pFile, fs_int64 offset, fs_seek_origin origin) 278 | { 279 | fs_file_pak* pPakFile; 280 | fs_pak* pPak; 281 | fs_result result; 282 | fs_int64 newCursor; 283 | 284 | pPakFile = (fs_file_pak*)fs_file_get_backend_data(pFile); 285 | FS_PAK_ASSERT(pPakFile != NULL); 286 | 287 | pPak = (fs_pak*)fs_get_backend_data(fs_file_get_fs(pFile)); 288 | FS_PAK_ASSERT(pPak != NULL); 289 | 290 | if (FS_PAK_ABS(offset) > 0xFFFFFFFF) { 291 | return FS_BAD_SEEK; /* Offset is too large. */ 292 | } 293 | 294 | if (origin == FS_SEEK_SET) { 295 | newCursor = 0; 296 | } else if (origin == FS_SEEK_CUR) { 297 | newCursor = pPakFile->cursor; 298 | } else if (origin == FS_SEEK_END) { 299 | newCursor = pPak->pTOC[pPakFile->tocIndex].size; 300 | } else { 301 | FS_PAK_ASSERT(!"Invalid seek origin."); 302 | return FS_INVALID_ARGS; 303 | } 304 | 305 | newCursor += offset; 306 | if (newCursor < 0) { 307 | return FS_BAD_SEEK; /* Negative offset. */ 308 | } 309 | if (newCursor > pPak->pTOC[pPakFile->tocIndex].size) { 310 | return FS_BAD_SEEK; /* Offset is larger than file size. */ 311 | } 312 | 313 | result = fs_stream_seek(pPakFile->pStream, newCursor, FS_SEEK_SET); 314 | if (result != FS_SUCCESS) { 315 | return result; 316 | } 317 | 318 | pPakFile->cursor = (fs_uint32)newCursor; /* Safe cast. */ 319 | 320 | return FS_SUCCESS; 321 | } 322 | 323 | static fs_result fs_file_tell_pak(fs_file* pFile, fs_int64* pCursor) 324 | { 325 | fs_file_pak* pPakFile; 326 | 327 | pPakFile = (fs_file_pak*)fs_file_get_backend_data(pFile); 328 | FS_PAK_ASSERT(pPakFile != NULL); 329 | 330 | *pCursor = pPakFile->cursor; 331 | 332 | return FS_SUCCESS; 333 | } 334 | 335 | static fs_result fs_file_flush_pak(fs_file* pFile) 336 | { 337 | /* Nothing to do. */ 338 | (void)pFile; 339 | return FS_SUCCESS; 340 | } 341 | 342 | static fs_result fs_file_info_pak(fs_file* pFile, fs_file_info* pInfo) 343 | { 344 | fs_file_pak* pPakFile; 345 | fs_pak* pPak; 346 | 347 | pPakFile = (fs_file_pak*)fs_file_get_backend_data(pFile); 348 | FS_PAK_ASSERT(pPakFile != NULL); 349 | 350 | pPak = (fs_pak*)fs_get_backend_data(fs_file_get_fs(pFile)); 351 | FS_PAK_ASSERT(pPak != NULL); 352 | 353 | FS_PAK_ASSERT(pInfo != NULL); 354 | pInfo->size = pPak->pTOC[pPakFile->tocIndex].size; 355 | pInfo->directory = FS_FALSE; /* An opened file should never be a directory. */ 356 | 357 | return FS_SUCCESS; 358 | } 359 | 360 | static fs_result fs_file_duplicate_pak(fs_file* pFile, fs_file* pDuplicatedFile) 361 | { 362 | fs_file_pak* pPakFile; 363 | fs_file_pak* pDuplicatedPakFile; 364 | 365 | pPakFile = (fs_file_pak*)fs_file_get_backend_data(pFile); 366 | FS_PAK_ASSERT(pPakFile != NULL); 367 | 368 | pDuplicatedPakFile = (fs_file_pak*)fs_file_get_backend_data(pDuplicatedFile); 369 | FS_PAK_ASSERT(pDuplicatedPakFile != NULL); 370 | 371 | /* We should be able to do this with a simple memcpy. */ 372 | FS_PAK_COPY_MEMORY(pDuplicatedPakFile, pPakFile, fs_file_alloc_size_pak(fs_file_get_fs(pFile))); 373 | 374 | return FS_SUCCESS; 375 | } 376 | 377 | 378 | typedef struct fs_iterator_pak 379 | { 380 | fs_iterator base; 381 | fs_uint32 index; /* The index of the current item. */ 382 | fs_uint32 count; /* The numebr of entries in items. */ 383 | struct 384 | { 385 | char name[56]; 386 | fs_uint32 tocIndex; /* Will be set to 0xFFFFFFFF for directories. */ 387 | } items[1]; 388 | } fs_iterator_pak; 389 | 390 | static fs_bool32 fs_iterator_item_exists_pak(fs_iterator_pak* pIteratorPak, const char* pName, size_t nameLen) 391 | { 392 | fs_uint32 i; 393 | 394 | for (i = 0; i < pIteratorPak->count; i += 1) { 395 | if (fs_strncmp(pIteratorPak->items[i].name, pName, nameLen) == 0) { 396 | return FS_TRUE; 397 | } 398 | } 399 | 400 | return FS_FALSE; 401 | } 402 | 403 | static void fs_iterator_resolve_pak(fs_iterator_pak* pIteratorPak) 404 | { 405 | fs_pak* pPak; 406 | 407 | pPak = (fs_pak*)fs_get_backend_data(pIteratorPak->base.pFS); 408 | FS_PAK_ASSERT(pPak != NULL); 409 | 410 | pIteratorPak->base.pName = pIteratorPak->items[pIteratorPak->index].name; 411 | pIteratorPak->base.nameLen = strlen(pIteratorPak->base.pName); 412 | 413 | memset(&pIteratorPak->base.info, 0, sizeof(fs_file_info)); 414 | if (pIteratorPak->items[pIteratorPak->index].tocIndex == 0xFFFFFFFF) { 415 | pIteratorPak->base.info.directory = FS_TRUE; 416 | pIteratorPak->base.info.size = 0; 417 | } else { 418 | pIteratorPak->base.info.directory = FS_FALSE; 419 | pIteratorPak->base.info.size = pPak->pTOC[pIteratorPak->items[pIteratorPak->index].tocIndex].size; 420 | } 421 | } 422 | 423 | FS_API fs_iterator* fs_first_pak(fs* pFS, const char* pDirectoryPath, size_t directoryPathLen) 424 | { 425 | /* 426 | PAK files only list files. They do not include any explicit directory entries. We'll therefore need 427 | to derive folders from the list of file paths. This means we'll need to accumulate the list of 428 | entries in this functions. 429 | */ 430 | fs_pak* pPak; 431 | fs_iterator_pak* pIteratorPak; 432 | fs_uint32 tocIndex; 433 | fs_uint32 itemCap = 16; 434 | 435 | pPak = (fs_pak*)fs_get_backend_data(pFS); 436 | FS_PAK_ASSERT(pPak != NULL); 437 | 438 | pIteratorPak = (fs_iterator_pak*)fs_calloc(sizeof(fs_iterator_pak) + (itemCap - 1) * sizeof(pIteratorPak->items[0]), fs_get_allocation_callbacks(pFS)); 439 | if (pIteratorPak == NULL) { 440 | return NULL; 441 | } 442 | 443 | pIteratorPak->base.pFS = pFS; 444 | pIteratorPak->index = 0; 445 | pIteratorPak->count = 0; 446 | 447 | /* Skip past "/" if it was specified. */ 448 | if (directoryPathLen > 0) { 449 | if (pDirectoryPath[0] == '/') { 450 | pDirectoryPath += 1; 451 | if (directoryPathLen != FS_NULL_TERMINATED) { 452 | directoryPathLen -= 1; 453 | } 454 | } 455 | } 456 | 457 | for (tocIndex = 0; tocIndex < pPak->fileCount; tocIndex += 1) { 458 | const char* pPathTail = fs_path_trim_base(pPak->pTOC[tocIndex].name, FS_NULL_TERMINATED, pDirectoryPath, directoryPathLen); 459 | if (pPathTail != NULL) { 460 | /* 461 | The file is contained within the directory, but it might not necessarily be appropriate to 462 | add this entry. We need to look at the next segments. If there is only one segment, it means 463 | this is a file and we can add it straight to the list. Otherwise, if there is an additional 464 | segment it means it's a folder, in which case we'll need to ensure there are no duplicates. 465 | */ 466 | fs_path_iterator iPathSegment; 467 | if (fs_path_first(pPathTail, FS_NULL_TERMINATED, &iPathSegment) == FS_SUCCESS) { 468 | /* 469 | It's a candidate. If this item is valid for this iteration, the name will be that of the 470 | first segment. 471 | */ 472 | if (!fs_iterator_item_exists_pak(pIteratorPak, iPathSegment.pFullPath + iPathSegment.segmentOffset, iPathSegment.segmentLength)) { 473 | if (pIteratorPak->count >= itemCap) { 474 | fs_iterator_pak* pNewIterator; 475 | 476 | itemCap *= 2; 477 | pNewIterator = (fs_iterator_pak*)fs_realloc(pIteratorPak, sizeof(fs_iterator_pak) + (itemCap - 1) * sizeof(pIteratorPak->items[0]), fs_get_allocation_callbacks(pFS)); 478 | if (pNewIterator == NULL) { 479 | fs_free(pIteratorPak, fs_get_allocation_callbacks(pFS)); 480 | return NULL; /* Out of memory. */ 481 | } 482 | 483 | pIteratorPak = pNewIterator; 484 | } 485 | 486 | FS_PAK_COPY_MEMORY(pIteratorPak->items[pIteratorPak->count].name, iPathSegment.pFullPath + iPathSegment.segmentOffset, iPathSegment.segmentLength); 487 | pIteratorPak->items[pIteratorPak->count].name[iPathSegment.segmentLength] = '\0'; 488 | 489 | if (fs_path_is_last(&iPathSegment)) { 490 | /* It's a file. */ 491 | pIteratorPak->items[pIteratorPak->count].tocIndex = tocIndex; 492 | } else { 493 | /* It's a directory. */ 494 | pIteratorPak->items[pIteratorPak->count].tocIndex = 0xFFFFFFFF; 495 | } 496 | 497 | pIteratorPak->count += 1; 498 | } else { 499 | /* An item with the same name already exists. Skip. */ 500 | } 501 | } else { 502 | /* 503 | pDirectoryPath is exactly equal to this item. Since PAK archives only explicitly list files 504 | and not directories, it means pDirectoryPath is actually a file. It is invalid to try iterating 505 | a file, so we need to abort. 506 | */ 507 | return NULL; 508 | } 509 | } else { 510 | /* This file is not contained within the given directory. */ 511 | } 512 | } 513 | 514 | if (pIteratorPak->count == 0) { 515 | fs_free(pIteratorPak, fs_get_allocation_callbacks(pFS)); 516 | return NULL; 517 | } 518 | 519 | pIteratorPak->index = 0; 520 | fs_iterator_resolve_pak(pIteratorPak); 521 | 522 | return (fs_iterator*)pIteratorPak; 523 | } 524 | 525 | FS_API fs_iterator* fs_next_pak(fs_iterator* pIterator) 526 | { 527 | fs_iterator_pak* pIteratorPak = (fs_iterator_pak*)pIterator; 528 | 529 | FS_PAK_ASSERT(pIteratorPak != NULL); 530 | 531 | if (pIteratorPak->index + 1 >= pIteratorPak->count) { 532 | fs_free(pIterator, fs_get_allocation_callbacks(pIteratorPak->base.pFS)); 533 | return NULL; /* No more items. */ 534 | } 535 | 536 | pIteratorPak->index += 1; 537 | fs_iterator_resolve_pak(pIteratorPak); 538 | 539 | return (fs_iterator*)pIteratorPak; 540 | } 541 | 542 | FS_API void fs_free_iterator_pak(fs_iterator* pIterator) 543 | { 544 | fs_iterator_pak* pIteratorPak = (fs_iterator_pak*)pIterator; 545 | FS_PAK_ASSERT(pIteratorPak != NULL); 546 | 547 | fs_free(pIteratorPak, fs_get_allocation_callbacks(pIteratorPak->base.pFS)); 548 | } 549 | 550 | 551 | fs_backend fs_pak_backend = 552 | { 553 | fs_alloc_size_pak, 554 | fs_init_pak, 555 | fs_uninit_pak, 556 | NULL, /* ioctl */ 557 | NULL, /* remove */ 558 | NULL, /* rename */ 559 | NULL, /* mkdir */ 560 | fs_info_pak, 561 | fs_file_alloc_size_pak, 562 | fs_file_open_pak, 563 | NULL, /* open_handle */ 564 | fs_file_close_pak, 565 | fs_file_read_pak, 566 | NULL, /* write */ 567 | fs_file_seek_pak, 568 | fs_file_tell_pak, 569 | fs_file_flush_pak, 570 | fs_file_info_pak, 571 | fs_file_duplicate_pak, 572 | fs_first_pak, 573 | fs_next_pak, 574 | fs_free_iterator_pak 575 | }; 576 | const fs_backend* FS_PAK = &fs_pak_backend; 577 | /* END fs_pak.c */ 578 | 579 | #endif /* fs_pak_c */ 580 | -------------------------------------------------------------------------------- /extras/backends/pak/fs_pak.h: -------------------------------------------------------------------------------- 1 | /* 2 | Quake PAK file support. 3 | */ 4 | #ifndef fs_pak_h 5 | #define fs_pak_h 6 | 7 | #if defined(__cplusplus) 8 | extern "C" { 9 | #endif 10 | 11 | /* BEG fs_pak.h */ 12 | extern const fs_backend* FS_PAK; 13 | /* END fs_pak.h */ 14 | 15 | #if defined(__cplusplus) 16 | } 17 | #endif 18 | #endif /* fs_pak_h */ 19 | -------------------------------------------------------------------------------- /extras/backends/sub/fs_sub.c: -------------------------------------------------------------------------------- 1 | #ifndef fs_sub_c 2 | #define fs_sub_c 3 | 4 | #include "../../../fs.h" 5 | #include "fs_sub.h" 6 | 7 | #include 8 | #include 9 | 10 | #define FS_SUB_UNUSED(x) (void)x 11 | 12 | static void fs_sub_zero_memory_default(void* p, size_t sz) 13 | { 14 | if (sz > 0) { 15 | memset(p, 0, sz); 16 | } 17 | } 18 | 19 | #ifndef FS_SUB_ZERO_MEMORY 20 | #define FS_SUB_ZERO_MEMORY(p, sz) fs_sub_zero_memory_default((p), (sz)) 21 | #endif 22 | #define FS_SUB_ZERO_OBJECT(p) FS_SUB_ZERO_MEMORY((p), sizeof(*(p))) 23 | 24 | #ifndef FS_SUB_ASSERT 25 | #define FS_SUB_ASSERT(x) assert(x) 26 | #endif 27 | 28 | 29 | /* Bit of a naughty hack. This is defined with FS_API in fs.c, but is not forward declared in fs.h. We can just declare it here. */ 30 | FS_API char* fs_strcpy(char* dst, const char* src); 31 | 32 | 33 | /* BEG fs_sub.c */ 34 | typedef struct fs_sub 35 | { 36 | fs* pOwnerFS; 37 | char* pRootDir; /* Points to the end of the structure. */ 38 | size_t rootDirLen; 39 | } fs_sub; 40 | 41 | typedef struct fs_file_sub 42 | { 43 | fs_file* pActualFile; 44 | } fs_file_sub; 45 | 46 | 47 | typedef struct fs_sub_path 48 | { 49 | char pFullPathStack[1024]; 50 | char* pFullPathHeap; 51 | char* pFullPath; 52 | int fullPathLen; 53 | } fs_sub_path; 54 | 55 | static fs_result fs_sub_path_init(fs* pFS, const char* pPath, size_t pathLen, fs_sub_path* pSubFSPath) 56 | { 57 | fs_sub* pSubFS; 58 | char pPathCleanStack[1024]; 59 | char* pPathCleanHeap = NULL; 60 | char* pPathClean; 61 | size_t pathCleanLen; 62 | 63 | FS_SUB_ASSERT(pFS != NULL); 64 | FS_SUB_ASSERT(pPath != NULL); 65 | FS_SUB_ASSERT(pSubFSPath != NULL); 66 | 67 | FS_SUB_ZERO_OBJECT(pSubFSPath); /* Safety. */ 68 | 69 | /* We first have to clean the path, with a strict requirement that we fail if attempting to navigate above the root. */ 70 | pathCleanLen = fs_path_normalize(pPathCleanStack, sizeof(pPathCleanStack), pPath, pathLen, FS_NO_ABOVE_ROOT_NAVIGATION); 71 | if (pathCleanLen <= 0) { 72 | return FS_DOES_NOT_EXIST; /* Almost certainly because we're trying to navigate above the root directory. */ 73 | } 74 | 75 | if (pathCleanLen >= sizeof(pPathCleanStack)) { 76 | pPathCleanHeap = (char*)fs_malloc(pathCleanLen + 1, fs_get_allocation_callbacks(pFS)); 77 | if (pPathCleanHeap == NULL) { 78 | return FS_OUT_OF_MEMORY; 79 | } 80 | 81 | fs_path_normalize(pPathCleanHeap, pathCleanLen + 1, pPath, pathLen, FS_NO_ABOVE_ROOT_NAVIGATION); /* This will never fail. */ 82 | pPathClean = pPathCleanHeap; 83 | } else { 84 | pPathClean = pPathCleanStack; 85 | } 86 | 87 | /* Now that the input path has been cleaned we need only append it to the base path. */ 88 | pSubFS = (fs_sub*)fs_get_backend_data(pFS); 89 | FS_SUB_ASSERT(pSubFS != NULL); 90 | 91 | pSubFSPath->fullPathLen = fs_path_append(pSubFSPath->pFullPathStack, sizeof(pSubFSPath->pFullPathStack), pSubFS->pRootDir, pSubFS->rootDirLen, pPathClean, pathCleanLen); 92 | if (pSubFSPath->fullPathLen < 0) { 93 | fs_free(pPathCleanHeap, fs_get_allocation_callbacks(pFS)); 94 | return FS_ERROR; /* Should never hit this, but leaving here for safety. */ 95 | } 96 | 97 | if (pSubFSPath->fullPathLen >= (int)sizeof(pSubFSPath->pFullPathStack)) { 98 | pSubFSPath->pFullPathHeap = (char*)fs_malloc(pSubFSPath->fullPathLen + 1, fs_get_allocation_callbacks(pFS)); 99 | if (pSubFSPath->pFullPathHeap == NULL) { 100 | fs_free(pPathCleanHeap, fs_get_allocation_callbacks(pFS)); 101 | return FS_OUT_OF_MEMORY; 102 | } 103 | 104 | fs_path_append(pSubFSPath->pFullPathHeap, pSubFSPath->fullPathLen + 1, pSubFS->pRootDir, pSubFS->rootDirLen, pPathClean, pathCleanLen); /* This will never fail. */ 105 | pSubFSPath->pFullPath = pSubFSPath->pFullPathHeap; 106 | } else { 107 | pSubFSPath->pFullPath = pSubFSPath->pFullPathStack; 108 | } 109 | 110 | return FS_SUCCESS; 111 | } 112 | 113 | static void fs_sub_path_uninit(fs_sub_path* pSubFSPath) 114 | { 115 | if (pSubFSPath->pFullPathHeap != NULL) { 116 | fs_free(pSubFSPath->pFullPathHeap, fs_get_allocation_callbacks(NULL)); 117 | } 118 | 119 | FS_SUB_ZERO_OBJECT(pSubFSPath); 120 | } 121 | 122 | 123 | static size_t fs_alloc_size_sub(const void* pBackendConfig) 124 | { 125 | fs_sub_config* pSubFSConfig = (fs_sub_config*)pBackendConfig; 126 | 127 | if (pSubFSConfig == NULL) { 128 | return 0; /* The sub config must be specified. */ 129 | } 130 | 131 | /* We include a copy of the path with the main allocation. */ 132 | return sizeof(fs_sub) + strlen(pSubFSConfig->pRootDir) + 1 + 1; /* +1 for trailing slash and +1 for null terminator. */ 133 | } 134 | 135 | static fs_result fs_init_sub(fs* pFS, const void* pBackendConfig, fs_stream* pStream) 136 | { 137 | fs_sub_config* pSubFSConfig = (fs_sub_config*)pBackendConfig; 138 | fs_sub* pSubFS; 139 | 140 | FS_SUB_ASSERT(pFS != NULL); 141 | FS_SUB_UNUSED(pStream); 142 | 143 | if (pSubFSConfig == NULL) { 144 | return FS_INVALID_ARGS; /* Must have a config. */ 145 | } 146 | 147 | if (pSubFSConfig->pOwnerFS == NULL) { 148 | return FS_INVALID_ARGS; /* Must have an owner FS. */ 149 | } 150 | 151 | if (pSubFSConfig->pRootDir == NULL) { 152 | return FS_INVALID_ARGS; /* Must have a root directory. */ 153 | } 154 | 155 | pSubFS = (fs_sub*)fs_get_backend_data(pFS); 156 | FS_SUB_ASSERT(pFS != NULL); 157 | 158 | pSubFS->pOwnerFS = pSubFSConfig->pOwnerFS; 159 | pSubFS->pRootDir = (char*)(pSubFS + 1); 160 | pSubFS->rootDirLen = strlen(pSubFSConfig->pRootDir); 161 | 162 | fs_strcpy(pSubFS->pRootDir, pSubFSConfig->pRootDir); 163 | 164 | /* Append a trailing slash if necessary. */ 165 | if (pSubFS->pRootDir[pSubFS->rootDirLen - 1] != '/') { 166 | pSubFS->pRootDir[pSubFS->rootDirLen] = '/'; 167 | pSubFS->pRootDir[pSubFS->rootDirLen + 1] = '\0'; 168 | pSubFS->rootDirLen += 1; 169 | } 170 | 171 | return FS_SUCCESS; 172 | } 173 | 174 | static void fs_uninit_sub(fs* pFS) 175 | { 176 | /* Nothing to do here. */ 177 | FS_SUB_UNUSED(pFS); 178 | } 179 | 180 | static fs_result fs_ioctl_sub(fs* pFS, int op, void* pArgs) 181 | { 182 | fs_sub* pSubFS; 183 | 184 | pSubFS = (fs_sub*)fs_get_backend_data(pFS); 185 | FS_SUB_ASSERT(pSubFS != NULL); 186 | 187 | return fs_ioctl(pSubFS->pOwnerFS, op, pArgs); 188 | } 189 | 190 | static fs_result fs_remove_sub(fs* pFS, const char* pFilePath) 191 | { 192 | fs_result result; 193 | fs_sub* pSubFS; 194 | fs_sub_path subPath; 195 | 196 | pSubFS = (fs_sub*)fs_get_backend_data(pFS); 197 | FS_SUB_ASSERT(pSubFS != NULL); 198 | 199 | result = fs_sub_path_init(pFS, pFilePath, FS_NULL_TERMINATED, &subPath); 200 | if (result != FS_SUCCESS) { 201 | return result; 202 | } 203 | 204 | result = fs_remove(pSubFS->pOwnerFS, subPath.pFullPath); 205 | fs_sub_path_uninit(&subPath); 206 | 207 | return result; 208 | } 209 | 210 | static fs_result fs_rename_sub(fs* pFS, const char* pOldName, const char* pNewName) 211 | { 212 | fs_result result; 213 | fs_sub* pSubFS; 214 | fs_sub_path subPathOld; 215 | fs_sub_path subPathNew; 216 | 217 | pSubFS = (fs_sub*)fs_get_backend_data(pFS); 218 | FS_SUB_ASSERT(pSubFS != NULL); 219 | 220 | result = fs_sub_path_init(pFS, pOldName, FS_NULL_TERMINATED, &subPathOld); 221 | if (result != FS_SUCCESS) { 222 | return result; 223 | } 224 | 225 | result = fs_sub_path_init(pFS, pNewName, FS_NULL_TERMINATED, &subPathNew); 226 | if (result != FS_SUCCESS) { 227 | fs_sub_path_uninit(&subPathOld); 228 | return result; 229 | } 230 | 231 | result = fs_rename(pSubFS->pOwnerFS, subPathOld.pFullPath, subPathNew.pFullPath); 232 | 233 | fs_sub_path_uninit(&subPathOld); 234 | fs_sub_path_uninit(&subPathNew); 235 | 236 | return result; 237 | } 238 | 239 | static fs_result fs_mkdir_sub(fs* pFS, const char* pPath) 240 | { 241 | fs_result result; 242 | fs_sub* pSubFS; 243 | fs_sub_path subPath; 244 | 245 | pSubFS = (fs_sub*)fs_get_backend_data(pFS); 246 | FS_SUB_ASSERT(pSubFS != NULL); 247 | 248 | result = fs_sub_path_init(pFS, pPath, FS_NULL_TERMINATED, &subPath); 249 | if (result != FS_SUCCESS) { 250 | return result; 251 | } 252 | 253 | result = fs_mkdir(pSubFS->pOwnerFS, subPath.pFullPath, FS_IGNORE_MOUNTS); 254 | fs_sub_path_uninit(&subPath); 255 | 256 | return result; 257 | } 258 | 259 | static fs_result fs_info_sub(fs* pFS, const char* pPath, int openMode, fs_file_info* pInfo) 260 | { 261 | fs_result result; 262 | fs_sub* pSubFS; 263 | fs_sub_path subPath; 264 | 265 | pSubFS = (fs_sub*)fs_get_backend_data(pFS); 266 | FS_SUB_ASSERT(pSubFS != NULL); 267 | 268 | result = fs_sub_path_init(pFS, pPath, FS_NULL_TERMINATED, &subPath); 269 | if (result != FS_SUCCESS) { 270 | return result; 271 | } 272 | 273 | result = fs_info(pSubFS->pOwnerFS, subPath.pFullPath, openMode, pInfo); 274 | fs_sub_path_uninit(&subPath); 275 | 276 | return result; 277 | } 278 | 279 | static size_t fs_file_alloc_size_sub(fs* pFS) 280 | { 281 | FS_SUB_UNUSED(pFS); 282 | return sizeof(fs_file_sub); 283 | } 284 | 285 | static fs_result fs_file_open_sub(fs* pFS, fs_stream* pStream, const char* pFilePath, int openMode, fs_file* pFile) 286 | { 287 | fs_result result; 288 | fs_sub_path subPath; 289 | fs_sub* pSubFS; 290 | fs_file_sub* pSubFSFile; 291 | 292 | FS_SUB_UNUSED(pStream); 293 | 294 | result = fs_sub_path_init(pFS, pFilePath, FS_NULL_TERMINATED, &subPath); 295 | if (result != FS_SUCCESS) { 296 | return result; 297 | } 298 | 299 | pSubFS = (fs_sub*)fs_get_backend_data(pFS); 300 | FS_SUB_ASSERT(pSubFS != NULL); 301 | 302 | pSubFSFile = (fs_file_sub*)fs_file_get_backend_data(pFile); 303 | FS_SUB_ASSERT(pSubFSFile != NULL); 304 | 305 | result = fs_file_open(pSubFS->pOwnerFS, subPath.pFullPath, openMode, &pSubFSFile->pActualFile); 306 | fs_sub_path_uninit(&subPath); 307 | 308 | return result; 309 | } 310 | 311 | static fs_result fs_file_open_handle_sub(fs* pFS, void* hBackendFile, fs_file* pFile) 312 | { 313 | fs_sub* pSubFS; 314 | fs_file_sub* pSubFSFile; 315 | 316 | pSubFS = (fs_sub*)fs_get_backend_data(pFS); 317 | FS_SUB_ASSERT(pSubFS != NULL); 318 | 319 | pSubFSFile = (fs_file_sub*)fs_file_get_backend_data(pFile); 320 | FS_SUB_ASSERT(pSubFSFile != NULL); 321 | 322 | return fs_file_open_from_handle(pSubFS->pOwnerFS, hBackendFile, &pSubFSFile->pActualFile); 323 | } 324 | 325 | static void fs_file_close_sub(fs_file* pFile) 326 | { 327 | fs_file_sub* pSubFSFile = (fs_file_sub*)fs_file_get_backend_data(pFile); 328 | FS_SUB_ASSERT(pSubFSFile != NULL); 329 | 330 | fs_file_close(pSubFSFile->pActualFile); 331 | } 332 | 333 | static fs_result fs_file_read_sub(fs_file* pFile, void* pDst, size_t bytesToRead, size_t* pBytesRead) 334 | { 335 | fs_file_sub* pSubFSFile = (fs_file_sub*)fs_file_get_backend_data(pFile); 336 | FS_SUB_ASSERT(pSubFSFile != NULL); 337 | 338 | return fs_file_read(pSubFSFile->pActualFile, pDst, bytesToRead, pBytesRead); 339 | } 340 | 341 | static fs_result fs_file_write_sub(fs_file* pFile, const void* pSrc, size_t bytesToWrite, size_t* pBytesWritten) 342 | { 343 | fs_file_sub* pSubFSFile = (fs_file_sub*)fs_file_get_backend_data(pFile); 344 | FS_SUB_ASSERT(pSubFSFile != NULL); 345 | 346 | return fs_file_write(pSubFSFile->pActualFile, pSrc, bytesToWrite, pBytesWritten); 347 | } 348 | 349 | static fs_result fs_file_seek_sub(fs_file* pFile, fs_int64 offset, fs_seek_origin origin) 350 | { 351 | fs_file_sub* pSubFSFile = (fs_file_sub*)fs_file_get_backend_data(pFile); 352 | FS_SUB_ASSERT(pSubFSFile != NULL); 353 | 354 | return fs_file_seek(pSubFSFile->pActualFile, offset, origin); 355 | } 356 | 357 | static fs_result fs_file_tell_sub(fs_file* pFile, fs_int64* pCursor) 358 | { 359 | fs_file_sub* pSubFSFile = (fs_file_sub*)fs_file_get_backend_data(pFile); 360 | FS_SUB_ASSERT(pSubFSFile != NULL); 361 | 362 | return fs_file_tell(pSubFSFile->pActualFile, pCursor); 363 | } 364 | 365 | static fs_result fs_file_flush_sub(fs_file* pFile) 366 | { 367 | fs_file_sub* pSubFSFile = (fs_file_sub*)fs_file_get_backend_data(pFile); 368 | FS_SUB_ASSERT(pSubFSFile != NULL); 369 | 370 | return fs_file_flush(pSubFSFile->pActualFile); 371 | } 372 | 373 | static fs_result fs_file_info_sub(fs_file* pFile, fs_file_info* pInfo) 374 | { 375 | fs_file_sub* pSubFSFile = (fs_file_sub*)fs_file_get_backend_data(pFile); 376 | FS_SUB_ASSERT(pSubFSFile != NULL); 377 | 378 | return fs_file_get_info(pSubFSFile->pActualFile, pInfo); 379 | } 380 | 381 | static fs_result fs_file_duplicate_sub(fs_file* pFile, fs_file* pDuplicatedFile) 382 | { 383 | fs_file_sub* pSubFSFile; 384 | fs_file_sub* pSubFSFileDuplicated; 385 | 386 | pSubFSFile = (fs_file_sub*)fs_file_get_backend_data(pFile); 387 | FS_SUB_ASSERT(pSubFSFile != NULL); 388 | 389 | pSubFSFileDuplicated = (fs_file_sub*)fs_file_get_backend_data(pDuplicatedFile); 390 | FS_SUB_ASSERT(pSubFSFileDuplicated != NULL); 391 | 392 | return fs_file_duplicate(pSubFSFile->pActualFile, &pSubFSFileDuplicated->pActualFile); 393 | } 394 | 395 | static fs_iterator* fs_first_sub(fs* pFS, const char* pDirectoryPath, size_t directoryPathLen) 396 | { 397 | fs_result result; 398 | fs_sub* pSubFS; 399 | fs_sub_path subPath; 400 | fs_iterator* pIterator; 401 | 402 | pSubFS = (fs_sub*)fs_get_backend_data(pFS); 403 | FS_SUB_ASSERT(pSubFS != NULL); 404 | 405 | result = fs_sub_path_init(pFS, pDirectoryPath, directoryPathLen, &subPath); 406 | if (result != FS_SUCCESS) { 407 | return NULL; 408 | } 409 | 410 | pIterator = fs_first(pSubFS->pOwnerFS, subPath.pFullPath, subPath.fullPathLen); 411 | fs_sub_path_uninit(&subPath); 412 | 413 | return pIterator; 414 | } 415 | 416 | static fs_iterator* fs_next_sub(fs_iterator* pIterator) 417 | { 418 | return fs_next(pIterator); 419 | } 420 | 421 | static void fs_free_iterator_sub(fs_iterator* pIterator) 422 | { 423 | fs_free_iterator(pIterator); 424 | } 425 | 426 | fs_backend fs_sub_backend = 427 | { 428 | fs_alloc_size_sub, 429 | fs_init_sub, 430 | fs_uninit_sub, 431 | fs_ioctl_sub, 432 | fs_remove_sub, 433 | fs_rename_sub, 434 | fs_mkdir_sub, 435 | fs_info_sub, 436 | fs_file_alloc_size_sub, 437 | fs_file_open_sub, 438 | fs_file_open_handle_sub, 439 | fs_file_close_sub, 440 | fs_file_read_sub, 441 | fs_file_write_sub, 442 | fs_file_seek_sub, 443 | fs_file_tell_sub, 444 | fs_file_flush_sub, 445 | fs_file_info_sub, 446 | fs_file_duplicate_sub, 447 | fs_first_sub, 448 | fs_next_sub, 449 | fs_free_iterator_sub 450 | }; 451 | const fs_backend* FS_SUB = &fs_sub_backend; 452 | /* END fs_sub.c */ 453 | 454 | #endif /* fs_sub_c */ 455 | -------------------------------------------------------------------------------- /extras/backends/sub/fs_sub.h: -------------------------------------------------------------------------------- 1 | /* 2 | This backend can be used to create a filesystem where the root directory of the new fs object is 3 | a subdirectory of another fs object. Trying to access anything outside of this root directory 4 | will result in an error. 5 | 6 | Everything is done in terms of the owner FS object, meaning the owner must be kept alive for the 7 | life of the sub object. It also means it will inherit the owner's registered archive types. 8 | 9 | The way this works is very simple. Whenever you try to open a file, the path is checked to see if 10 | it's attempting to access anything outside of the root directory. If it is, an error is returned. 11 | Otherwise, the root directory is prepended to the path and the operation is passed on to the owner 12 | FS object. 13 | 14 | To use this backend, you need to create a fs_sub_config object and fill in the pOwnerFS and 15 | pRootDir fields. Then pass this object into `fs_init()`: 16 | 17 | fs_sub_config subConfig; 18 | subConfig.pOwnerFS = pOwnerFS; 19 | subConfig.pRootDir = "subdir"; 20 | 21 | fs_config fsConfig = fs_config_init(FS_SUB, &subConfig, NULL); 22 | 23 | fs* pSubFS; 24 | fs_init(&fsConfig, &pSubFS); 25 | 26 | You must ensure you destroy your sub object before destroying the owner FS object. 27 | */ 28 | #ifndef fs_sub_h 29 | #define fs_sub_h 30 | 31 | #if defined(__cplusplus) 32 | extern "C" { 33 | #endif 34 | 35 | /* BEG fs_sub.h */ 36 | typedef struct fs_sub_config 37 | { 38 | fs* pOwnerFS; 39 | const char* pRootDir; 40 | } fs_sub_config; 41 | 42 | extern const fs_backend* FS_SUB; 43 | /* END fs_sub.h */ 44 | 45 | #if defined(__cplusplus) 46 | } 47 | #endif 48 | #endif /* fs_sub_h */ 49 | -------------------------------------------------------------------------------- /extras/backends/zip/fs_zip.h: -------------------------------------------------------------------------------- 1 | /* 2 | Zip file support. 3 | 4 | This only supports STORE and DEFLATE. It does not support DEFLATE64. 5 | 6 | To use this, you'll first need to a fs_stream containing a Zip archive file. You can get this 7 | easily from a fs object. 8 | 9 | fs_file* pZipArchiveFile; 10 | fs_file_open(pFS, "archive.zip", FS_READ, &pZipArchiveFile); // Assumes pFS was initialized earlier. 11 | 12 | ... 13 | 14 | fs* pZip; 15 | fs_init(FS_ZIP, NULL, fs_file_get_stream(pZipArchiveFile), NULL, &pZip); 16 | 17 | ... 18 | 19 | fs_file* pFileInsideZip; 20 | fs_file_open(pZip, "file.txt", FS_READ, &pFileInsideZip); 21 | 22 | ... now just read from pFileInsideZip like any other file ... 23 | 24 | A Zip archive is its own file system and is therefore implemented as an fs backend. In order to 25 | actually use the backend, it needs to have access to the raw data of the entire Zip file. This is 26 | supplied via a fs_stream object. The fs_file object is a stream and can be used for this purpose. 27 | The code above just opens the Zip file from an earlier created fs object. 28 | 29 | Once you have the fs_stream object for the Zip file, you can initialize a fs object, telling it to 30 | use the Zip backend which you do by passing in FS_ZIP, which is declared in this file. If all goes 31 | well, you'll get a pointer to a new fs object representing the Zip archive and you can use it to 32 | open files from within it just like any other file. 33 | 34 | You can pass in NULL for the backend config in fs_init(). 35 | */ 36 | #ifndef fs_zip_h 37 | #define fs_zip_h 38 | 39 | #if defined(__cplusplus) 40 | extern "C" { 41 | #endif 42 | 43 | /* BEG fs_zip.h */ 44 | extern const fs_backend* FS_ZIP; 45 | /* END fs_zip.h */ 46 | 47 | #if defined(__cplusplus) 48 | } 49 | #endif 50 | #endif /* fs_zip_h */ 51 | -------------------------------------------------------------------------------- /fs.h: -------------------------------------------------------------------------------- 1 | /* 2 | File system library. Choice of public domain or MIT-0. See license statements at the end of this file. 3 | fs - v1.0.0 - Release Date TBD 4 | 5 | David Reid - mackron@gmail.com 6 | 7 | GitHub: https://github.com/mackron/fs 8 | */ 9 | 10 | /* 11 | 1. Introduction 12 | =============== 13 | This library is used to abstract access to the regular file system and archives such as ZIP files. 14 | 15 | 1.1. Basic Usage 16 | ---------------- 17 | The main object in the library is the `fs` object. Below is the most basic way to initialize a `fs` 18 | object: 19 | 20 | ```c 21 | fs_result result; 22 | fs* pFS; 23 | 24 | result = fs_init(NULL, &pFS); 25 | if (result != FS_SUCCESS) { 26 | // Failed to initialize. 27 | } 28 | ``` 29 | 30 | The above code will initialize a `fs` object representing the system's regular file system. It uses 31 | stdio under the hood. Once this is set up you can load files: 32 | 33 | ```c 34 | fs_file* pFile; 35 | 36 | result = fs_file_open(pFS, "file.txt", FS_READ, &pFile); 37 | if (result != FS_SUCCESS) { 38 | // Failed to open file. 39 | } 40 | ``` 41 | 42 | If you don't need any of the advanced features of the library, you can just pass in NULL for the 43 | `fs` object which will just use the native file system like normal: 44 | 45 | ```c 46 | fs_file_open(NULL, "file.txt", FS_READ, &pFile); 47 | ``` 48 | 49 | From here on out, examples will use an `fs` object for the sake of consistency, but all basic IO 50 | APIs that do not use things like mounting and archive registration will work with NULL. 51 | 52 | To close a file, use `fs_file_close()`: 53 | 54 | ```c 55 | fs_file_close(pFile); 56 | ``` 57 | 58 | Reading content from the file is very standard: 59 | 60 | ```c 61 | size_t bytesRead; 62 | 63 | result = fs_file_read(pFile, pBuffer, bytesToRead, &bytesRead); 64 | if (result != FS_SUCCESS) { 65 | // Failed to read file. You can use FS_AT_END to check if reading failed due to being at EOF. 66 | } 67 | ``` 68 | 69 | In the code above, the number of bytes actually read is output to a variable. You can use this to 70 | determine if you've reached the end of the file. You can also check if the result is FS_AT_END. You 71 | can pass in null for the last parameter of `fs_file_read()` in which an error will be returned if 72 | the exact number of bytes requested could not be read. 73 | 74 | Writing works the same way as reading: 75 | 76 | ```c 77 | fs_file* pFile; 78 | 79 | result = fs_file_open(pFS, "file.txt", FS_WRITE, &pFile); 80 | if (result != FS_SUCCESS) { 81 | // Failed to open file. 82 | } 83 | 84 | result = fs_file_write(pFile, pBuffer, bytesToWrite, &bytesWritten); 85 | ``` 86 | 87 | The `FS_WRITE` option will default to `FS_TRUNCATE`. You can also use `FS_APPEND` and 88 | `FS_OVERWRITE`: 89 | 90 | ```c 91 | 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. 92 | ``` 93 | 94 | Files can be opened for both reading and writing by simply combining the two: 95 | 96 | ```c 97 | fs_file_open(pFS, "file.txt", FS_READ | FS_WRITE, &pFile); 98 | ``` 99 | 100 | Seeking and telling is very standard as well: 101 | 102 | ```c 103 | fs_file_seek(pFile, 0, FS_SEEK_END); 104 | 105 | fs_int64 cursorPos; 106 | fs_file_tell(pFile, &cursorPos); 107 | ``` 108 | 109 | Retrieving information about a file is done with `fs_file_info()`: 110 | 111 | ```c 112 | fs_file_info info; 113 | fs_file_info(pFile, &info); 114 | ``` 115 | 116 | If you want to get information about a file without opening it, you can use `fs_info()`: 117 | 118 | ```c 119 | fs_file_info info; 120 | fs_info(pFS, "file.txt", &info); 121 | ``` 122 | 123 | A file handle can be duplicated with `fs_file_duplicate()`: 124 | 125 | ```c 126 | fs_file* pFileDup; 127 | fs_file_duplicate(pFile, &pFileDup); 128 | ``` 129 | 130 | Note that this will only duplicate the file handle. It does not make a copy of the file on the file 131 | system itself. The duplicated file handle will be entirely independent of the original handle. 132 | 133 | To delete a file, use `fs_remove()`: 134 | 135 | ```c 136 | fs_remove(pFS, "file.txt"); 137 | ``` 138 | 139 | Files can be renamed and moved with `fs_rename()`: 140 | 141 | ```c 142 | fs_rename(pFS, "file.txt", "new-file.txt"); 143 | ``` 144 | 145 | To create a directory, use `fs_mkdir()`: 146 | 147 | ```c 148 | fs_mkdir(pFS, "new-directory", 0); 149 | ``` 150 | 151 | By default, `fs_mkdir()` will create the directory hierarchy for you. If you want to disable this 152 | so it fails if the directory hierarchy doesn't exist, you can use `FS_NO_CREATE_DIRS`: 153 | 154 | ```c 155 | fs_mkdir(pFS, "new-directory", FS_NO_CREATE_DIRS); 156 | ``` 157 | 158 | 1.2. Archives 159 | ------------- 160 | To enable support for archives, you need an `fs` object, and it must be initialized with a config: 161 | 162 | ```c 163 | #include "extras/backends/zip/fs_zip.h" // <-- This is where FS_ZIP is declared. 164 | #include "extras/backends/pak/fs_pak.h" // <-- This is where FS_PAK is declared. 165 | 166 | ... 167 | 168 | fs_archive_type pArchiveTypes[] = 169 | { 170 | {FS_ZIP, "zip"}, 171 | {FS_PAK, "pak"} 172 | }; 173 | 174 | fs_config fsConfig = fs_config_init(FS_STDIO, NULL, NULL); 175 | fsConfig.pArchiveTypes = pArchiveTypes; 176 | fsConfig.archiveTypeCount = sizeof(pArchiveTypes) / sizeof(pArchiveTypes[0]); 177 | 178 | fs* pFS; 179 | fs_init(&fsConfig, &pFS); 180 | ``` 181 | 182 | In the code above we are registering support for ZIP archives (`FS_ZIP`) and Quake PAK archives 183 | (`FS_PAK`). Whenever a file with a "zip" or "pak" extension is found, the library will be able to 184 | access the archive. The library will determine whether or not a file is an archive based on it's 185 | extension. You can use whatever extension you would like for a backend, and you can associated 186 | multiple extensions to the same backend. You can also associate different backends to the same 187 | extension, in which case the library will use the first one that works. If the extension of a file 188 | does not match with one of the registered archive types it'll assume it's not an archive and will 189 | skip it. Below is an example of one way you can read from an archive: 190 | 191 | ```c 192 | result = fs_file_open(pFS, "archive.zip/file-inside-archive.txt", FS_READ, &pFile); 193 | if (result != FS_SUCCESS) { 194 | // Failed to open file. 195 | } 196 | ``` 197 | 198 | In the example above, we've explicitly specified the name of the archive in the file path. The 199 | library also supports the ability to handle archives transparently, meaning you don't need to 200 | explicitly specify the archive. The code below will also work: 201 | 202 | ```c 203 | fs_file_open(pFS, "file-inside-archive.txt", FS_READ, &pFile); 204 | ``` 205 | 206 | Transparently handling archives like this has overhead because the library needs to scan the file 207 | system and check every archive it finds. To avoid this, you can explicitly disable this feature: 208 | 209 | ```c 210 | fs_file_open(pFS, "archive.zip/file-inside-archive.txt", FS_READ | FS_VERBOSE, &pFile); 211 | ``` 212 | 213 | In the code above, the `FS_VERBOSE` flag will require you to pass in a verbose file path, meaning 214 | you need to explicitly specify the archive in the path. You can take this one step further by 215 | disabling access to archives in this manner altogether via `FS_OPAQUE`: 216 | 217 | ```c 218 | result = fs_file_open(pFS, "archive.zip/file-inside-archive.txt", FS_READ | FS_OPAQUE, &pFile); 219 | if (result != FS_SUCCESS) { 220 | // This example will always fail. 221 | } 222 | ``` 223 | 224 | In the example above, opening the file will fail because `FS_OPAQUE` is telling the library to 225 | treat archives as if they're totally opaque which means the files within cannot be accessed. 226 | 227 | Up to this point the handling of archives has been done automatically via `fs_file_open()`, however 228 | the library allows you to manage archives manually. To do this you just initialize a `fs` object to 229 | represent the archive: 230 | 231 | ```c 232 | // Open the archive file itself first. 233 | fs_file* pArchiveFile; 234 | 235 | result = fs_file_open(pFS, "archive.zip", FS_READ, &pArchiveFile); 236 | if (result != FS_SUCCESS) { 237 | // Failed to open archive file. 238 | } 239 | 240 | 241 | // Once we have the archive file we can create the `fs` object representing the archive. 242 | fs* pArchive; 243 | fs_config archiveConfig; 244 | 245 | archiveConfig = fs_config_init(FS_ZIP, NULL, fs_file_get_stream(pArchiveFile)); 246 | 247 | result = fs_init(&archiveConfig, &pArchive); 248 | if (result != FS_SUCCESS) { 249 | // Failed to initialize archive. 250 | } 251 | 252 | ... 253 | 254 | // During teardown, make sure the archive `fs` object is uninitialized before the stream. 255 | fs_uninit(pArchive); 256 | fs_file_close(pArchiveFile); 257 | ``` 258 | 259 | To initialize an `fs` object for an archive you need a stream to provide the raw archive data to 260 | the backend. Conveniently, the `fs_file` object itself is a stream. In the example above we're just 261 | opening a file from a different `fs` object (usually one representing the default file system) to 262 | gain access to a stream. The stream does not need to be a `fs_file`. You can implement your own 263 | `fs_stream` object, and a `fs_memory_stream` is included as stock with the library for when you 264 | want to store the contents of an archive in-memory. Once you have the `fs` object for the archive 265 | you can use it just like any other: 266 | 267 | ```c 268 | result = fs_file_open(pArchive, "file-inside-archive.txt", FS_READ, &pFile); 269 | if (result != FS_SUCCESS) { 270 | // Failed to open file. 271 | } 272 | ``` 273 | 274 | In addition to the above, you can use `fs_open_archive()` to open an archive from a file: 275 | 276 | ```c 277 | fs* pArchive; 278 | fs_open_archive(pFS, "archive.zip", FS_READ, &pArchive); 279 | 280 | ... 281 | 282 | // When tearing down, do *not* use `fs_uninit()`. Use `fs_close_archive()` instead. 283 | fs_close_archive(pArchive); 284 | ``` 285 | 286 | Note that you need to use `fs_close_archive()` when opening an archive like this. The reason for 287 | this is that there's some internal reference counting and memory management happening under the 288 | hood. You should only call `fs_close_archive()` if `fs_open_archive()` succeeds. 289 | 290 | When opening an archive with `fs_open_archive()`, it will inherit the archive types from the parent 291 | `fs` object and will therefore support archives within archives. Use caution when doing this 292 | because if both archives are compressed you will get a big performance hit. Only the inner-most 293 | archive should be compressed. 294 | 295 | 296 | 1.3. Mounting 297 | ------------- 298 | There is no ability to change the working directory in this library. Instead you can mount a 299 | physical directory to virtual path, similar in concept to Unix operating systems. The difference, 300 | however, is that you can mount multiple directories to the same mount point in which case a 301 | prioritization system will be used. There are separate mount points for reading and writing. Below 302 | is an example of mounting for reading: 303 | 304 | ```c 305 | fs_mount(pFS, "/some/actual/path", NULL, FS_READ); 306 | ``` 307 | 308 | Do unmount, you need to specify the actual path, not the virtual path: 309 | 310 | ```c 311 | fs_unmount(pFS, "/some/actual/path", FS_READ); 312 | ``` 313 | 314 | In the example above, `NULL` is equivalent to an empty path. If, for example, you have a file with 315 | the path "/some/actual/path/file.txt", you can open it like the following: 316 | 317 | ```c 318 | fs_file_open(pFS, "file.txt", FS_READ, &pFile); 319 | ``` 320 | 321 | You don't need to specify the "/some/actual/path" part because it's handled by the mount. If you 322 | specify a virtual path, you can do something like the following: 323 | 324 | ```c 325 | fs_mount(pFS, "/some/actual/path", "assets", FS_READ); 326 | ``` 327 | 328 | In this case, loading files that are physically located in "/some/actual/path" would need to be 329 | prefixed with "assets": 330 | 331 | ```c 332 | fs_file_open(pFS, "assets/file.txt", FS_READ, &pFile); 333 | ``` 334 | 335 | You can mount multiple paths to the same virtual path in which case a prioritization system will be 336 | used: 337 | 338 | ```c 339 | fs_mount(pFS, "/usr/share/mygame/gamedata/base", "gamedata", FS_READ); // Base game. Lowest priority. 340 | fs_mount(pFS, "/home/user/.local/share/mygame/gamedata/mod1", "gamedata", FS_READ); // Mod #1. Middle priority. 341 | fs_mount(pFS, "/home/user/.local/share/mygame/gamedata/mod2", "gamedata", FS_READ); // Mod #2. Highest priority. 342 | ``` 343 | 344 | The example above shows a basic system for setting up some kind of modding support in a game. In 345 | this case, attempting to load a file from the "gamedata" mount point will first check the "mod2" 346 | directory, and if it cannot be opened from there, it will check "mod1", and finally it'll fall back 347 | to the base game data. 348 | 349 | Internally there are a separate set of mounts for reading and writing. To set up a mount point for 350 | opening files in write mode, you need to specify the `FS_WRITE` option: 351 | 352 | ```c 353 | fs_mount(pFS, "/home/user/.config/mygame", "config", FS_WRITE); 354 | fs_mount(pFS, "/home/user/.local/share/mygame/saves", "saves", FS_WRITE); 355 | ``` 356 | 357 | To open a file for writing, you need only prefix the path with the mount's virtual path, exactly 358 | like read mode: 359 | 360 | ```c 361 | fs_file_open(pFS, "config/game.cfg", FS_WRITE, &pFile); // Prefixed with "config", so will use the "config" mount point. 362 | fs_file_open(pFS, "saves/save0.sav", FS_WRITE, &pFile); // Prefixed with "saves", so will use the "saves" mount point. 363 | ``` 364 | 365 | If you want to mount a directory for reading and writing, you can use both `FS_READ` and 366 | `FS_WRITE` together: 367 | 368 | ```c 369 | fs_mount(pFS, "/home/user/.config/mygame", "config", FS_READ | FS_WRITE); 370 | ``` 371 | 372 | You can set up read and write mount points to the same virtual path: 373 | 374 | ```c 375 | fs_mount(pFS, "/usr/share/mygame/config", "config", FS_READ); 376 | fs_mount(pFS, "/home/user/.local/share/mygame/config", "config", FS_READ | FS_WRITE); 377 | ``` 378 | 379 | When opening a file for reading, it'll first try searching the second mount point, and if it's not 380 | found will fall back to the first. When opening in write mode, it will only ever use the second 381 | mount point as the output directory because that's the only one set up with `FS_WRITE`. With this 382 | setup, the first mount point is essentially protected from modification. 383 | 384 | When mounting a directory for writing, the library will create the directory hierarchy for you. If 385 | you want to disable this functionality, you can use the `FS_NO_CREATE_DIRS` flag: 386 | 387 | ```c 388 | fs_mount(pFS, "/home/user/.config/mygame", "config", FS_WRITE | FS_NO_CREATE_DIRS); 389 | ``` 390 | 391 | 392 | By default, you can move outside the mount point with ".." segments. If you want to disable this 393 | functionality, you can use the `FS_NO_ABOVE_ROOT_NAVIGATION` flag when opening the file: 394 | 395 | ```c 396 | fs_file_open(pFS, "../file.txt", FS_READ | FS_NO_ABOVE_ROOT_NAVIGATION, &pFile); 397 | ``` 398 | 399 | In addition, any mount points that start with a "/" will be considered absolute and will not allow 400 | any above-root navigation: 401 | 402 | ```c 403 | fs_mount(pFS, "/usr/share/mygame/gamedata/base", "/gamedata", FS_READ); 404 | ``` 405 | 406 | In the example above, the "/gamedata" mount point starts with a "/", so it will not allow any 407 | above-root navigation which means you cannot navigate above "/usr/share/mygame/gamedata/base". When 408 | opening a file with this kind of mount point, you would need to specify the leading slash: 409 | 410 | ```c 411 | fs_file_open(pFS, "/gamedata/file.txt", FS_READ, &pFile); // Note how the path starts with "/". 412 | ``` 413 | 414 | 415 | You can also mount an archives to a virtual path: 416 | 417 | ```c 418 | fs_mount(pFS, "/usr/share/mygame/gamedata.zip", "gamedata", FS_READ); 419 | ``` 420 | 421 | In order to do this, the `fs` object must have been configured with support for the given archive 422 | type. Note that writing directly into an archive is not supported by this API. To write into an 423 | archive, the backend itself must support writing, and you will need to manually initialize a `fs` 424 | object for the archive an write into it directly. 425 | 426 | 427 | The examples above have been hard coding paths, but you can use `fs_mount_sysdir()` to mount a 428 | system directory to a virtual path. This is just a convenience helper function, and you need not 429 | use it if you'd rather deal with system directories yourself: 430 | 431 | ```c 432 | fs_mount_sysdir(pFS, FS_SYSDIR_CONFIG, "myapp", "/config", FS_READ | FS_WRITE); 433 | ``` 434 | 435 | This function requires that you specify a sub-directory of the system directory to mount. The reason 436 | for this is to encourage the application to use good practice to avoid cluttering the file system. 437 | 438 | Use `fs_unmount_sysdir()` to unmount a system directory. When using this you must specify the 439 | sub-directory you used when mounting it: 440 | 441 | ```c 442 | fs_unmount_sysdir(pFS, FS_SYSDIR_CONFIG, "myapp", FS_READ | FS_WRITE); 443 | ``` 444 | 445 | 446 | Mounting a `fs` object to a virtual path is also supported. 447 | 448 | ```c 449 | fs* pSomeOtherFS; // <-- This would have been initialized earlier. 450 | 451 | fs_mount_fs(pFS, pSomeOtherFS, "assets.zip", FS_READ); 452 | 453 | ... 454 | 455 | fs_unmount_fs(pFS, pSomeOtherFS, FS_READ); 456 | ``` 457 | 458 | 459 | If the file cannot be opened from any mounts it will attempt to open the file from the backend's 460 | default search path. Mounts always take priority. When opening in transparent mode with 461 | `FS_TRANSPARENT` (default), it will first try opening the file as if it were not in an archive. If 462 | that fails, it will look inside archives. 463 | 464 | When opening a file, if you pass in NULL for the `pFS` parameter it will open the file like normal 465 | using the standard file system. That is, it'll work exactly as if you were using stdio `fopen()`, 466 | and you will not have access to mount points. Keep in mind that there is no notion of a "current 467 | directory" in this library so you'll be stuck with the initial working directory. 468 | 469 | 470 | 471 | 1.4. Enumeration 472 | ---------------- 473 | You can enumerate over the contents of a directory like the following: 474 | 475 | ```c 476 | for (fs_iterator* pIterator = fs_first(pFS, "directory/to/enumerate", FS_NULL_TERMINATED, 0); pIterator != NULL; pIterator = fs_next(pIterator)) { 477 | printf("Name: %s\n", pIterator->pName); 478 | printf("Size: %llu\n", pIterator->info.size); 479 | } 480 | ``` 481 | 482 | If you want to terminate iteration early, use `fs_free_iterator()` to free the iterator object. 483 | `fs_next()` will free the iterator for you when it reaches the end. 484 | 485 | Like when opening a file, you can specify `FS_OPAQUE`, `FS_VERBOSE` or `FS_TRANSPARENT` (default) 486 | in `fs_first()` to control which files are enumerated. Enumerated files will be consistent with 487 | what would be opened when using the same option with `fs_file_open()`. 488 | 489 | Internally, `fs_first()` will gather all of the enumerated files. This means you should expect 490 | `fs_first()` to be slow compared to `fs_next()`. 491 | 492 | Enumerated entries will be sorted by name in terms of `strcmp()`. 493 | 494 | Enumeration is not recursive. If you want to enumerate recursively you can inspect the `directory` 495 | member of the `info` member in `fs_iterator`. 496 | 497 | 498 | 1.5. System Directories 499 | ----------------------- 500 | It can often be useful to know the exact paths of known standard system directories, such as the 501 | home directory. You can use the `fs_sysdir()` function for this: 502 | 503 | ```c 504 | char pPath[256]; 505 | size_t pathLen = fs_sysdir(FS_SYSDIR_HOME, pPath, sizeof(pPath)); 506 | if (pathLen > 0) { 507 | if (pathLen < sizeof(pPath)) { 508 | // Success! 509 | } else { 510 | // The buffer was too small. Expand the buffer to at least `pathLen + 1` and try again. 511 | } 512 | } else { 513 | // An error occurred. 514 | } 515 | 516 | ``` 517 | 518 | `fs_sysdir()` will return the length of the path written to `pPath`, or 0 if an error occurred. If 519 | the buffer is too small, it will return the required size, not including the null terminator. 520 | 521 | Recognized system directories include the following: 522 | 523 | - FS_SYSDIR_HOME 524 | - FS_SYSDIR_TEMP 525 | - FS_SYSDIR_CONFIG 526 | - FS_SYSDIR_DATA 527 | - FS_SYSDIR_CACHE 528 | 529 | 530 | 531 | 1.6. Temporary Files 532 | -------------------- 533 | You can create a temporary file or folder with `fs_mktmp()`. To create a temporary folder, use the 534 | `FS_MKTMP_DIR` option: 535 | 536 | ```c 537 | char pTmpPath[256]; 538 | fs_result result = fs_mktmp("prefix", pTmpPath, sizeof(pTmpPath), FS_MKTMP_DIR); 539 | if (result != FS_SUCCESS) { 540 | // Failed to create temporary file. 541 | } 542 | ``` 543 | 544 | Similarly, to create a temporary file, use the `FS_MKTMP_FILE` option: 545 | 546 | ```c 547 | char pTmpPath[256]; 548 | fs_result result = fs_mktmp("prefix", pTmpPath, sizeof(pTmpPath), FS_MKTMP_FILE); 549 | if (result != FS_SUCCESS) { 550 | // Failed to create temporary file. 551 | } 552 | ``` 553 | 554 | `fs_mktmp()` will create a temporary file or folder with a unique name based on the provided 555 | prefix and will return the full path to the created file or folder in `pTmpPath`. To open the 556 | temporary file, you can pass in the path to `fs_file_open()`, making sure to ignore mount points 557 | with `FS_IGNORE_MOUNTS`: 558 | 559 | ```c 560 | fs_file* pFile; 561 | result = fs_file_open(pFS, pTmpPath, FS_WRITE | FS_IGNORE_MOUNTS, &pFile); 562 | if (result != FS_SUCCESS) { 563 | // Failed to open temporary file. 564 | } 565 | ``` 566 | 567 | If you just want to create a temporary file and don't care about the name, you can use 568 | `fs_file_open()` with the `FS_TEMP` flag. In this case, the library will treat the file path 569 | as the prefix: 570 | 571 | ```c 572 | fs_file* pFile; 573 | result = fs_file_open(pFS, "prefix", FS_TEMP, &pFile); 574 | if (result != FS_SUCCESS) { 575 | // Failed to open temporary file. 576 | } 577 | ``` 578 | 579 | The use of temporary files is only valid with `fs` objects that make use of the standard file 580 | system, such as the stdio backend. 581 | 582 | The prefix can include subdirectories, such as "myapp/subdir". In this case the library will create 583 | the directory hierarchy for you, unless you pass in `FS_NO_CREATE_DIRS`. Note that not all 584 | platforms treat the name portion of the prefix the same. In particular, Windows will only use up to 585 | the first 3 characters of the name portion of the prefix. 586 | 587 | If you don't like the behavior of `fs_mktmp()`, you can consider using `fs_sysdir()` with 588 | `FS_SYSDIR_TEMP` and create the temporary file yourself. 589 | 590 | 591 | 592 | 2. Thread Safety 593 | ================ 594 | The following points apply regarding thread safety. 595 | 596 | - Opening files across multiple threads is safe. Backends are responsible for ensuring thread 597 | safety when opening files. 598 | 599 | - An individual `fs_file` object is not thread safe. If you want to use a specific `fs_file` 600 | object across multiple threads, you will need to synchronize access to it yourself. Using 601 | different `fs_file` objects across multiple threads is safe. 602 | 603 | - Mounting and unmounting is not thread safe. You must use your own synchronization if you 604 | want to do this across multiple threads. 605 | 606 | - Opening a file on one thread while simultaneously mounting or unmounting on another thread is 607 | not safe. Again, you must use your own synchronization if you need to do this. The recommended 608 | usage is to set up your mount points once during initialization before opening any files. 609 | 610 | 611 | 612 | 3. Backends 613 | =========== 614 | You can implement custom backends to support different file systems and archive formats. A stdio 615 | backend is the default backend and is built into the library. A backend implements the functions 616 | in the `fs_backend` structure. 617 | 618 | A ZIP backend is included in the "extras" folder of this library's repository. Refer to this for 619 | a complete example for how to implement a backend (not including write support, but I'm sure 620 | you'll figure it out!). A PAK backend is also included in the "extras" folder, and is simpler than 621 | the ZIP backend which might also be a good place to start. 622 | 623 | The backend abstraction is designed to relieve backends from having to worry about the 624 | implementation details of the main library. Backends should only concern themselves with their 625 | own local content and not worry about things like mount points, archives, etc. Those details will 626 | be handled at a higher level in the library. 627 | 628 | Instances of a `fs` object can be configured with backend-specific configuration data. This is 629 | passed to the backend as a void pointer to the necessary functions. This data will point to a 630 | backend-defined structure that the backend will know how to use. 631 | 632 | In order for the library to know how much memory to allocate for the `fs` object, the backend 633 | needs to implement the `alloc_size` function. This function should return the total size of the 634 | backend-specific data to associate with the `fs` object. Internally, this memory will be stored 635 | at the end of the `fs` object. The backend can access this data via `fs_get_backend_data()`: 636 | 637 | ```c 638 | typedef struct my_fs_data 639 | { 640 | int someData; 641 | } my_fs_data; 642 | 643 | ... 644 | 645 | my_fs_data* pBackendData = (my_fs_data*)fs_get_backend_data(pFS); 646 | assert(pBackendData != NULL); 647 | 648 | do_something(pBackendData->someData); 649 | ``` 650 | 651 | This pattern will be a central part of how backends are implemented. If you don't have any 652 | backend-specific data, you can just return 0 from `alloc_size()` and simply not reference the 653 | backend data pointer. 654 | 655 | The main library will allocate the `fs` object, including any additional space specified by the 656 | `alloc_size` function. Once this is done, it'll call the `init` function to initialize the backend. 657 | This function will take a pointer to the `fs` object, the backend-specific configuration data, and 658 | a stream object. The stream is used to provide the backend with the raw data of an archive, which 659 | will be required for archive backends like ZIP. If your backend requires this, you should check 660 | for if the stream is null, and if so, return an error. See section "4. Streams" for more details 661 | on how to use streams. You need not take a copy of the stream pointer for use outside of `init()`. 662 | Instead you can just use `fs_get_stream()` to get the stream object when you need it. You should 663 | not ever close or otherwise take ownership of the stream - that will be handled at a higher level. 664 | 665 | The `uninit` function is where you should do any cleanup. Do not close the stream here. 666 | 667 | The `ioctl` function is optional. You can use this to implement custom IO control commands. Return 668 | `FS_INVALID_COMMAND` if the command is not recognized. The format of the `pArg` parameter is 669 | command specific. If the backend does not need to implement this function, it can be left as `NULL` 670 | or return `FS_NOT_IMPLEMENTED`. 671 | 672 | The `remove` function is used to delete a file or directory. This is not recursive. If the path is 673 | a directory, the backend should return an error if it is not empty. Backends do not need to 674 | implement this function in which case they can leave the callback pointer as `NULL`, or have it 675 | return `FS_NOT_IMPLEMENTED`. 676 | 677 | The `rename` function is used to rename a file. This will act as a move if the source and 678 | destination are in different directories. If the destination already exists, it should be 679 | overwritten. This function is optional and can be left as `NULL` or return `FS_NOT_IMPLEMENTED`. 680 | 681 | The `mkdir` function is used to create a directory. This is not recursive. If the directory already 682 | exists, the backend should return `FS_SUCCESS`. This function is optional and can be left as `NULL` 683 | or return `FS_NOT_IMPLEMENTED`. 684 | 685 | The `info` function is used to get information about a file. If the backend does not have the 686 | notion of the last modified or access time, it can set those values to 0. Set `directory` to 1 (or 687 | FS_TRUE) if it's a directory. Likewise, set `symlink` to 1 if it's a symbolic link. It is important 688 | that this function return the info of the exact file that would be opened with `file_open()`. This 689 | function is mandatory. 690 | 691 | Like when initializing a `fs` object, the library needs to know how much backend-specific data to 692 | allocate for the `fs_file` object. This is done with the `file_alloc_size` function. This function 693 | is basically the same as `alloc_size` for the `fs` object, but for `fs_file`. If the backend does 694 | not need any additional data, it can return 0. The backend can access this data via 695 | `fs_file_get_backend_data()`. 696 | 697 | The `file_open` function is where the backend should open the file. If the `fs` object that owns 698 | the file was initialized with a stream, i.e. it's an archive, the stream will be non-null. You 699 | should store this pointer for later use in `file_read`, etc. Do *not* make a duplicate of the 700 | stream with `fs_stream_duplicate()`. Instead just take a copy of the pointer. The `openMode` 701 | parameter will be a combination of `FS_READ`, `FS_WRITE`, `FS_TRUNCATE`, `FS_APPEND` and 702 | `FS_OVERWRITE`. When opening in write mode (`FS_WRITE`), it will default to truncate mode. You 703 | should ignore the `FS_OPAQUE`, `FS_VERBOSE` and `FS_TRANSPARENT` flags. If the file does not exist, 704 | the backend should return `FS_DOES_NOT_EXIST`. If the file is a directory, it should return 705 | `FS_IS_DIRECTORY`. 706 | 707 | The file should be closed with `file_close`. This is where the backend should release any resources 708 | associated with the file. Do not uninitialize the stream here - it'll be cleaned up at a higher 709 | level. 710 | 711 | The `file_read` function is used to read data from the file. The backend should return `FS_AT_END` 712 | when the end of the file is reached, but only if the number of bytes read is 0. 713 | 714 | The `file_write` function is used to write data to the file. If the file is opened in append mode, 715 | the backend should seek to the end of the file before writing. This is optional and need only be 716 | specified if the backend supports writing. 717 | 718 | The `file_seek` function is used to seek the cursor. The backend should return `FS_BAD_SEEK` if the 719 | seek is out of bounds. 720 | 721 | The `file_tell` function is used to get the current cursor position. There is only one cursor, even 722 | when the file is opened in read and write mode. 723 | 724 | The `file_flush` function is used to flush any buffered data to the file. This is optional and can 725 | be left as `NULL` or return `FS_NOT_IMPLEMENTED`. 726 | 727 | The `file_info` function is used to get information about an opened file. It returns the same 728 | information as `info` but for an opened file. This is mandatory. 729 | 730 | The `file_duplicate` function is used to duplicate a file. The destination file will be a new file 731 | and already allocated. The backend need only copy the necessary backend-specific data to the new 732 | file. 733 | 734 | The `first`, `next` and `free_iterator` functions are used to enumerate the contents of a directory. 735 | If the directory is empty, or an error occurs, `fs_first` should return `NULL`. The `next` function 736 | should return `NULL` when there are no more entries. When `next` returns `NULL`, the backend needs 737 | to free the iterator object. The `free_iterator` function is used to free the iterator object 738 | explicitly. The backend is responsible for any memory management of the name string. A typical way 739 | to deal with this is to allocate the allocate additional space for the name immediately after the 740 | `fs_iterator` allocation. 741 | 742 | Backends are responsible for guaranteeing thread-safety of different files across different 743 | threads. This should typically be quite easy since most system backends, such as stdio, are already 744 | thread-safe, and archive backends are typically read-only which should make thread-safety trivial 745 | on that front as well. You need not worry about thread-safety of a single individual file handle. 746 | But when you have two different file handles, they must be able to be used on two different threads 747 | at the same time. 748 | 749 | 750 | 4. Streams 751 | ========== 752 | Streams are the data delivery mechanism for archive backends. You can implement custom streams, but 753 | this should be uncommon because `fs_file` itself is a stream, and a memory stream is included in 754 | the library called `fs_memory_stream`. Between these two the majority of use cases should be 755 | covered. 756 | 757 | A stream is initialized using a specialized initialization function depending on the stream type. 758 | For `fs_file`, simply opening the file is enough. For `fs_memory_stream`, you need to call 759 | `fs_memory_stream_init_readonly()` for a standard read-only stream, or 760 | `fs_memory_stream_init_write()` for a stream with write (and read) support. If you want to 761 | implement your own stream type you would need to implement a similar initialization function. 762 | 763 | Use `fs_stream_read()` and `fs_stream_write()` to read and write data from a stream. If the stream 764 | does not support reading or writing, the respective function will return `FS_NOT_IMPLEMENTED`. 765 | 766 | The cursor can be set and retrieved with `fs_stream_seek()` and `fs_stream_tell()`. There is only 767 | a single cursor which is shared between reading and writing. 768 | 769 | Streams can be duplicated. A duplicated stream is a fully independent stream. This functionality 770 | is used heavily internally by the library so if you build a custom stream you should support it 771 | if you can. Without duplication support, you will not be able to open files within archives. To 772 | duplicate a stream, use `fs_stream_duplicate()`. To delete a duplicated stream, use 773 | `fs_stream_delete_duplicate()`. Do not use implementation-specific uninitialization routines to 774 | uninitialize a duplicated stream - `fs_stream_delete_duplicate()` will deal with that for you. 775 | 776 | Streams are not thread safe. If you want to use a stream across multiple threads, you will need to 777 | synchronize access to it yourself. Using different stream objects across multiple threads is safe. 778 | A duplicated stream is entirely independent of the original stream and can be used on a different 779 | thread to the original stream. 780 | 781 | The `fs_stream` object is a base class. If you want to implement your own stream, you should make 782 | the first member of your stream object a `fs_stream` object. This will allow you to cast between 783 | `fs_stream*` and your custom stream type. 784 | 785 | See `fs_stream_vtable` for a list of functions that need to be implemented for a custom stream. If 786 | the stream does not support writing, the `write` callback can be left as `NULL` or return 787 | `FS_NOT_IMPLEMENTED`. 788 | 789 | See `fs_memory_stream` for an example of how to implement a custom stream. 790 | */ 791 | 792 | /* 793 | This library has been designed to be amalgamated into other libraries of mine. You will probably 794 | see some random tags and stuff in this file. These are just used for doing a dumb amalgamation. 795 | */ 796 | #ifndef fs_h 797 | #define fs_h 798 | 799 | #if defined(__cplusplus) 800 | extern "C" { 801 | #endif 802 | 803 | /* BEG fs_compiler_compat.h */ 804 | #include /* For size_t. */ 805 | #include /* For va_list. */ 806 | 807 | #if defined(SIZE_MAX) 808 | #define FS_SIZE_MAX SIZE_MAX 809 | #else 810 | #define FS_SIZE_MAX 0xFFFFFFFF /* When SIZE_MAX is not defined by the standard library just default to the maximum 32-bit unsigned integer. */ 811 | #endif 812 | 813 | #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__) 814 | #define FS_SIZEOF_PTR 8 815 | #else 816 | #define FS_SIZEOF_PTR 4 817 | #endif 818 | 819 | #if FS_SIZEOF_PTR == 8 820 | #define FS_64BIT 821 | #else 822 | #define FS_32BIT 823 | #endif 824 | 825 | #if defined(FS_USE_STDINT) 826 | #include 827 | typedef int8_t fs_int8; 828 | typedef uint8_t fs_uint8; 829 | typedef int16_t fs_int16; 830 | typedef uint16_t fs_uint16; 831 | typedef int32_t fs_int32; 832 | typedef uint32_t fs_uint32; 833 | typedef int64_t fs_int64; 834 | typedef uint64_t fs_uint64; 835 | #else 836 | typedef signed char fs_int8; 837 | typedef unsigned char fs_uint8; 838 | typedef signed short fs_int16; 839 | typedef unsigned short fs_uint16; 840 | typedef signed int fs_int32; 841 | typedef unsigned int fs_uint32; 842 | #if defined(_MSC_VER) && !defined(__clang__) 843 | typedef signed __int64 fs_int64; 844 | typedef unsigned __int64 fs_uint64; 845 | #else 846 | #if defined(__clang__) || (defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6))) 847 | #pragma GCC diagnostic push 848 | #pragma GCC diagnostic ignored "-Wlong-long" 849 | #if defined(__clang__) 850 | #pragma GCC diagnostic ignored "-Wc++11-long-long" 851 | #endif 852 | #endif 853 | typedef signed long long fs_int64; 854 | typedef unsigned long long fs_uint64; 855 | #if defined(__clang__) || (defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6))) 856 | #pragma GCC diagnostic pop 857 | #endif 858 | #endif 859 | #endif /* FS_USE_STDINT */ 860 | 861 | #if FS_SIZEOF_PTR == 8 862 | typedef fs_uint64 fs_uintptr; 863 | typedef fs_int64 fs_intptr; 864 | #else 865 | typedef fs_uint32 fs_uintptr; 866 | typedef fs_int32 fs_intptr; 867 | #endif 868 | 869 | typedef unsigned char fs_bool8; 870 | typedef unsigned int fs_bool32; 871 | #define FS_TRUE 1 872 | #define FS_FALSE 0 873 | 874 | 875 | #define FS_INT64_MAX ((fs_int64)(((fs_uint64)0x7FFFFFFF << 32) | 0xFFFFFFFF)) 876 | 877 | 878 | #ifndef FS_API 879 | #define FS_API 880 | #endif 881 | 882 | #ifdef _MSC_VER 883 | #define FS_INLINE __forceinline 884 | #elif defined(__GNUC__) 885 | /* 886 | I've had a bug report where GCC is emitting warnings about functions possibly not being inlineable. This warning happens when 887 | the __attribute__((always_inline)) attribute is defined without an "inline" statement. I think therefore there must be some 888 | case where "__inline__" is not always defined, thus the compiler emitting these warnings. When using -std=c89 or -ansi on the 889 | command line, we cannot use the "inline" keyword and instead need to use "__inline__". In an attempt to work around this issue 890 | I am using "__inline__" only when we're compiling in strict ANSI mode. 891 | */ 892 | #if defined(__STRICT_ANSI__) 893 | #define FS_GNUC_INLINE_HINT __inline__ 894 | #else 895 | #define FS_GNUC_INLINE_HINT inline 896 | #endif 897 | 898 | #if (__GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ >= 2)) || defined(__clang__) 899 | #define FS_INLINE FS_GNUC_INLINE_HINT __attribute__((always_inline)) 900 | #else 901 | #define FS_INLINE FS_GNUC_INLINE_HINT 902 | #endif 903 | #elif defined(__WATCOMC__) 904 | #define FS_INLINE __inline 905 | #else 906 | #define FS_INLINE 907 | #endif 908 | 909 | 910 | #if defined(__has_attribute) 911 | #if __has_attribute(format) 912 | #define FS_ATTRIBUTE_FORMAT(fmt, va) __attribute__((format(printf, fmt, va))) 913 | #endif 914 | #endif 915 | #ifndef FS_ATTRIBUTE_FORMAT 916 | #define FS_ATTRIBUTE_FORMAT(fmt, va) 917 | #endif 918 | 919 | 920 | #define FS_NULL_TERMINATED ((size_t)-1) 921 | /* END fs_compiler_compat.h */ 922 | 923 | 924 | /* BEG fs_result.h */ 925 | typedef enum fs_result 926 | { 927 | /* Compression Non-Error Result Codes. */ 928 | FS_HAS_MORE_OUTPUT = 102, /* Some stream has more output data to be read, but there's not enough room in the output buffer. */ 929 | FS_NEEDS_MORE_INPUT = 100, /* Some stream needs more input data before it can be processed. */ 930 | 931 | /* Main Result Codes. */ 932 | FS_SUCCESS = 0, 933 | FS_ERROR = -1, /* Generic, unknown error. */ 934 | FS_INVALID_ARGS = -2, 935 | FS_INVALID_OPERATION = -3, 936 | FS_OUT_OF_MEMORY = -4, 937 | FS_OUT_OF_RANGE = -5, 938 | FS_ACCESS_DENIED = -6, 939 | FS_DOES_NOT_EXIST = -7, 940 | FS_ALREADY_EXISTS = -8, 941 | FS_INVALID_FILE = -10, 942 | FS_TOO_BIG = -11, 943 | FS_PATH_TOO_LONG = -12, 944 | FS_NOT_DIRECTORY = -14, 945 | FS_IS_DIRECTORY = -15, 946 | FS_DIRECTORY_NOT_EMPTY = -16, 947 | FS_AT_END = -17, 948 | FS_BUSY = -19, 949 | FS_INTERRUPT = -21, 950 | FS_BAD_SEEK = -25, 951 | FS_NOT_IMPLEMENTED = -29, 952 | FS_TIMEOUT = -34, 953 | FS_CHECKSUM_MISMATCH = -100, 954 | FS_NO_BACKEND = -101 955 | } fs_result; 956 | /* END fs_result.h */ 957 | 958 | 959 | /* BEG fs_allocation_callbacks.h */ 960 | typedef struct fs_allocation_callbacks 961 | { 962 | void* pUserData; 963 | void* (* onMalloc )(size_t sz, void* pUserData); 964 | void* (* onRealloc)(void* p, size_t sz, void* pUserData); 965 | void (* onFree )(void* p, void* pUserData); 966 | } fs_allocation_callbacks; 967 | 968 | FS_API void* fs_malloc(size_t sz, const fs_allocation_callbacks* pAllocationCallbacks); 969 | FS_API void* fs_calloc(size_t sz, const fs_allocation_callbacks* pAllocationCallbacks); 970 | FS_API void* fs_realloc(void* p, size_t sz, const fs_allocation_callbacks* pAllocationCallbacks); 971 | FS_API void fs_free(void* p, const fs_allocation_callbacks* pAllocationCallbacks); 972 | /* END fs_allocation_callbacks.h */ 973 | 974 | 975 | 976 | /* BEG fs_stream.h */ 977 | /* 978 | Streams. 979 | 980 | The feeding of input and output data is done via a stream. 981 | 982 | To implement a custom stream, such as a memory stream, or a file stream, you need to extend from 983 | `fs_stream` and implement `fs_stream_vtable`. You can access your custom data by casting the 984 | `fs_stream` to your custom type. 985 | 986 | The stream vtable can support both reading and writing, but it doesn't need to support both at 987 | the same time. If one is not supported, simply leave the relevant `read` or `write` callback as 988 | `NULL`, or have them return FS_NOT_IMPLEMENTED. 989 | */ 990 | typedef enum fs_seek_origin 991 | { 992 | FS_SEEK_SET = 0, 993 | FS_SEEK_CUR = 1, 994 | FS_SEEK_END = 2 995 | } fs_seek_origin; 996 | 997 | typedef struct fs_stream_vtable fs_stream_vtable; 998 | typedef struct fs_stream fs_stream; 999 | 1000 | struct fs_stream_vtable 1001 | { 1002 | fs_result (* read )(fs_stream* pStream, void* pDst, size_t bytesToRead, size_t* pBytesRead); 1003 | fs_result (* write )(fs_stream* pStream, const void* pSrc, size_t bytesToWrite, size_t* pBytesWritten); 1004 | fs_result (* seek )(fs_stream* pStream, fs_int64 offset, fs_seek_origin origin); 1005 | fs_result (* tell )(fs_stream* pStream, fs_int64* pCursor); 1006 | /* BEG fs_stream_vtable_duplicate */ 1007 | size_t (* duplicate_alloc_size)(fs_stream* pStream); /* Optional. Returns the allocation size of the stream. When not defined, duplicating is disabled. */ 1008 | fs_result (* duplicate )(fs_stream* pStream, fs_stream* pDuplicatedStream); /* Optional. Duplicate the stream. */ 1009 | void (* uninit )(fs_stream* pStream); /* Optional. Uninitialize the stream. */ 1010 | /* END fs_stream_vtable_duplicate */ 1011 | }; 1012 | 1013 | struct fs_stream 1014 | { 1015 | const fs_stream_vtable* pVTable; 1016 | }; 1017 | 1018 | FS_API fs_result fs_stream_init(const fs_stream_vtable* pVTable, fs_stream* pStream); 1019 | FS_API fs_result fs_stream_read(fs_stream* pStream, void* pDst, size_t bytesToRead, size_t* pBytesRead); 1020 | FS_API fs_result fs_stream_write(fs_stream* pStream, const void* pSrc, size_t bytesToWrite, size_t* pBytesWritten); 1021 | FS_API fs_result fs_stream_seek(fs_stream* pStream, fs_int64 offset, fs_seek_origin origin); 1022 | FS_API fs_result fs_stream_tell(fs_stream* pStream, fs_int64* pCursor); 1023 | 1024 | /* BEG fs_stream_writef.h */ 1025 | FS_API fs_result fs_stream_writef(fs_stream* pStream, const char* fmt, ...) FS_ATTRIBUTE_FORMAT(2, 3); 1026 | FS_API fs_result fs_stream_writef_ex(fs_stream* pStream, const fs_allocation_callbacks* pAllocationCallbacks, const char* fmt, ...) FS_ATTRIBUTE_FORMAT(3, 4); 1027 | FS_API fs_result fs_stream_writefv(fs_stream* pStream, const char* fmt, va_list args); 1028 | FS_API fs_result fs_stream_writefv_ex(fs_stream* pStream, const fs_allocation_callbacks* pAllocationCallbacks, const char* fmt, va_list args); 1029 | /* END fs_stream_writef.h */ 1030 | 1031 | /* BEG fs_stream_duplicate.h */ 1032 | /* 1033 | Duplicates a stream. 1034 | 1035 | This will allocate the new stream on the heap. The caller is responsible for freeing the stream 1036 | with `fs_stream_delete_duplicate()` when it's no longer needed. 1037 | */ 1038 | FS_API fs_result fs_stream_duplicate(fs_stream* pStream, const fs_allocation_callbacks* pAllocationCallbacks, fs_stream** ppDuplicatedStream); 1039 | 1040 | /* 1041 | Deletes a duplicated stream. 1042 | 1043 | Do not use this for a stream that was not duplicated with `fs_stream_duplicate()`. 1044 | */ 1045 | FS_API void fs_stream_delete_duplicate(fs_stream* pDuplicatedStream, const fs_allocation_callbacks* pAllocationCallbacks); 1046 | /* END fs_stream_duplicate.h */ 1047 | 1048 | /* BEG fs_stream_helpers.h */ 1049 | /* 1050 | Helper functions for reading the entire contents of a stream, starting from the current cursor position. Free 1051 | the returned pointer with fs_free(). 1052 | 1053 | The format (FS_FORMAT_TEXT or FS_FORMAT_BINARY) is used to determine whether or not a null terminator should be 1054 | appended to the end of the data. 1055 | 1056 | For flexiblity in case the backend does not support cursor retrieval or positioning, the data will be read 1057 | in fixed sized chunks. 1058 | */ 1059 | typedef enum fs_format 1060 | { 1061 | FS_FORMAT_TEXT, 1062 | FS_FORMAT_BINARY 1063 | } fs_format; 1064 | 1065 | 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); 1066 | /* END fs_stream_helpers.h */ 1067 | /* END fs_stream.h */ 1068 | 1069 | 1070 | /* BEG fs_sysdir.h */ 1071 | typedef enum fs_sysdir_type 1072 | { 1073 | FS_SYSDIR_HOME, 1074 | FS_SYSDIR_TEMP, 1075 | FS_SYSDIR_CONFIG, 1076 | FS_SYSDIR_DATA, 1077 | FS_SYSDIR_CACHE 1078 | } fs_sysdir_type; 1079 | 1080 | 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. */ 1081 | /* END fs_sysdir.h */ 1082 | 1083 | 1084 | /* BEG fs_mktmp.h */ 1085 | /* Make sure these options do not conflict with FS_NO_CREATE_DIRS. */ 1086 | #define FS_MKTMP_DIR 0x0800 /* Create a temporary directory. */ 1087 | #define FS_MKTMP_FILE 0x1000 /* Create a temporary file. */ 1088 | 1089 | 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. */ 1090 | /* END fs_mktmp.h */ 1091 | 1092 | 1093 | /* BEG fs.h */ 1094 | /* Open mode flags. */ 1095 | #define FS_READ 0x0001 1096 | #define FS_WRITE 0x0002 1097 | #define FS_APPEND (FS_WRITE | 0x0004) 1098 | #define FS_OVERWRITE (FS_WRITE | 0x0008) 1099 | #define FS_TRUNCATE (FS_WRITE) 1100 | #define FS_TEMP (FS_TRUNCATE | 0x0010) 1101 | 1102 | #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. */ 1103 | #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. */ 1104 | #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" */ 1105 | 1106 | #define FS_NO_CREATE_DIRS 0x0040 /* When used, directories will not be created automatically when opening files for writing. */ 1107 | #define FS_IGNORE_MOUNTS 0x0080 /* When used, mounted directories and archives will be ignored when opening and iterating files. */ 1108 | #define FS_ONLY_MOUNTS 0x0100 /* When used, only mounted directories and archives will be considered when opening and iterating files. */ 1109 | #define FS_NO_SPECIAL_DIRS 0x0200 /* When used, the presence of special directories like "." and ".." will be result in an error when opening files. */ 1110 | #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(). */ 1111 | 1112 | #define FS_LOWEST_PRIORITY 0x2000 /* Only used with mounting. When set will create the mount with a lower priority to existing mounts. */ 1113 | 1114 | #define FS_NO_INCREMENT_REFCOUNT 0x4000 /* Internal use only. Used with fs_open_archive_ex() internally. */ 1115 | 1116 | /* Garbage collection policies.*/ 1117 | #define FS_GC_POLICY_THRESHOLD 0x0001 /* Only garbage collect unreferenced opened archives until the count is below the configured threshold. */ 1118 | #define FS_GC_POLICY_FULL 0x0002 /* Garbage collect every unreferenced opened archive, regardless of how many are open.*/ 1119 | 1120 | 1121 | typedef struct fs_config fs_config; 1122 | typedef struct fs fs; 1123 | typedef struct fs_file fs_file; 1124 | typedef struct fs_file_info fs_file_info; 1125 | typedef struct fs_iterator fs_iterator; 1126 | typedef struct fs_backend fs_backend; 1127 | 1128 | /* 1129 | This callback is fired when the reference count of a fs object changes. This is useful if you want 1130 | to do some kind of advanced memory management, such as garbage collection. If the new reference count 1131 | is 1, it means no other objects are referencing the fs object. 1132 | */ 1133 | typedef void (* fs_on_refcount_changed_proc)(void* pUserData, fs* pFS, fs_uint32 newRefCount, fs_uint32 oldRefCount); 1134 | 1135 | typedef struct fs_archive_type 1136 | { 1137 | const fs_backend* pBackend; 1138 | const char* pExtension; 1139 | } fs_archive_type; 1140 | 1141 | struct fs_file_info 1142 | { 1143 | fs_uint64 size; 1144 | fs_uint64 lastModifiedTime; 1145 | fs_uint64 lastAccessTime; 1146 | int directory; 1147 | int symlink; 1148 | }; 1149 | 1150 | struct fs_iterator 1151 | { 1152 | fs* pFS; 1153 | const char* pName; /* Must be null terminated. The FS implementation is responsible for manageing the memory allocation. */ 1154 | size_t nameLen; 1155 | fs_file_info info; 1156 | }; 1157 | 1158 | struct fs_config 1159 | { 1160 | const fs_backend* pBackend; 1161 | const void* pBackendConfig; 1162 | fs_stream* pStream; 1163 | const fs_archive_type* pArchiveTypes; 1164 | size_t archiveTypeCount; 1165 | fs_on_refcount_changed_proc onRefCountChanged; 1166 | void* pRefCountChangedUserData; 1167 | const fs_allocation_callbacks* pAllocationCallbacks; 1168 | }; 1169 | 1170 | FS_API fs_config fs_config_init_default(void); 1171 | FS_API fs_config fs_config_init(const fs_backend* pBackend, void* pBackendConfig, fs_stream* pStream); 1172 | 1173 | 1174 | typedef struct fs_backend 1175 | { 1176 | size_t (* alloc_size )(const void* pBackendConfig); 1177 | 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. */ 1178 | void (* uninit )(fs* pFS); 1179 | fs_result (* ioctl )(fs* pFS, int op, void* pArg); /* Optional. */ 1180 | fs_result (* remove )(fs* pFS, const char* pFilePath); 1181 | fs_result (* rename )(fs* pFS, const char* pOldName, const char* pNewName); 1182 | fs_result (* mkdir )(fs* pFS, const char* pPath); /* This is not recursive. Return FS_SUCCESS if directory already exists. */ 1183 | 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. */ 1184 | size_t (* file_alloc_size )(fs* pFS); 1185 | 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). */ 1186 | 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. */ 1187 | void (* file_close )(fs_file* pFile); 1188 | 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. */ 1189 | fs_result (* file_write )(fs_file* pFile, const void* pSrc, size_t bytesToWrite, size_t* pBytesWritten); 1190 | fs_result (* file_seek )(fs_file* pFile, fs_int64 offset, fs_seek_origin origin); 1191 | fs_result (* file_tell )(fs_file* pFile, fs_int64* pCursor); 1192 | fs_result (* file_flush )(fs_file* pFile); 1193 | fs_result (* file_info )(fs_file* pFile, fs_file_info* pInfo); 1194 | fs_result (* file_duplicate )(fs_file* pFile, fs_file* pDuplicate); /* Duplicate the file handle. */ 1195 | fs_iterator* (* first )(fs* pFS, const char* pDirectoryPath, size_t directoryPathLen); 1196 | fs_iterator* (* next )(fs_iterator* pIterator); /* <-- Must return null when there are no more files. In this case, free_iterator must be called internally. */ 1197 | 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. */ 1198 | } fs_backend; 1199 | 1200 | FS_API fs_result fs_init(const fs_config* pConfig, fs** ppFS); 1201 | FS_API void fs_uninit(fs* pFS); 1202 | FS_API fs_result fs_ioctl(fs* pFS, int op, void* pArg); 1203 | FS_API fs_result fs_remove(fs* pFS, const char* pFilePath); /* Does not consider mounts. */ 1204 | FS_API fs_result fs_rename(fs* pFS, const char* pOldName, const char* pNewName); /* Does not consider mounts. */ 1205 | 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. */ 1206 | 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. */ 1207 | FS_API fs_stream* fs_get_stream(fs* pFS); 1208 | FS_API const fs_allocation_callbacks* fs_get_allocation_callbacks(fs* pFS); 1209 | 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. */ 1210 | FS_API size_t fs_get_backend_data_size(fs* pFS); 1211 | FS_API fs* fs_ref(fs* pFS); /* Increments the reference count. Returns pFS. */ 1212 | FS_API fs_uint32 fs_unref(fs* pFS); /* Decrements the reference count. Does not uninitialize. */ 1213 | FS_API fs_uint32 fs_refcount(fs* pFS); 1214 | 1215 | 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); 1216 | FS_API fs_result fs_open_archive(fs* pFS, const char* pArchivePath, int openMode, fs** ppArchive); 1217 | FS_API void fs_close_archive(fs* pArchive); 1218 | FS_API void fs_gc_archives(fs* pFS, int policy); 1219 | FS_API void fs_set_archive_gc_threshold(fs* pFS, size_t threshold); 1220 | FS_API size_t fs_get_archive_gc_threshold(fs* pFS); 1221 | 1222 | FS_API fs_result fs_file_open(fs* pFS, const char* pFilePath, int openMode, fs_file** ppFile); 1223 | FS_API fs_result fs_file_open_from_handle(fs* pFS, void* hBackendFile, fs_file** ppFile); 1224 | FS_API void fs_file_close(fs_file* pFile); 1225 | 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. */ 1226 | FS_API fs_result fs_file_write(fs_file* pFile, const void* pSrc, size_t bytesToWrite, size_t* pBytesWritten); 1227 | FS_API fs_result fs_file_writef(fs_file* pFile, const char* fmt, ...) FS_ATTRIBUTE_FORMAT(2, 3); 1228 | FS_API fs_result fs_file_writefv(fs_file* pFile, const char* fmt, va_list args); 1229 | FS_API fs_result fs_file_seek(fs_file* pFile, fs_int64 offset, fs_seek_origin origin); 1230 | FS_API fs_result fs_file_tell(fs_file* pFile, fs_int64* pCursor); 1231 | FS_API fs_result fs_file_flush(fs_file* pFile); 1232 | FS_API fs_result fs_file_get_info(fs_file* pFile, fs_file_info* pInfo); 1233 | FS_API fs_result fs_file_duplicate(fs_file* pFile, fs_file** ppDuplicate); /* Duplicate the file handle. */ 1234 | FS_API void* fs_file_get_backend_data(fs_file* pFile); 1235 | FS_API size_t fs_file_get_backend_data_size(fs_file* pFile); 1236 | 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. */ 1237 | FS_API fs* fs_file_get_fs(fs_file* pFile); 1238 | 1239 | FS_API fs_iterator* fs_first_ex(fs* pFS, const char* pDirectoryPath, size_t directoryPathLen, int mode); 1240 | FS_API fs_iterator* fs_first(fs* pFS, const char* pDirectoryPath, int mode); 1241 | FS_API fs_iterator* fs_next(fs_iterator* pIterator); 1242 | FS_API void fs_free_iterator(fs_iterator* pIterator); 1243 | 1244 | FS_API fs_result fs_mount(fs* pFS, const char* pActualPath, const char* pVirtualPath, int options); 1245 | FS_API fs_result fs_unmount(fs* pFS, const char* pActualPath, int options); 1246 | FS_API fs_result fs_mount_sysdir(fs* pFS, fs_sysdir_type type, const char* pSubDir, const char* pVirtualPath, int options); 1247 | FS_API fs_result fs_unmount_sysdir(fs* pFS, fs_sysdir_type type, const char* pSubDir, int options); 1248 | FS_API fs_result fs_mount_fs(fs* pFS, fs* pOtherFS, const char* pVirtualPath, int options); 1249 | FS_API fs_result fs_unmount_fs(fs* pFS, fs* pOtherFS, int options); /* Must be matched up with fs_mount_fs(). */ 1250 | 1251 | /* 1252 | Helper functions for reading the entire contents of a file, starting from the current cursor position. Free 1253 | the returned pointer with fs_free(), using the same allocation callbacks as the fs object. You can use 1254 | fs_get_allocation_callbacks() if necessary, like so: 1255 | 1256 | fs_free(pFileData, fs_get_allocation_callbacks(pFS)); 1257 | 1258 | The format (FS_FORMAT_TEXT or FS_FORMAT_BINARY) is used to determine whether or not a null terminator should be 1259 | appended to the end of the data. 1260 | 1261 | For flexiblity in case the backend does not support cursor retrieval or positioning, the data will be read 1262 | in fixed sized chunks. 1263 | */ 1264 | FS_API fs_result fs_file_read_to_end(fs_file* pFile, fs_format format, void** ppData, size_t* pDataSize); 1265 | FS_API fs_result fs_file_open_and_read(fs* pFS, const char* pFilePath, fs_format format, void** ppData, size_t* pDataSize); 1266 | FS_API fs_result fs_file_open_and_write(fs* pFS, const char* pFilePath, void* pData, size_t dataSize); 1267 | 1268 | 1269 | /* Default Backend. */ 1270 | extern const fs_backend* FS_STDIO; /* The default stdio backend. The handle for fs_file_open_from_handle() is a FILE*. */ 1271 | /* END fs.h */ 1272 | 1273 | 1274 | /* BEG fs_errno.h */ 1275 | FS_API fs_result fs_result_from_errno(int error); 1276 | /* END fs_errno.h */ 1277 | 1278 | 1279 | /* BEG fs_path.h */ 1280 | /* 1281 | These functions are low-level functions for working with paths. The most important part of this API 1282 | is probably the iteration functions. These functions are used for iterating over each of the 1283 | segments of a path. This library will recognize both '\' and '/'. If you want to use a different 1284 | separator, you'll need to use a different library. Likewise, this library will treat paths as case 1285 | sensitive. Again, you'll need to use a different library if this is not suitable for you. 1286 | 1287 | Iteration will always return both sides of a separator. For example, if you iterate "abc/def", 1288 | you will get two items: "abc" and "def". Where this is of particular importance and where you must 1289 | be careful, is the handling of the root directory. If you iterate "/", it will also return two 1290 | items. The first will be length 0 with an offset of zero which represents the left side of the "/" 1291 | and the second will be length 0 with an offset of 1 which represents the right side. The reason for 1292 | this design is that it makes iteration unambiguous and makes it easier to reconstruct a path. 1293 | 1294 | The path API does not do any kind of validation to check if it represents an actual path on the 1295 | file system. Likewise, it does not do any validation to check if the path contains invalid 1296 | characters. All it cares about is "/" and "\". 1297 | */ 1298 | typedef struct 1299 | { 1300 | const char* pFullPath; 1301 | size_t fullPathLength; 1302 | size_t segmentOffset; 1303 | size_t segmentLength; 1304 | } fs_path_iterator; 1305 | 1306 | FS_API fs_result fs_path_first(const char* pPath, size_t pathLen, fs_path_iterator* pIterator); 1307 | FS_API fs_result fs_path_last(const char* pPath, size_t pathLen, fs_path_iterator* pIterator); 1308 | FS_API fs_result fs_path_next(fs_path_iterator* pIterator); 1309 | FS_API fs_result fs_path_prev(fs_path_iterator* pIterator); 1310 | FS_API fs_bool32 fs_path_is_first(const fs_path_iterator* pIterator); 1311 | FS_API fs_bool32 fs_path_is_last(const fs_path_iterator* pIterator); 1312 | FS_API int fs_path_iterators_compare(const fs_path_iterator* pIteratorA, const fs_path_iterator* pIteratorB); 1313 | FS_API int fs_path_compare(const char* pPathA, size_t pathALen, const char* pPathB, size_t pathBLen); 1314 | 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. */ 1315 | 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. */ 1316 | 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. */ 1317 | 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. */ 1318 | FS_API const char* fs_path_trim_base(const char* pPath, size_t pathLen, const char* pBasePath, size_t basePathLen); 1319 | FS_API fs_bool32 fs_path_begins_with(const char* pPath, size_t pathLen, const char* pBasePath, size_t basePathLen); 1320 | 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. */ 1321 | 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:", "//
", 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. */ 1322 | /* END fs_path.h */ 1323 | 1324 | 1325 | /* BEG fs_memory_stream.h */ 1326 | /* 1327 | Memory streams support both reading and writing within the same stream. To only support read-only 1328 | mode, use fs_memory_stream_init_readonly(). With this you can pass in a standard data/size pair. 1329 | 1330 | If you need writing support, use fs_memory_stream_init_write(). When writing data, the stream will 1331 | output to a buffer that is owned by the stream. When you need to access the data, do so by 1332 | inspecting the pointer directly with `stream.write.pData` and `stream.write.dataSize`. This mode 1333 | also supports reading. 1334 | 1335 | You can overwrite data by seeking to the required location and then just writing like normal. To 1336 | append data, just seek to the end: 1337 | 1338 | fs_memory_stream_seek(pStream, 0, FS_SEEK_END); 1339 | 1340 | The memory stream need not be uninitialized in read-only mode. In write mode you can use 1341 | `fs_memory_stream_uninit()` to free the data. Alternatively you can just take ownership of the 1342 | buffer and free it yourself with `fs_free()`. 1343 | 1344 | Below is an example for write mode. 1345 | 1346 | ```c 1347 | fs_memory_stream stream; 1348 | fs_memory_stream_init_write(NULL, &stream); 1349 | 1350 | // Write some data to the stream... 1351 | fs_memory_stream_write(&stream, pSomeData, someDataSize, NULL); 1352 | 1353 | // Do something with the data. 1354 | do_something_with_my_data(stream.write.pData, stream.write.dataSize); 1355 | ``` 1356 | 1357 | To free the data, you can use `fs_memory_stream_uninit()`, or you can take ownership of the data 1358 | and free it yourself with `fs_free()`: 1359 | 1360 | ```c 1361 | fs_memory_stream_uninit(&stream); 1362 | ``` 1363 | 1364 | Or to take ownership: 1365 | 1366 | ```c 1367 | size_t dataSize; 1368 | void* pData = fs_memory_stream_take_ownership(&stream, &dataSize); 1369 | ``` 1370 | 1371 | With the above, `pData` will be the pointer to the data and `dataSize` will be the size of the data 1372 | and you will be responsible for deleting the buffer with `fs_free()`. 1373 | 1374 | 1375 | Read mode is simpler: 1376 | 1377 | ```c 1378 | fs_memory_stream stream; 1379 | fs_memory_stream_init_readonly(pData, dataSize, &stream); 1380 | 1381 | // Read some data. 1382 | fs_memory_stream_read(&stream, &myBuffer, bytesToRead, NULL); 1383 | ``` 1384 | 1385 | There is only one cursor. As you read and write the cursor will move forward. If you need to 1386 | read and write from different locations from the same fs_memory_stream object, you need to 1387 | seek before doing your read or write. You cannot read and write at the same time across 1388 | multiple threads for the same fs_memory_stream object. 1389 | */ 1390 | typedef struct fs_memory_stream fs_memory_stream; 1391 | 1392 | struct fs_memory_stream 1393 | { 1394 | fs_stream base; 1395 | void** ppData; /* Will be set to &readonly.pData in readonly mode. */ 1396 | size_t* pDataSize; /* Will be set to &readonly.dataSize in readonly mode. */ 1397 | struct 1398 | { 1399 | const void* pData; 1400 | size_t dataSize; 1401 | } readonly; 1402 | struct 1403 | { 1404 | void* pData; /* Will only be set in write mode. */ 1405 | size_t dataSize; 1406 | size_t dataCap; 1407 | } write; 1408 | size_t cursor; 1409 | fs_allocation_callbacks allocationCallbacks; /* This is copied from the allocation callbacks passed in from e_memory_stream_init(). Only used in write mode. */ 1410 | }; 1411 | 1412 | FS_API fs_result fs_memory_stream_init_write(const fs_allocation_callbacks* pAllocationCallbacks, fs_memory_stream* pStream); 1413 | FS_API fs_result fs_memory_stream_init_readonly(const void* pData, size_t dataSize, fs_memory_stream* pStream); 1414 | 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. */ 1415 | FS_API fs_result fs_memory_stream_read(fs_memory_stream* pStream, void* pDst, size_t bytesToRead, size_t* pBytesRead); 1416 | FS_API fs_result fs_memory_stream_write(fs_memory_stream* pStream, const void* pSrc, size_t bytesToWrite, size_t* pBytesWritten); 1417 | FS_API fs_result fs_memory_stream_seek(fs_memory_stream* pStream, fs_int64 offset, int origin); 1418 | FS_API fs_result fs_memory_stream_tell(fs_memory_stream* pStream, size_t* pCursor); 1419 | FS_API fs_result fs_memory_stream_remove(fs_memory_stream* pStream, size_t offset, size_t size); 1420 | FS_API fs_result fs_memory_stream_truncate(fs_memory_stream* pStream); 1421 | 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. */ 1422 | /* END fs_memory_stream.h */ 1423 | 1424 | 1425 | /* BEG fs_utils.h */ 1426 | FS_API void fs_sort(void* pBase, size_t count, size_t stride, int (*compareProc)(void*, const void*, const void*), void* pUserData); 1427 | 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); 1428 | 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); 1429 | 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); 1430 | 1431 | FS_API int fs_strncmp(const char* str1, const char* str2, size_t maxLen); 1432 | FS_API int fs_strnicmp(const char* str1, const char* str2, size_t count); 1433 | /* END fs_utils.h */ 1434 | 1435 | 1436 | /* BEG fs_snprintf.h */ 1437 | FS_API int fs_vsprintf(char* buf, char const* fmt, va_list va); 1438 | FS_API int fs_vsnprintf(char* buf, size_t count, char const* fmt, va_list va); 1439 | FS_API int fs_sprintf(char* buf, char const* fmt, ...) FS_ATTRIBUTE_FORMAT(2, 3); 1440 | FS_API int fs_snprintf(char* buf, size_t count, char const* fmt, ...) FS_ATTRIBUTE_FORMAT(3, 4); 1441 | /* END fs_snprintf.h */ 1442 | 1443 | 1444 | #if defined(__cplusplus) 1445 | } 1446 | #endif 1447 | #endif /* fs_h */ 1448 | 1449 | /* 1450 | This software is available as a choice of the following licenses. Choose 1451 | whichever you prefer. 1452 | 1453 | =============================================================================== 1454 | ALTERNATIVE 1 - Public Domain (www.unlicense.org) 1455 | =============================================================================== 1456 | This is free and unencumbered software released into the public domain. 1457 | 1458 | Anyone is free to copy, modify, publish, use, compile, sell, or distribute this 1459 | software, either in source code form or as a compiled binary, for any purpose, 1460 | commercial or non-commercial, and by any means. 1461 | 1462 | In jurisdictions that recognize copyright laws, the author or authors of this 1463 | software dedicate any and all copyright interest in the software to the public 1464 | domain. We make this dedication for the benefit of the public at large and to 1465 | the detriment of our heirs and successors. We intend this dedication to be an 1466 | overt act of relinquishment in perpetuity of all present and future rights to 1467 | this software under copyright law. 1468 | 1469 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 1470 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 1471 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 1472 | AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 1473 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 1474 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 1475 | 1476 | For more information, please refer to 1477 | 1478 | =============================================================================== 1479 | ALTERNATIVE 2 - MIT No Attribution 1480 | =============================================================================== 1481 | Copyright 2025 David Reid 1482 | 1483 | Permission is hereby granted, free of charge, to any person obtaining a copy of 1484 | this software and associated documentation files (the "Software"), to deal in 1485 | the Software without restriction, including without limitation the rights to 1486 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 1487 | of the Software, and to permit persons to whom the Software is furnished to do 1488 | so. 1489 | 1490 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 1491 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 1492 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 1493 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 1494 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 1495 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 1496 | SOFTWARE. 1497 | */ 1498 | -------------------------------------------------------------------------------- /tests/fstest.c: -------------------------------------------------------------------------------- 1 | #include "../fs.h" 2 | #include "../extras/backends/zip/fs_zip.h" 3 | #include "../extras/backends/pak/fs_pak.h" 4 | #include "../extras/backends/sub/fs_sub.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | FS_API int fs_strncpy(char* dst, const char* src, size_t count); /* <-- This is not forward declared in the header. It exists in the .c file though. */ 12 | 13 | 14 | static void fstest_print_result_v(const char* pPattern, int result, va_list args) 15 | { 16 | const char* pResultStr = (result == 0) ? "PASS" : "FAIL"; 17 | char pBuffer[1024]; 18 | char pSpaces[1024]; 19 | 20 | memset(pSpaces, ' ', sizeof(pSpaces)); 21 | fs_vsnprintf(pBuffer, 1024, pPattern, args); 22 | 23 | printf("%s%s%.*s%s\033[0m\n", ((result == 0) ? "\033[32m" : "\033[31m"), pBuffer, 80 - 4 - (int)strlen(pBuffer), pSpaces, pResultStr); 24 | } 25 | 26 | static void fstest_print_result_f(const char* pPattern, int result, ...) 27 | { 28 | va_list args; 29 | va_start(args, result); 30 | { 31 | fstest_print_result_v(pPattern, result, args); 32 | } 33 | va_end(args); 34 | } 35 | 36 | 37 | 38 | 39 | static int fstest_breakup_path_forward(const char* pPath, size_t pathLen, fs_path_iterator pIterator[32], size_t* pCount) 40 | { 41 | fs_result result; 42 | fs_path_iterator i; 43 | 44 | *pCount = 0; 45 | 46 | for (result = fs_path_first(pPath, pathLen, &i); result == FS_SUCCESS; result = fs_path_next(&i)) { 47 | pIterator[*pCount] = i; 48 | *pCount += 1; 49 | } 50 | 51 | if (result == FS_SUCCESS || result == FS_AT_END) { 52 | return 0; 53 | } else { 54 | return 1; 55 | } 56 | } 57 | 58 | static int fstest_breakup_path_reverse(const char* pPath, size_t pathLen, fs_path_iterator pIterator[32], size_t* pCount) 59 | { 60 | fs_result result; 61 | fs_path_iterator i; 62 | 63 | *pCount = 0; 64 | 65 | for (result = fs_path_last(pPath, pathLen, &i); result == FS_SUCCESS; result = fs_path_prev(&i)) { 66 | pIterator[*pCount] = i; 67 | *pCount += 1; 68 | } 69 | 70 | if (result == FS_SUCCESS || result == FS_AT_END) { 71 | return 0; 72 | } else { 73 | return 1; 74 | } 75 | } 76 | 77 | static int fstest_reconstruct_path_forward(const fs_path_iterator* pIterator, size_t iteratorCount, char pPath[1024]) 78 | { 79 | size_t i; 80 | size_t len = 0; 81 | 82 | pPath[0] = '\0'; 83 | 84 | for (i = 0; i < iteratorCount; i++) { 85 | fs_strncpy(pPath + len, pIterator[i].pFullPath + pIterator[i].segmentOffset, pIterator[i].segmentLength); 86 | len += pIterator[i].segmentLength; 87 | 88 | if (i+1 < iteratorCount) { 89 | pPath[len] = '/'; 90 | len += 1; 91 | } 92 | } 93 | 94 | pPath[len] = '\0'; 95 | return 0; 96 | } 97 | 98 | static int fstest_reconstruct_path_reverse(const fs_path_iterator* pIterator, size_t iteratorCount, char pPath[1024]) 99 | { 100 | size_t i; 101 | size_t len = 0; 102 | 103 | pPath[0] = '\0'; 104 | 105 | for (i = iteratorCount; i > 0; i--) { 106 | fs_strncpy(pPath + len, pIterator[i-1].pFullPath + pIterator[i-1].segmentOffset, pIterator[i-1].segmentLength); 107 | len += pIterator[i-1].segmentLength; 108 | 109 | if (i-1 > 0) { 110 | pPath[len] = '/'; 111 | len += 1; 112 | } 113 | } 114 | 115 | pPath[len] = '\0'; 116 | return 0; 117 | } 118 | 119 | static int fstest_path(const char* pPath) 120 | { 121 | fs_path_iterator segmentsForward[32]; 122 | fs_path_iterator segmentsReverse[32]; 123 | size_t segmentsForwardCount; 124 | size_t segmentsReverseCount; 125 | char pPathReconstructedForward[1024]; 126 | char pPathReconstructedReverse[1024]; 127 | int forwardResult = 0; 128 | int reverseResult = 0; 129 | 130 | printf("Path: \"%s\"\n", pPath); 131 | 132 | fstest_breakup_path_forward(pPath, (size_t)-1, segmentsForward, &segmentsForwardCount); 133 | fstest_breakup_path_reverse(pPath, (size_t)-1, segmentsReverse, &segmentsReverseCount); 134 | 135 | fstest_reconstruct_path_forward(segmentsForward, segmentsForwardCount, pPathReconstructedForward); 136 | fstest_reconstruct_path_reverse(segmentsReverse, segmentsReverseCount, pPathReconstructedReverse); 137 | 138 | if (strcmp(pPath, pPathReconstructedForward) != 0) { 139 | forwardResult = 1; 140 | } 141 | if (strcmp(pPath, pPathReconstructedReverse) != 0) { 142 | reverseResult = 1; 143 | } 144 | 145 | fstest_print_result_f(" Forward: \"%s\"", forwardResult, pPathReconstructedForward); 146 | fstest_print_result_f(" Reverse: \"%s\"", reverseResult, pPathReconstructedReverse); 147 | 148 | if (forwardResult == 0 && reverseResult == 0) { 149 | return 0; 150 | } else { 151 | return 1; 152 | } 153 | } 154 | 155 | static int fstest_path_normalize(const char* pPath, const char* pExpected) 156 | { 157 | char pNormalizedPath[1024]; 158 | int result; 159 | int length; 160 | 161 | printf("Path: \"%s\" = \"%s\"\n", pPath, (pExpected == NULL) ? "ERROR" : pExpected); 162 | 163 | /* Get the length first so we can check that it's working correctly. */ 164 | length = fs_path_normalize(NULL, 0, pPath, FS_NULL_TERMINATED, 0); 165 | 166 | result = fs_path_normalize(pNormalizedPath, sizeof(pNormalizedPath), pPath, FS_NULL_TERMINATED, 0); 167 | if (result < 0) { 168 | if (pExpected != NULL) { 169 | fstest_print_result_f(" Normalized: %s", 1, "ERROR"); 170 | return 1; 171 | } else { 172 | fstest_print_result_f(" Normalized: %s", 0, "ERROR"); 173 | return 0; 174 | } 175 | } 176 | 177 | /* Compare the length. */ 178 | result = length != result; 179 | fstest_print_result_f(" Length: %d", result, length); 180 | 181 | /* Compare the result with expected. */ 182 | result = strcmp(pNormalizedPath, pExpected) != 0; 183 | fstest_print_result_f(" Normalized: \"%s\"", result, pNormalizedPath); 184 | 185 | /* Just for placing a breakpoint for debugging. */ 186 | if (result == 1) { 187 | return 1; 188 | } 189 | 190 | return result; 191 | } 192 | 193 | static int fstest_paths() 194 | { 195 | int result = 0; 196 | 197 | result |= fstest_path("/"); 198 | result |= fstest_path(""); 199 | result |= fstest_path("/abc"); 200 | result |= fstest_path("/abc/"); 201 | result |= fstest_path("abc/"); 202 | result |= fstest_path("/abc/def/ghi"); 203 | result |= fstest_path("/abc/def/ghi/"); 204 | result |= fstest_path("abc/def/ghi/"); 205 | result |= fstest_path("C:"); 206 | result |= fstest_path("C:/"); 207 | result |= fstest_path("C:/abc"); 208 | result |= fstest_path("C:/abc/"); 209 | result |= fstest_path("C:/abc/def/ghi"); 210 | result |= fstest_path("C:/abc/def/ghi/"); 211 | result |= fstest_path("//localhost"); 212 | result |= fstest_path("//localhost/abc"); 213 | result |= fstest_path("//localhost//abc"); 214 | result |= fstest_path("~"); 215 | result |= fstest_path("~/Documents"); 216 | 217 | printf("\n"); 218 | printf("Path Normalization\n"); 219 | result |= fstest_path_normalize("", ""); 220 | result |= fstest_path_normalize("/", "/"); 221 | result |= fstest_path_normalize("/abc/def/ghi", "/abc/def/ghi"); 222 | result |= fstest_path_normalize("/..", NULL); /* Expecting error. */ 223 | result |= fstest_path_normalize("..", ".."); 224 | result |= fstest_path_normalize("abc/../def", "def"); 225 | result |= fstest_path_normalize("abc/./def", "abc/def"); 226 | result |= fstest_path_normalize("../abc/def", "../abc/def"); 227 | result |= fstest_path_normalize("abc/def/..", "abc"); 228 | result |= fstest_path_normalize("abc/../../def", "../def"); 229 | result |= fstest_path_normalize("/abc/../../def", NULL); /* Expecting error. */ 230 | result |= fstest_path_normalize("abc/def/", "abc/def"); 231 | result |= fstest_path_normalize("/abc/def/", "/abc/def"); 232 | 233 | printf("\n"); 234 | 235 | if (result == 0) { 236 | return 0; 237 | } else { 238 | return 1; 239 | } 240 | } 241 | 242 | 243 | static int fstest_sysdirs(void) 244 | { 245 | char pPath[1024]; 246 | size_t length; 247 | 248 | length = fs_sysdir(FS_SYSDIR_HOME, pPath, sizeof(pPath)); 249 | printf("HOME: %s\n", (length == 0) ? "ERROR" : pPath); 250 | 251 | length = fs_sysdir(FS_SYSDIR_TEMP, pPath, sizeof(pPath)); 252 | printf("TEMP: %s\n", (length == 0) ? "ERROR" : pPath); 253 | 254 | length = fs_sysdir(FS_SYSDIR_CONFIG, pPath, sizeof(pPath)); 255 | printf("CONFIG: %s\n", (length == 0) ? "ERROR" : pPath); 256 | 257 | length = fs_sysdir(FS_SYSDIR_DATA, pPath, sizeof(pPath)); 258 | printf("DATA: %s\n", (length == 0) ? "ERROR" : pPath); 259 | 260 | length = fs_sysdir(FS_SYSDIR_CACHE, pPath, sizeof(pPath)); 261 | printf("CACHE: %s\n", (length == 0) ? "ERROR" : pPath); 262 | 263 | return 0; 264 | } 265 | 266 | static int fstest_default_io(void) 267 | { 268 | /* TODO: Implement me. */ 269 | 270 | printf("Temporaries\n"); 271 | { 272 | char tmpPath[1024]; 273 | fs_result result; 274 | 275 | result = fs_mktmp(".testing/xyz", tmpPath, sizeof(tmpPath), FS_MKTMP_DIR); 276 | fstest_print_result_f(" mktmp: %s", (result != FS_SUCCESS), tmpPath); 277 | 278 | { 279 | fs_file* pFile; 280 | result = fs_file_open(NULL, ".testing/abc", FS_TEMP, &pFile); 281 | if (result == FS_SUCCESS) { 282 | fs_file_writef(pFile, "Hello"); 283 | fs_file_close(pFile); 284 | } else { 285 | fstest_print_result_f(" Open: %s", (result != FS_SUCCESS), ".testing/abc"); 286 | } 287 | } 288 | } 289 | 290 | return 0; 291 | } 292 | 293 | static int fstest_archive_io_file(fs* pFS, const char* pFilePath, const char* pOutputDirectory, int openMode) 294 | { 295 | fs_result result; 296 | fs_file_info fileInfo; 297 | fs_file* pFileIn; 298 | fs_file* pFileOut; 299 | char pOutputFilePath[1024]; 300 | fs_result readResult = FS_SUCCESS; 301 | fs_result writeResult = FS_SUCCESS; 302 | fs_uint64 totalBytesRead; 303 | 304 | result = fs_info(pFS, pFilePath, FS_READ | openMode, &fileInfo); 305 | fstest_print_result_f(" Info %s", (result != FS_SUCCESS), pFilePath); 306 | if (result != 0) { 307 | return 1; 308 | } 309 | 310 | result = fs_file_open(pFS, pFilePath, FS_READ | openMode, &pFileIn); 311 | fstest_print_result_f(" Open %s", (result != FS_SUCCESS), pFilePath); 312 | if (result != 0) { 313 | return 1; 314 | } 315 | 316 | result = fs_file_get_info(pFileIn, &fileInfo); 317 | fstest_print_result_f(" File Info %s", (result != FS_SUCCESS), pFilePath); 318 | if (result != 0) { 319 | fs_file_close(pFileIn); 320 | return 1; 321 | } 322 | 323 | fs_snprintf(pOutputFilePath, sizeof(pOutputFilePath), "%s/%s", pOutputDirectory, fs_path_file_name(pFilePath, (size_t)-1)); 324 | result = fs_file_open(pFS, pOutputFilePath, FS_WRITE | FS_TRUNCATE, &pFileOut); 325 | fstest_print_result_f(" Open %s", (result != FS_SUCCESS), pOutputFilePath); 326 | if (result != 0) { 327 | fs_file_close(pFileIn); 328 | return 1; 329 | } 330 | 331 | totalBytesRead = 0; 332 | for (;;) { 333 | char chunk[4096]; 334 | size_t bytesRead = 0; 335 | 336 | result = fs_file_read(pFileIn, chunk, sizeof(chunk), &bytesRead); 337 | if (result != FS_SUCCESS) { 338 | if (result == FS_AT_END && bytesRead == 0) { 339 | readResult = FS_SUCCESS; /* Don't present an error for an EOF condition. */ 340 | } else { 341 | readResult = result; 342 | } 343 | 344 | break; 345 | } 346 | 347 | if (bytesRead == 0) { 348 | break; 349 | } 350 | 351 | totalBytesRead += bytesRead; 352 | 353 | result = fs_file_write(pFileOut, chunk, bytesRead, NULL); 354 | if (result != FS_SUCCESS) { 355 | writeResult = result; 356 | break; 357 | } 358 | } 359 | 360 | fstest_print_result_f(" Read %s", (readResult != FS_SUCCESS), pFilePath); 361 | fstest_print_result_f(" Write %s", (writeResult != FS_SUCCESS), pOutputFilePath); 362 | fstest_print_result_f(" Bytes %llu", (totalBytesRead != fileInfo.size), totalBytesRead); 363 | 364 | 365 | fs_file_close(pFileIn); 366 | fs_file_close(pFileOut); 367 | 368 | return 0; 369 | } 370 | 371 | static int fstest_archive_io() 372 | { 373 | int result; 374 | fs* pFS; 375 | fs_config fsConfig; 376 | fs_archive_type pArchiveTypes[] = 377 | { 378 | {FS_ZIP, "zip"}, 379 | {FS_PAK, "pak"} 380 | }; 381 | 382 | printf("Archive I/O\n"); 383 | 384 | fsConfig = fs_config_init_default(); 385 | fsConfig.pArchiveTypes = pArchiveTypes; 386 | fsConfig.archiveTypeCount = sizeof(pArchiveTypes) / sizeof(pArchiveTypes[0]); 387 | 388 | result = fs_init(&fsConfig, &pFS) != FS_SUCCESS; 389 | fstest_print_result_f(" FS_STDIO Initialization", (result != FS_SUCCESS)); 390 | if (result != FS_SUCCESS) { 391 | return 1; 392 | } 393 | 394 | /* These test that loading above the level of the mount point correct results in an error. */ 395 | /* TODO: Do proper automated tests for these. */ 396 | #if 1 397 | fs_mount(pFS, "testvectors", "mnt", FS_READ); 398 | { 399 | fs_file* pFile; 400 | 401 | result = fs_file_open(pFS, "mnt/../testvectors/miniaudio.h", FS_READ | FS_NO_ABOVE_ROOT_NAVIGATION /*| FS_ONLY_MOUNTS*/, &pFile); 402 | if (result == FS_SUCCESS) { 403 | assert(!"Test failed. Should not be able to open a file above the mount point."); 404 | fs_file_close(pFile); 405 | } 406 | } 407 | fs_unmount(pFS, "testvectors", FS_READ); 408 | #endif 409 | 410 | /* Test archives in archives. */ 411 | #if 1 412 | { 413 | fs_file* pFile1; 414 | fs_file* pFile2; 415 | 416 | result = fs_file_open(pFS, "testvectors/testvectors2.zip/testvectors.zip/miniaudio.h", FS_READ | FS_VERBOSE, &pFile1); 417 | if (result != FS_SUCCESS) { 418 | printf("Failed to open file.\n"); 419 | return 1; 420 | } 421 | 422 | fs_file_close(pFile1); 423 | 424 | 425 | result = fs_file_open(pFS, "testvectors/testvectors2.zip/testvectors.zip/miniaudio.h", FS_READ | FS_VERBOSE, &pFile2); 426 | if (result != FS_SUCCESS) { 427 | printf("Failed to open file.\n"); 428 | return 1; 429 | } 430 | 431 | fs_file_close(pFile2); 432 | 433 | 434 | //fs_gc_archives(pFS, FS_GC_POLICY_FULL); 435 | } 436 | #endif 437 | 438 | 439 | fs_mount(pFS, "testvectors/extracted", NULL, FS_WRITE); 440 | 441 | //fs_mount(pFS, "test", NULL, FS_READ); 442 | //fs_mount(pFS, "blah", NULL, FS_READ); 443 | 444 | result |= fstest_archive_io_file(pFS, "testvectors/testvectors.zip/miniaudio.h", "", FS_VERBOSE); 445 | result |= fstest_archive_io_file(pFS, "testvectors/miniaudio.h", "", FS_TRANSPARENT); 446 | result |= fstest_archive_io_file(pFS, "testvectors/testvectors.zip/miniaudio.h", "", FS_TRANSPARENT); /* Files opened in transparent mode must still support verbose paths. */ 447 | 448 | #if 1 449 | /* Mounted tests. TODO: Improve these. Make a separate test. */ 450 | if (fs_mount(pFS, "testvectors", NULL, FS_READ) != FS_SUCCESS) { printf("FAILED TO MOUNT 'testvectors'\n"); } 451 | { 452 | result |= fstest_archive_io_file(pFS, "testvectors.zip/miniaudio.h", "", FS_VERBOSE); 453 | } 454 | fs_unmount(pFS, "testvectors", FS_READ); 455 | 456 | if (fs_mount(pFS, "testvectors/testvectors.zip", NULL, FS_READ) != FS_SUCCESS) { printf("FAILED TO MOUNT 'testvectors/testvectors.zip'\n"); } 457 | { 458 | result |= fstest_archive_io_file(pFS, "miniaudio.h", "", FS_VERBOSE); 459 | } 460 | fs_unmount(pFS, "testvectors/testvectors.zip", FS_READ); 461 | #endif 462 | 463 | fs_uninit(pFS); 464 | return result; 465 | } 466 | 467 | static int fstest_write_io() 468 | { 469 | int result; 470 | fs* pFS; 471 | fs_config fsConfig; 472 | 473 | printf("Write I/O\n"); 474 | 475 | fsConfig = fs_config_init_default(); 476 | 477 | result = fs_init(&fsConfig, &pFS) != FS_SUCCESS; 478 | fstest_print_result_f(" FS_STDIO Initialization", (result != FS_SUCCESS)); 479 | if (result != FS_SUCCESS) { 480 | return 1; 481 | } 482 | 483 | fs_mount(pFS, "testvectors/write", NULL, FS_WRITE); 484 | fs_mount(pFS, "testvectors/write/config", "config", FS_WRITE); 485 | fs_mount(pFS, "testvectors/write/config/editor", "config/editor", FS_WRITE); 486 | 487 | { 488 | fs_file* pFile; 489 | const char* pContent = "Hello, World!"; 490 | 491 | result = fs_file_open(pFS, "config/editor/editor.cfg", FS_WRITE | FS_TRUNCATE, &pFile) != FS_SUCCESS; 492 | if (result != 0) { 493 | printf("Failed to open file for writing.\n"); 494 | return 1; 495 | } 496 | 497 | result = fs_file_write(pFile, pContent, strlen(pContent), NULL) != FS_SUCCESS; 498 | if (result != 0) { 499 | printf("Failed to write to file.\n"); 500 | return 1; 501 | } 502 | 503 | fs_file_close(pFile); 504 | } 505 | 506 | return 0; 507 | } 508 | 509 | static int fstest_iteration() 510 | { 511 | int result; 512 | fs* pFS; 513 | fs_config fsConfig; 514 | fs_archive_type pArchiveTypes[] = 515 | { 516 | {FS_ZIP, "zip"} 517 | }; 518 | 519 | printf("Iteration\n"); 520 | 521 | /* Default iteration. */ 522 | printf(" Default\n"); 523 | { 524 | fs_iterator* pIterator; 525 | 526 | for (pIterator = fs_first(NULL, "", 0); pIterator != NULL; pIterator = fs_next(pIterator)) { 527 | printf(" %s\n", pIterator->pName); 528 | } 529 | } 530 | 531 | 532 | fsConfig = fs_config_init_default(); 533 | fsConfig.pArchiveTypes = pArchiveTypes; 534 | fsConfig.archiveTypeCount = sizeof(pArchiveTypes) / sizeof(pArchiveTypes[0]); 535 | 536 | result = fs_init(&fsConfig, &pFS); 537 | 538 | /* Iteration with registered archives. */ 539 | printf(" With Archives\n"); 540 | result = fs_init(&fsConfig, &pFS) != FS_SUCCESS; 541 | fstest_print_result_f(" FS_STDIO Initialization", (result != FS_SUCCESS)); 542 | if (result != FS_SUCCESS) { 543 | return 1; 544 | } 545 | 546 | fs_mount(pFS, "testvectors", NULL, FS_READ); 547 | 548 | { 549 | fs_iterator* pIterator; 550 | 551 | for (pIterator = fs_first(pFS, "iteration", FS_TRANSPARENT); pIterator != NULL; pIterator = fs_next(pIterator)) { 552 | printf(" %s\n", pIterator->pName); 553 | } 554 | } 555 | 556 | return 0; 557 | 558 | } 559 | 560 | static int fstest_io() 561 | { 562 | int result = 0; 563 | 564 | result |= fstest_sysdirs(); 565 | result |= fstest_default_io(); 566 | result |= fstest_archive_io(); 567 | result |= fstest_write_io(); 568 | result |= fstest_iteration(); 569 | 570 | if (result == 0) { 571 | return 0; 572 | } else { 573 | return 1; 574 | } 575 | } 576 | 577 | 578 | int main(int argc, char** argv) 579 | { 580 | fstest_paths(); 581 | fstest_io(); 582 | 583 | (void)argc; 584 | (void)argv; 585 | 586 | return 0; 587 | } 588 | -------------------------------------------------------------------------------- /tools/fsu.c: -------------------------------------------------------------------------------- 1 | #include "../fs.h" 2 | #include "../extras/backends/zip/fs_zip.h" 3 | #include "../extras/backends/pak/fs_pak.h" 4 | 5 | #include 6 | #include 7 | 8 | void print_help(void) 9 | { 10 | printf("Usage: fsu [operation] [args]\n"); 11 | printf("\n"); 12 | printf(" extract \n"); 13 | } 14 | 15 | fs_result extract_iterator(fs* pFS, fs* pArchive, fs_iterator* pIterator, const char* pFolderPath) 16 | { 17 | fs_result result; 18 | 19 | while (pIterator != NULL) { 20 | /* TODO: Make this more robust by dynamically allocating on the heap as necessary. */ 21 | char pFullPath[4096]; 22 | fs_path_append(pFullPath, sizeof(pFullPath), pFolderPath, FS_NULL_TERMINATED, pIterator->pName, pIterator->nameLen); 23 | 24 | if (pIterator->info.directory) { 25 | printf("Directory: %s\n", pFullPath); 26 | 27 | fs_mkdir(pFS, pFullPath, 0); 28 | 29 | result = extract_iterator(pFS, pArchive, fs_first(pArchive, pFullPath, FS_OPAQUE), pFullPath); 30 | if (result != FS_SUCCESS) { 31 | printf("Failed to extract directory %s with code %d\n", pIterator->pName, result); 32 | return result; 33 | } 34 | } else { 35 | fs_file* pFileI; 36 | fs_file* pFileO; 37 | 38 | printf("File: %s\n", pFullPath); 39 | 40 | /* TODO: Don't think we need this check if we're extracting to a temp folder. Remove this when we've got temp folders implemented. */ 41 | if (fs_info(pFS, pFullPath, FS_OPAQUE, NULL) == FS_SUCCESS) { 42 | printf("File %s already exists. Aborting.\n", pFullPath); 43 | return FS_SUCCESS; 44 | } 45 | 46 | result = fs_file_open(pFS, pFullPath, FS_TRUNCATE | FS_OPAQUE, &pFileO); 47 | if (result != FS_SUCCESS) { 48 | printf("Failed to open output file \"%s\" with code %d\n", pFullPath, result); 49 | return result; 50 | } 51 | 52 | 53 | result = fs_file_open(pArchive, pFullPath, FS_READ | FS_OPAQUE, &pFileI); 54 | if (result != FS_SUCCESS) { 55 | printf("Failed to open archived file \"%s\" with code %d\n", pFullPath, result); 56 | fs_file_close(pFileO); 57 | return result; 58 | } 59 | 60 | /* Now we just keep reading in chunk until we run out of data. */ 61 | for (;;) { 62 | char chunk[4096]; 63 | size_t bytesRead = 0; 64 | 65 | result = fs_file_read(pFileI, chunk, sizeof(chunk), &bytesRead); 66 | if (result != FS_SUCCESS) { 67 | break; 68 | } 69 | 70 | if (bytesRead == 0) { 71 | break; 72 | } 73 | 74 | result = fs_file_write(pFileO, chunk, bytesRead, NULL); 75 | if (result != FS_SUCCESS) { 76 | break; 77 | } 78 | } 79 | 80 | fs_file_close(pFileI); 81 | fs_file_close(pFileO); 82 | 83 | if (result != FS_SUCCESS && result != FS_AT_END) { 84 | printf("Failed to read file \"%s\" with code %d\n", pFullPath, result); 85 | return result; 86 | } 87 | } 88 | 89 | pIterator = fs_next(pIterator); 90 | } 91 | 92 | return FS_SUCCESS; 93 | } 94 | 95 | int extract(int argc, char** argv) 96 | { 97 | const char* pInputPath; 98 | const char* pOutputPath; 99 | fs_result result; 100 | fs* pFS; 101 | fs_config fsConfig; 102 | fs_archive_type pArchiveTypes[] = 103 | { 104 | {FS_ZIP, "zip"}, 105 | {FS_PAK, "pak"} 106 | }; 107 | fs* pArchive; 108 | 109 | if (argc < 2) { 110 | printf("No input file.\n"); 111 | return 1; 112 | } 113 | 114 | pInputPath = argv[1]; 115 | 116 | if (argc > 2) { 117 | pOutputPath = argv[2]; 118 | } else { 119 | pOutputPath = "."; 120 | } 121 | 122 | fsConfig = fs_config_init_default(); 123 | fsConfig.pArchiveTypes = pArchiveTypes; 124 | fsConfig.archiveTypeCount = sizeof(pArchiveTypes) / sizeof(pArchiveTypes[0]); 125 | 126 | result = fs_init(&fsConfig, &pFS); 127 | if (result != 0) { 128 | printf("Failed to initialize FS object with code %d\n", result); 129 | return 1; 130 | } 131 | 132 | /* Make sure the output directory exists. */ 133 | result = fs_mkdir(pFS, pOutputPath, FS_IGNORE_MOUNTS); 134 | if (result != FS_SUCCESS) { 135 | printf("Failed to create output directory \"%s\" with code %d\n", pOutputPath, result); 136 | fs_uninit(pFS); 137 | return 1; 138 | } 139 | 140 | /* TODO: Extract to a temp folder and then move to the output target. */ 141 | /*fs_mount(pFS, pTempPath, "NULL", FS_WRITE);*/ 142 | fs_mount(pFS, pOutputPath, NULL, FS_WRITE); 143 | 144 | 145 | 146 | result = fs_open_archive(pFS, pInputPath, FS_READ | FS_OPAQUE, &pArchive); 147 | if (result != FS_SUCCESS) { 148 | printf("Failed to open archive \"%s\" with code %d\n", pInputPath, result); 149 | fs_uninit(pFS); 150 | return 1; 151 | } 152 | 153 | 154 | 155 | result = extract_iterator(pFS, pArchive, fs_first(pArchive, "/", FS_OPAQUE), ""); 156 | if (result != FS_SUCCESS) { 157 | printf("Failed to extract archive with code %d\n", result); 158 | fs_uninit(pArchive); 159 | fs_uninit(pFS); 160 | return 1; 161 | } 162 | 163 | /* TODO: Use fs_rename() to move the files. Note that this does not look at mounts so we'll need to look at pTempPath and pOutputPath explicitly. */ 164 | /* TODO: Delete the temp folder. */ 165 | 166 | 167 | fs_close_archive(pArchive); 168 | fs_uninit(pFS); 169 | 170 | return 0; 171 | } 172 | 173 | int main(int argc, char** argv) 174 | { 175 | if (argc < 2) { 176 | print_help(); 177 | return 1; 178 | } 179 | 180 | if (strcmp(argv[1], "extract") == 0) { 181 | return extract(argc - 1, argv + 1); 182 | } else { 183 | print_help(); 184 | return 1; 185 | } 186 | 187 | 188 | return 0; 189 | } --------------------------------------------------------------------------------