From 44b847fbf819c18e97d00b53d37d3f96d26eebbc Mon Sep 17 00:00:00 2001 From: David Reid Date: Wed, 7 Jan 2026 18:10:39 +1000 Subject: [PATCH] Update fs. --- external/fs/fs.c | 805 +++++++++++++++++++++++++++++++++++++++-------- external/fs/fs.h | 301 +++++++++++++++--- 2 files changed, 934 insertions(+), 172 deletions(-) diff --git a/external/fs/fs.c b/external/fs/fs.c index d35c134d..2dfc1fc8 100644 --- a/external/fs/fs.c +++ b/external/fs/fs.c @@ -3,14 +3,6 @@ #include "fs.h" -/* BEG fs_platform_detection.c */ -#if defined(_WIN32) - #define FS_WIN32 -#else - #define FS_POSIX -#endif -/* END fs_platform_detection.c */ - #include /* BEG fs_common_macros.c */ @@ -19,6 +11,16 @@ #include #include +#if defined(__clang__) && defined(__has_attribute) + #if __has_attribute(suppress) + #define FS_SUPPRESS_CLANG_ANALYZER __attribute__((suppress)) + #else + #define FS_SUPPRESS_CLANG_ANALYZER + #endif +#else + #define FS_SUPPRESS_CLANG_ANALYZER +#endif + /* BEG fs_va_copy.c */ #ifndef fs_va_copy #if !defined(_MSC_VER) || _MSC_VER >= 1800 @@ -370,7 +372,7 @@ FS_API int fs_strnicmp(const char* str1, const char* str2, size_t count) /* BEG fs_result.c */ -FS_API const char* fs_result_to_string(fs_result result) +FS_API const char* fs_result_description(fs_result result) { switch (result) { @@ -831,72 +833,9 @@ code. These are the differences: Parameter ordering is the same as c89thread to make amalgamation easier. */ -/* BEG fs_thread_basic_types.c */ -#if defined(FS_POSIX) - #ifndef FS_USE_PTHREAD - #define FS_USE_PTHREAD - #endif - - #ifndef FS_NO_PTHREAD_IN_HEADER - #include - typedef pthread_t fs_pthread_t; - typedef pthread_mutex_t fs_pthread_mutex_t; - #else - typedef fs_uintptr fs_pthread_t; - typedef union fs_pthread_mutex_t { char __data[40]; fs_uint64 __alignment; } fs_pthread_mutex_t; - #endif -#endif -/* END fs_thread_basic_types.c */ - - -/* BEG fs_thread_mtx.h */ -#if defined(FS_WIN32) - typedef struct - { - void* handle; /* HANDLE, CreateMutex(), CreateEvent() */ - int type; - } fs_mtx; -#else - /* - We may need to force the use of a manual recursive mutex which will happen when compiling - on very old compilers, or with `-std=c89`. - */ - - /* If __STDC_VERSION__ is not defined it means we're compiling in C89 mode. */ - #if !defined(FS_USE_MANUAL_RECURSIVE_MUTEX) && !defined(__STDC_VERSION__) - #define FS_USE_MANUAL_RECURSIVE_MUTEX - #endif - - /* This is for checking if PTHREAD_MUTEX_RECURSIVE is available. */ - #if !defined(FS_USE_MANUAL_RECURSIVE_MUTEX) && (!defined(__USE_UNIX98) && !defined(__USE_XOPEN2K8)) - #define FS_USE_MANUAL_RECURSIVE_MUTEX - #endif - - #ifdef FS_USE_MANUAL_RECURSIVE_MUTEX - typedef struct - { - fs_pthread_mutex_t mutex; /* The underlying pthread mutex. */ - fs_pthread_mutex_t guard; /* Guard for metadata (owner and recursionCount). */ - fs_pthread_t owner; - int recursionCount; - int type; - } fs_mtx; - #else - typedef fs_pthread_mutex_t fs_mtx; - #endif -#endif - -enum -{ - fs_mtx_plain = 0x00000000, - fs_mtx_timed = 0x00000001, - fs_mtx_recursive = 0x00000002 -}; -/* END fs_thread_mtx.h */ - /* BEG fs_thread_mtx.c */ #if defined(FS_WIN32) && !defined(FS_USE_PTHREAD) -static int fs_mtx_init(fs_mtx* mutex, int type) +FS_API int fs_mtx_init(fs_mtx* mutex, int type) { HANDLE hMutex; @@ -929,7 +868,7 @@ static int fs_mtx_init(fs_mtx* mutex, int type) return FS_SUCCESS; } -static void fs_mtx_destroy(fs_mtx* mutex) +FS_API void fs_mtx_destroy(fs_mtx* mutex) { if (mutex == NULL) { return; @@ -938,7 +877,7 @@ static void fs_mtx_destroy(fs_mtx* mutex) CloseHandle((HANDLE)mutex->handle); } -static int fs_mtx_lock(fs_mtx* mutex) +FS_API int fs_mtx_lock(fs_mtx* mutex) { DWORD result; @@ -954,7 +893,7 @@ static int fs_mtx_lock(fs_mtx* mutex) return FS_SUCCESS; } -static int fs_mtx_unlock(fs_mtx* mutex) +FS_API int fs_mtx_unlock(fs_mtx* mutex) { BOOL result; @@ -975,7 +914,7 @@ static int fs_mtx_unlock(fs_mtx* mutex) return FS_SUCCESS; } #else -static int fs_mtx_init(fs_mtx* mutex, int type) +FS_API int fs_mtx_init(fs_mtx* mutex, int type) { int result; @@ -1029,7 +968,7 @@ static int fs_mtx_init(fs_mtx* mutex, int type) #endif } -static void fs_mtx_destroy(fs_mtx* mutex) +FS_API void fs_mtx_destroy(fs_mtx* mutex) { if (mutex == NULL) { return; @@ -1051,7 +990,7 @@ static void fs_mtx_destroy(fs_mtx* mutex) #endif } -static int fs_mtx_lock(fs_mtx* mutex) +FS_API int fs_mtx_lock(fs_mtx* mutex) { int result; @@ -1123,7 +1062,7 @@ static int fs_mtx_lock(fs_mtx* mutex) #endif } -static int fs_mtx_unlock(fs_mtx* mutex) +FS_API int fs_mtx_unlock(fs_mtx* mutex) { int result; @@ -1551,17 +1490,6 @@ static void fs_backend_uninit(const fs_backend* pBackend, fs* pFS) } } -static fs_result fs_backend_ioctl(const fs_backend* pBackend, fs* pFS, int command, void* pArgs) -{ - FS_ASSERT(pBackend != NULL); - - if (pBackend->ioctl == NULL) { - return FS_NOT_IMPLEMENTED; - } else { - return pBackend->ioctl(pFS, command, pArgs); - } -} - static fs_result fs_backend_remove(const fs_backend* pBackend, fs* pFS, const char* pFilePath) { FS_ASSERT(pBackend != NULL); @@ -2366,6 +2294,22 @@ static const char* fs_string_cstr(const fs_string* pString) return pString->stack; } +static char* fs_string_cstr_mutable(fs_string* pString) +{ + FS_ASSERT(pString != NULL); + FS_ASSERT(pString->ref == NULL); /* Can't get a mutable string from a reference string. */ + + if (pString->ref != NULL) { + return NULL; + } + + if (pString->heap != NULL) { + return pString->heap; + } + + return pString->stack; +} + static size_t fs_string_len(const fs_string* pString) { return pString->len; @@ -2670,6 +2614,10 @@ FS_API fs_result fs_init(const fs_config* pConfig, fs** ppFS) result = FS_SUCCESS; } + if (result != FS_SUCCESS) { + return result; + } + *ppFS = pFS; return FS_SUCCESS; } @@ -2719,15 +2667,6 @@ FS_API void fs_uninit(fs* pFS) fs_free(pFS, &pFS->allocationCallbacks); } -FS_API fs_result fs_ioctl(fs* pFS, int request, void* pArg) -{ - if (pFS == NULL) { - return FS_INVALID_ARGS; - } - - return fs_backend_ioctl(pFS->pBackend, pFS, request, pArg); -} - FS_API fs_result fs_remove(fs* pFS, const char* pFilePath, int options) { fs_result result; @@ -2800,7 +2739,7 @@ FS_API fs_result fs_rename(fs* pFS, const char* pOldPath, const char* pNewPath, pMountPointNew = fs_find_best_write_mount_point(pFS, pNewPath, options, &realNewPath); if (pMountPointNew == NULL) { - fs_string_free(&realNewPath, fs_get_allocation_callbacks(pFS)); + fs_string_free(&realOldPath, fs_get_allocation_callbacks(pFS)); return FS_DOES_NOT_EXIST; /* Couldn't find a mount point. */ } @@ -2897,7 +2836,6 @@ FS_API fs_result fs_mkdir(fs* pFS, const char* pPath, int options) } FS_COPY_MEMORY(pRunningPathHeap, pRunningPathStack, runningPathLen); - pRunningPath = pRunningPathHeap; } else { char* pNewRunningPathHeap; @@ -2908,8 +2846,10 @@ FS_API fs_result fs_mkdir(fs* pFS, const char* pPath, int options) return FS_OUT_OF_MEMORY; } - pRunningPath = pNewRunningPathHeap; + pRunningPathHeap = pNewRunningPathHeap; } + + pRunningPath = pRunningPathHeap; } FS_COPY_MEMORY(pRunningPath + runningPathLen, iSegment.pFullPath + iSegment.segmentOffset, iSegment.segmentLength); @@ -3973,7 +3913,13 @@ FS_API fs_result fs_file_duplicate(fs_file* pFile, fs_file** ppDuplicate) return result; } - return fs_backend_file_duplicate(fs_get_backend_or_default(fs_file_get_fs(pFile)), pFile, *ppDuplicate); + result = fs_backend_file_duplicate(fs_get_backend_or_default(fs_file_get_fs(pFile)), pFile, *ppDuplicate); + if (result != FS_SUCCESS) { + fs_file_free(ppDuplicate); + return result; + } + + return result; } FS_API void* fs_file_get_backend_data(fs_file* pFile) @@ -5324,6 +5270,609 @@ FS_API fs_result fs_file_open_and_write(fs* pFS, const char* pFilePath, const vo } +/* "FSSRLZ1\0" */ +#define FS_SERIALIZED_SIG_0 0x52535346 +#define FS_SERIALIZED_SIG_1 0x00315A4C + +static fs_result fs_serialize_directory(fs* pFS, const char* pDirectoryPath, const char* pBasePath, int options, fs_stream* pOutputStream, fs_stream* pTOCStream, fs_uint32* pTOCEntryCount, fs_uint64* pRunningFileOffset) +{ + fs_result result; + fs_iterator* pIterator; + char padding[8] = { 0, 0, 0, 0, 0, 0, 0, 0 }; + + FS_ASSERT(pTOCEntryCount != NULL); + + for (pIterator = fs_first(pFS, pDirectoryPath, options); pIterator != NULL; pIterator = fs_next(pIterator)) { + fs_string path = fs_string_new(); + int pathLen; + const char* pTrimmedPath; + size_t trimmedPathLen; + fs_uint32 fileFlags; + fs_uint64 fileSize; + fs_uint64 fileOffset; + + *pTOCEntryCount += 1; + + /* Flags. */ + fileFlags = 0; + if (pIterator->info.directory) { + fileFlags |= 0x1; /* Directory. */ + } + + result = fs_stream_write(pTOCStream, &fileFlags, 4, NULL); + if (result != FS_SUCCESS) { + return result; + } + + + /* Construct the full path. It's the full path, trimmed with the base path, that we store in the TOC. */ + pathLen = fs_path_append(path.stack, sizeof(path.stack), pDirectoryPath, FS_NULL_TERMINATED, pIterator->pName, pIterator->nameLen); + if (pathLen < 0) { + return FS_ERROR; + } + + path.len = (size_t)pathLen; + + if (path.len > sizeof(path.stack)) { + result = fs_string_alloc(pathLen, fs_get_allocation_callbacks(pFS), &path); + if (result != FS_SUCCESS) { + return result; + } + + fs_path_append(path.heap, path.len + 1, pDirectoryPath, FS_NULL_TERMINATED, pIterator->pName, pIterator->nameLen); + } + + pTrimmedPath = fs_path_trim_base(fs_string_cstr(&path), fs_string_len(&path), pBasePath, FS_NULL_TERMINATED); + if (pTrimmedPath == NULL) { + fs_string_free(&path, fs_get_allocation_callbacks(pFS)); + return FS_ERROR; + } + + trimmedPathLen = strlen(pTrimmedPath); + + /* Path Length. */ + result = fs_stream_write(pTOCStream, &trimmedPathLen, 4, NULL); + if (result != FS_SUCCESS) { + fs_string_free(&path, fs_get_allocation_callbacks(pFS)); + return result; + } + + /* Path. */ + result = fs_stream_write(pTOCStream, pTrimmedPath, trimmedPathLen, NULL); + if (result != FS_SUCCESS) { + fs_string_free(&path, fs_get_allocation_callbacks(pFS)); + return result; + } + + /* Null Terminator. */ + result = fs_stream_write(pTOCStream, "\0", 1, NULL); + if (result != FS_SUCCESS) { + fs_string_free(&path, fs_get_allocation_callbacks(pFS)); + return result; + } + + /* Padding. */ + result = fs_stream_write(pTOCStream, padding, FS_ALIGN(trimmedPathLen + 1, 8) - (trimmedPathLen + 1), NULL); + if (result != FS_SUCCESS) { + fs_string_free(&path, fs_get_allocation_callbacks(pFS)); + return result; + } + + + if (pIterator->info.directory) { + fileOffset = 0; + fileSize = 0; + } else { + fileOffset = *pRunningFileOffset; + fileSize = 0; + + /* Open the file and transfer the data to the stream. */ + { + fs_file* pFile; + char buffer[4096]; + size_t bytesRead; + + result = fs_file_open(pFS, fs_string_cstr(&path), FS_READ | options, &pFile); + if (result != FS_SUCCESS) { + fs_string_free(&path, fs_get_allocation_callbacks(pFS)); + return result; + } + + for (;;) { + result = fs_file_read(pFile, buffer, sizeof(buffer), &bytesRead); + if (result != FS_SUCCESS && result != FS_AT_END) { + fs_file_close(pFile); + fs_string_free(&path, fs_get_allocation_callbacks(pFS)); + return result; + } + + if (bytesRead == 0) { + break; + } + + result = fs_stream_write(pOutputStream, buffer, bytesRead, NULL); + if (result != FS_SUCCESS) { + fs_file_close(pFile); + fs_string_free(&path, fs_get_allocation_callbacks(pFS)); + return result; + } + + fileSize += bytesRead; + + if (result == FS_AT_END) { + break; + } + } + + fs_file_close(pFile); + pFile = NULL; + } + *pRunningFileOffset += fileSize; + + /* Add padding zeros to align the data to 8 bytes. */ + result = fs_stream_write(pOutputStream, padding, FS_ALIGN(*pRunningFileOffset, 8) - *pRunningFileOffset, NULL); + if (result != FS_SUCCESS) { + fs_string_free(&path, fs_get_allocation_callbacks(pFS)); + return result; + } + *pRunningFileOffset = FS_ALIGN(*pRunningFileOffset, 8); + } + + + /* File Size. */ + result = fs_stream_write(pTOCStream, &fileSize, 8, NULL); + if (result != FS_SUCCESS) { + fs_string_free(&path, fs_get_allocation_callbacks(pFS)); + return result; + } + + /* File Offset. */ + result = fs_stream_write(pTOCStream, &fileOffset, 8, NULL); + if (result != FS_SUCCESS) { + fs_string_free(&path, fs_get_allocation_callbacks(pFS)); + return result; + } + + + /* If it was a directory we need to call this function recursively. */ + if (pIterator->info.directory) { + result = fs_serialize_directory(pFS, fs_string_cstr(&path), pBasePath, options, pOutputStream, pTOCStream, pTOCEntryCount, pRunningFileOffset); + if (result != FS_SUCCESS) { + fs_string_free(&path, fs_get_allocation_callbacks(pFS)); + return result; + } + } + + + /* The full path is no longer needed. */ + fs_string_free(&path, fs_get_allocation_callbacks(pFS)); + } + + return FS_SUCCESS; +} + +FS_API fs_result fs_serialize(fs* pFS, const char* pDirectoryPath, int options, fs_stream* pOutputStream) +{ + fs_result result; + fs_uint32 sig[2] = { FS_SERIALIZED_SIG_0, FS_SERIALIZED_SIG_1 }; + fs_memory_stream toc; + fs_uint32 tocEntryCount = 0; /* <-- Must be initialized to zero. */ + fs_uint64 runningOffset = 0; /* <-- Must be initialized to zero. */ + fs_uint64 tocOffset; + fs_int64 initialPos; + + if (pOutputStream == NULL) { + return FS_INVALID_ARGS; + } + + if (pDirectoryPath == NULL) { + pDirectoryPath = ""; + } + + + /* Initialize the TOC stream. We fill out the TOC stream separately, and then at the end we append it to the end of the main stream. */ + result = fs_memory_stream_init_write(fs_get_allocation_callbacks(pFS), &toc); + if (result != FS_SUCCESS) { + return result; + } + + + /* + We want our data to be aligned to 8 bytes, so we'll grab the initial position of the stream and write some padding + to get our initial aligment set up. + */ + result = fs_stream_tell(pOutputStream, &initialPos); + if (result != FS_SUCCESS) { + fs_memory_stream_uninit(&toc); + return result; + } + + result = fs_stream_write(pOutputStream, "\0\0\0\0\0\0\0\0", FS_ALIGN(initialPos, 8) - initialPos, NULL); + if (result != FS_SUCCESS) { + fs_memory_stream_uninit(&toc); + return result; + } + + + /* File Data. */ + result = fs_serialize_directory(pFS, pDirectoryPath, pDirectoryPath, options, pOutputStream, (fs_stream*)&toc, &tocEntryCount, &runningOffset); + if (result != FS_SUCCESS) { + fs_memory_stream_uninit(&toc); + return result; + } + + /* TOC. */ + { + void* pTOCData; + size_t tocDataSize; + + /* Grab the TOC offset for output later. */ + tocOffset = runningOffset; + + pTOCData = fs_memory_stream_take_ownership(&toc, &tocDataSize); /* <-- Should never fail. */ + FS_ASSERT(pTOCData != NULL || tocDataSize == 0); + + fs_memory_stream_uninit(&toc); + + result = fs_stream_write(pOutputStream, pTOCData, tocDataSize, NULL); + fs_free(pTOCData, fs_get_allocation_callbacks(pFS)); + + if (result != FS_SUCCESS) { + fs_memory_stream_uninit(&toc); + return result; + } + + runningOffset += tocDataSize; + } + + /* Tail. */ + { + char padding[8] = { 0, 0, 0, 0, 0, 0, 0, 0 }; + fs_int64 baseOffset; + fs_uint32 reserved; + + /* Padding to align to 8 bytes. */ + result = fs_stream_write(pOutputStream, padding, FS_ALIGN(runningOffset, 8) - runningOffset, NULL); + if (result != FS_SUCCESS) { + return result; + } + runningOffset = FS_ALIGN(runningOffset, 8); + + + /* + We'll only be outputting 32 more bytes at this point: + + Signature (8 bytes) + Base Offset (8 bytes) + TOC Offset (8 bytes) + TOC Entry Count (4 bytes) + Reserved (4 bytes) + */ + baseOffset = -(fs_int64)(runningOffset + 32); + + /* Signature. */ + result = fs_stream_write(pOutputStream, sig, 8, NULL); + if (result != FS_SUCCESS) { + return result; + } + + /* Base Offset. */ + result = fs_stream_write(pOutputStream, &baseOffset, 8, NULL); + if (result != FS_SUCCESS) { + return result; + } + + /* TOC Offset. */ + result = fs_stream_write(pOutputStream, &tocOffset, 8, NULL); + if (result != FS_SUCCESS) { + return result; + } + + /* TOC Entry Count. */ + result = fs_stream_write(pOutputStream, &tocEntryCount, 4, NULL); + if (result != FS_SUCCESS) { + return result; + } + + /* Reserved. */ + reserved = 0; + result = fs_stream_write(pOutputStream, &reserved, 4, NULL); + if (result != FS_SUCCESS) { + return result; + } + } + + return FS_SUCCESS; +} + + + +static fs_result fs_stream_read_u32_le(fs_stream* pStream, fs_uint32* pResult) +{ + fs_result result; + fs_uint8 bytes[4]; + + result = fs_stream_read(pStream, bytes, 4, NULL); + if (result != FS_SUCCESS) { + return result; + } + + *pResult = ((fs_uint32)bytes[0] << 0) | ((fs_uint32)bytes[1] << 8) | ((fs_uint32)bytes[2] << 16) | ((fs_uint32)bytes[3] << 24); + + return FS_SUCCESS; +} + +static fs_result fs_stream_read_u64_le(fs_stream* pStream, fs_uint64* pResult) +{ + fs_result result; + fs_uint8 bytes[8]; + + result = fs_stream_read(pStream, bytes, 8, NULL); + if (result != FS_SUCCESS) { + return result; + } + + *pResult = + ((fs_uint64)bytes[0] << 0) | ((fs_uint64)bytes[1] << 8) | ((fs_uint64)bytes[2] << 16) | ((fs_uint64)bytes[3] << 24) | + ((fs_uint64)bytes[4] << 32) | ((fs_uint64)bytes[5] << 40) | ((fs_uint64)bytes[6] << 48) | ((fs_uint64)bytes[7] << 56); + + return FS_SUCCESS; +} + +static fs_result fs_stream_read_s64_le(fs_stream* pStream, fs_int64* pResult) +{ + fs_result result; + fs_uint64 temp; + + result = fs_stream_read_u64_le(pStream, &temp); + if (result != FS_SUCCESS) { + return result; + } + + *pResult = (fs_int64)temp; + + return FS_SUCCESS; +} + +FS_API fs_result fs_deserialize(fs* pFS, const char* pDirectoryPath, int options, fs_stream* pInputStream) +{ + fs_result result; + fs_uint32 sig[2]; + fs_int64 baseOffset; + fs_uint64 tocOffset; + fs_uint32 tocEntryCount; + fs_uint32 reserved; + fs_uint32 iEntry; + + if (pInputStream == NULL) { + return FS_INVALID_ARGS; + } + + if (pDirectoryPath == NULL) { + pDirectoryPath = ""; + } + + /* First thing we need to do is read the tail. */ + result = fs_stream_seek(pInputStream, -32, FS_SEEK_END); + if (result != FS_SUCCESS) { + return result; /* Failed to seek to the tail. Either too small, or the stream does not support seeking from the end. */ + } + + /* Signature. */ + result = fs_stream_read_u32_le(pInputStream, &sig[0]); + if (result != FS_SUCCESS) { + return result; + } + + result = fs_stream_read_u32_le(pInputStream, &sig[1]); + if (result != FS_SUCCESS) { + return result; + } + + if (sig[0] != FS_SERIALIZED_SIG_0 || sig[1] != FS_SERIALIZED_SIG_1) { + return FS_INVALID_DATA; /* Not a serialized stream. */ + } + + /* Base Offset (relative to end). */ + result = fs_stream_read_s64_le(pInputStream, &baseOffset); + if (result != FS_SUCCESS) { + return result; + } + + /* TOC Offset (local, relative to base). */ + result = fs_stream_read_u64_le(pInputStream, &tocOffset); + if (result != FS_SUCCESS) { + return result; + } + + /* TOC Entry Count. */ + result = fs_stream_read_u32_le(pInputStream, &tocEntryCount); + if (result != FS_SUCCESS) { + return result; + } + + /* Reserved (set to zero). */ + result = fs_stream_read_u32_le(pInputStream, &reserved); + if (result != FS_SUCCESS) { + return result; + } + + /* Now we can seek to the TOC. */ + result = fs_stream_seek(pInputStream, baseOffset + (fs_int64)tocOffset, FS_SEEK_END); + if (result != FS_SUCCESS) { + return result; + } + + /* To restore the files we just iterate over the TOC and read. */ + for (iEntry = 0; iEntry < tocEntryCount; iEntry += 1) { + fs_uint32 flags; + fs_uint32 localPathLen; + fs_string localPath; + fs_string fullPath; + int fullPathLen; + fs_uint64 fileSize; + fs_uint64 fileOffset; + fs_int64 currentOffset; + fs_file* pFile = NULL; + + /* Flags. */ + result = fs_stream_read_u32_le(pInputStream, &flags); + if (result != FS_SUCCESS) { + return result; + } + + /* Path Length. */ + result = fs_stream_read_u32_le(pInputStream, &localPathLen); + if (result != FS_SUCCESS) { + return result; + } + + /* Path. */ + result = fs_string_alloc(localPathLen + 1, fs_get_allocation_callbacks(pFS), &localPath); + if (result != FS_SUCCESS) { + return result; + } + + result = fs_stream_read(pInputStream, fs_string_cstr_mutable(&localPath), localPathLen + 1, NULL); /* +1 to read the null terminator in the same call. */ + if (result != FS_SUCCESS) { + fs_string_free(&localPath, fs_get_allocation_callbacks(pFS)); + return result; + } + + if (fs_string_cstr(&localPath)[localPathLen] != '\0') { + fs_string_free(&localPath, fs_get_allocation_callbacks(pFS)); + return FS_INVALID_DATA; /* Path is not null terminated. */ + } + + localPath.len = (size_t)localPathLen; + + fullPath = fs_string_new(); + fullPathLen = fs_path_append(fullPath.stack, sizeof(fullPath.stack), pDirectoryPath, FS_NULL_TERMINATED, fs_string_cstr(&localPath), fs_string_len(&localPath)); + if (fullPathLen < 0) { + fs_string_free(&localPath, fs_get_allocation_callbacks(pFS)); + return FS_ERROR; + } + + fullPath.len = (size_t)fullPathLen; + + if (fullPath.len > sizeof(fullPath.stack)) { + result = fs_string_alloc(fullPathLen, fs_get_allocation_callbacks(pFS), &fullPath); + if (result != FS_SUCCESS) { + fs_string_free(&localPath, fs_get_allocation_callbacks(pFS)); + return result; + } + + fs_path_append(fullPath.heap, fullPath.len + 1, pDirectoryPath, FS_NULL_TERMINATED, fs_string_cstr(&localPath), fs_string_len(&localPath)); + } + + fs_string_free(&localPath, fs_get_allocation_callbacks(pFS)); + + + /* Padding. */ + result = fs_stream_seek(pInputStream, FS_ALIGN(localPathLen + 1, 8) - (localPathLen + 1), FS_SEEK_CUR); + if (result != FS_SUCCESS) { + fs_string_free(&fullPath, fs_get_allocation_callbacks(pFS)); + return result; + } + + /* File Size. */ + result = fs_stream_read_u64_le(pInputStream, &fileSize); + if (result != FS_SUCCESS) { + fs_string_free(&fullPath, fs_get_allocation_callbacks(pFS)); + return result; + } + + /* File Offset. */ + result = fs_stream_read_u64_le(pInputStream, &fileOffset); + if (result != FS_SUCCESS) { + fs_string_free(&fullPath, fs_get_allocation_callbacks(pFS)); + return result; + } + + + /* We've now read the TOC entry. We now have enough information to extract the data. */ + result = fs_stream_tell(pInputStream, ¤tOffset); + if (result != FS_SUCCESS) { + fs_string_free(&fullPath, fs_get_allocation_callbacks(pFS)); + return result; + } + + if ((flags & 0x1) != 0) { + /* Directory. */ + result = fs_mkdir(pFS, fs_string_cstr(&fullPath), options); + fs_string_free(&fullPath, fs_get_allocation_callbacks(pFS)); + + if (result != FS_SUCCESS && result != FS_ALREADY_EXISTS) { + return result; + } + } else { + /* File. */ + fs_uint64 bytesRemaining; + + result = fs_file_open(pFS, fs_string_cstr(&fullPath), FS_WRITE | FS_TRUNCATE | options, &pFile); + fs_string_free(&fullPath, fs_get_allocation_callbacks(pFS)); + + if (result != FS_SUCCESS) { + return result; + } + + /* Seek to the file data using base offset + local offset. */ + result = fs_stream_seek(pInputStream, baseOffset + (fs_int64)fileOffset, FS_SEEK_END); + if (result != FS_SUCCESS) { + fs_file_close(pFile); + return result; + } + + /* Copy the data across. */ + bytesRemaining = fileSize; + while (bytesRemaining > 0) { + char buffer[4096]; + size_t bytesRead; + fs_uint64 bytesToRead; + + bytesToRead = bytesRemaining; + if (bytesToRead > sizeof(buffer)) { + bytesToRead = sizeof(buffer); + } + + result = fs_stream_read(pInputStream, buffer, (size_t)bytesToRead, &bytesRead); /* Safe cast to size_t because it's clamped to the capacity of `buffer`. */ + if (result != FS_SUCCESS && result != FS_AT_END) { + fs_file_close(pFile); + return result; + } + + if (bytesRead == 0) { + break; + } + + result = fs_file_write(pFile, buffer, bytesRead, NULL); + if (result != FS_SUCCESS) { + fs_file_close(pFile); + return result; + } + + bytesRemaining -= bytesRead; + + if (result == FS_AT_END) { + break; + } + } + + fs_file_close(pFile); + pFile = NULL; + } + + /* We need to seek back to where we were in the TOC so we can continue processing. */ + result = fs_stream_seek(pInputStream, currentOffset, FS_SEEK_SET); + if (result != FS_SUCCESS) { + return result; + } + } + + return FS_SUCCESS; +} + + /* BEG fs_backend_posix.c */ #if !defined(_WIN32) /* <-- Add any platforms that lack POSIX support here. */ #define FS_SUPPORTS_POSIX @@ -5369,15 +5918,6 @@ static void fs_uninit_posix(fs* pFS) (void)pFS; } -static fs_result fs_ioctl_posix(fs* pFS, int op, void* pArg) -{ - (void)pFS; - (void)op; - (void)pArg; - - return FS_NOT_IMPLEMENTED; -} - static fs_result fs_remove_posix(fs* pFS, const char* pFilePath) { int result = remove(pFilePath); @@ -5933,7 +6473,6 @@ static fs_backend fs_posix_backend = fs_alloc_size_posix, fs_init_posix, fs_uninit_posix, - fs_ioctl_posix, fs_remove_posix, fs_rename_posix, fs_mkdir_posix, @@ -6190,15 +6729,6 @@ static void fs_uninit_win32(fs* pFS) (void)pFS; } -static fs_result fs_ioctl_win32(fs* pFS, int op, void* pArg) -{ - (void)pFS; - (void)op; - (void)pArg; - - return FS_NOT_IMPLEMENTED; -} - static fs_result fs_remove_win32(fs* pFS, const char* pFilePath) { BOOL resultWin32; @@ -6841,7 +7371,6 @@ static fs_backend fs_win32_backend = fs_alloc_size_win32, fs_init_win32, fs_uninit_win32, - fs_ioctl_win32, fs_remove_win32, fs_rename_win32, fs_mkdir_win32, @@ -8402,7 +8931,8 @@ FS_API fs_result fs_memory_stream_read(fs_memory_stream* pStream, void* pDst, si FS_API fs_result fs_memory_stream_write(fs_memory_stream* pStream, const void* pSrc, size_t bytesToWrite, size_t* pBytesWritten) { - size_t newSize; + size_t writeEndPosition; + size_t newDataSize; if (pBytesWritten != NULL) { *pBytesWritten = 0; @@ -8417,13 +8947,13 @@ FS_API fs_result fs_memory_stream_write(fs_memory_stream* pStream, const void* p return FS_INVALID_OPERATION; } - newSize = *pStream->pDataSize + bytesToWrite; - if (newSize > pStream->write.dataCap) { - /* Need to resize. */ + /* Calculate where the write will end and resize if necessary. */ + writeEndPosition = pStream->cursor + bytesToWrite; + if (writeEndPosition > pStream->write.dataCap) { void* pNewBuffer; size_t newCap; - newCap = FS_MAX(newSize, pStream->write.dataCap * 2); + newCap = FS_MAX(writeEndPosition, pStream->write.dataCap * 2); pNewBuffer = fs_realloc(*pStream->ppData, newCap, &pStream->allocationCallbacks); if (pNewBuffer == NULL) { return FS_OUT_OF_MEMORY; @@ -8433,10 +8963,15 @@ FS_API fs_result fs_memory_stream_write(fs_memory_stream* pStream, const void* p pStream->write.dataCap = newCap; } - FS_ASSERT(newSize <= pStream->write.dataCap); + FS_ASSERT(writeEndPosition <= pStream->write.dataCap); - FS_COPY_MEMORY(FS_OFFSET_PTR(*pStream->ppData, *pStream->pDataSize), pSrc, bytesToWrite); - *pStream->pDataSize = newSize; + /* Write out the data, starting from our cursor. */ + FS_COPY_MEMORY(FS_OFFSET_PTR(*pStream->ppData, pStream->cursor), pSrc, bytesToWrite); + pStream->cursor = writeEndPosition; + + /* Update the data size if we wrote beyond the current end. */ + newDataSize = FS_MAX(*pStream->pDataSize, writeEndPosition); + *pStream->pDataSize = newDataSize; if (pBytesWritten != NULL) { *pBytesWritten = bytesToWrite; /* We always write all or nothing here. */ @@ -8457,8 +8992,6 @@ FS_API fs_result fs_memory_stream_seek(fs_memory_stream* pStream, fs_int64 offse return FS_INVALID_ARGS; /* Trying to seek too far. This will never happen on 64-bit builds. */ } - newCursor = pStream->cursor; - if (origin == FS_SEEK_SET) { newCursor = 0; } else if (origin == FS_SEEK_CUR) { @@ -8830,7 +9363,7 @@ static FS_ASAN fs_uint32 fs_strlen_limited(char const* s, fs_uint32 limit) return (fs_uint32)(sn - s); } -FS_API_SPRINTF_DEF int fs_vsprintfcb(fs_sprintf_callback* callback, void* user, char* buf, char const* fmt, va_list va) +FS_SUPPRESS_CLANG_ANALYZER FS_API_SPRINTF_DEF int fs_vsprintfcb(fs_sprintf_callback* callback, void* user, char* buf, char const* fmt, va_list va) { static char hex[] = "0123456789abcdefxp"; static char hexu[] = "0123456789ABCDEFXP"; @@ -9883,7 +10416,7 @@ static char* fs_clamp_callback(const char* buf, void* user, size_t len) len = c->count; if (len) { - if (buf != c->buf) { + if (buf != c->buf && buf != NULL) { const char* s, *se; char* d; d = c->buf; diff --git a/external/fs/fs.h b/external/fs/fs.h index 03dea67f..3d961a67 100644 --- a/external/fs/fs.h +++ b/external/fs/fs.h @@ -740,12 +740,6 @@ init uninit This is where you should do any cleanup. Do not close the stream here. -ioctl - This function is optional. You can use this to implement custom IO control commands. Return - `FS_INVALID_OPERATION` if the command is not recognized. The format of the `pArg` parameter is - command specific. If the backend does not need to implement this function, it can be left as - `NULL` or return `FS_NOT_IMPLEMENTED`. - remove This function is used to delete a file or directory. This is not recursive. If the path is a directory, the backend should return an error if it is not empty. Backends do not need to @@ -939,6 +933,14 @@ see some random tags and stuff in this file. These are just used for doing a dum extern "C" { #endif +/* BEG fs_platform_detection.c */ +#if defined(_WIN32) + #define FS_WIN32 +#else + #define FS_POSIX +#endif +/* END fs_platform_detection.c */ + /* BEG fs_compiler_compat.h */ #include /* For size_t. */ #include /* For va_list. */ @@ -1060,6 +1062,75 @@ typedef unsigned int fs_bool32; /* END fs_compiler_compat.h */ +/* BEG fs_thread_basic_types.h */ +#if defined(FS_POSIX) + #ifndef FS_USE_PTHREAD + #define FS_USE_PTHREAD + #endif + + #ifndef FS_NO_PTHREAD_IN_HEADER + #include + typedef pthread_t fs_pthread_t; + typedef pthread_mutex_t fs_pthread_mutex_t; + #else + typedef fs_uintptr fs_pthread_t; + typedef union fs_pthread_mutex_t { char __data[40]; fs_uint64 __alignment; } fs_pthread_mutex_t; + #endif +#endif +/* END fs_thread_basic_types.h */ + + +/* BEG fs_thread_mtx.h */ +#if defined(FS_WIN32) + typedef struct + { + void* handle; /* HANDLE, CreateMutex(), CreateEvent() */ + int type; + } fs_mtx; +#else + /* + We may need to force the use of a manual recursive mutex which will happen when compiling + on very old compilers, or with `-std=c89`. + */ + + /* If __STDC_VERSION__ is not defined it means we're compiling in C89 mode. */ + #if !defined(FS_USE_MANUAL_RECURSIVE_MUTEX) && !defined(__STDC_VERSION__) + #define FS_USE_MANUAL_RECURSIVE_MUTEX + #endif + + /* This is for checking if PTHREAD_MUTEX_RECURSIVE is available. */ + #if !defined(FS_USE_MANUAL_RECURSIVE_MUTEX) && (!defined(__USE_UNIX98) && !defined(__USE_XOPEN2K8)) + #define FS_USE_MANUAL_RECURSIVE_MUTEX + #endif + + #ifdef FS_USE_MANUAL_RECURSIVE_MUTEX + typedef struct + { + fs_pthread_mutex_t mutex; /* The underlying pthread mutex. */ + fs_pthread_mutex_t guard; /* Guard for metadata (owner and recursionCount). */ + fs_pthread_t owner; + int recursionCount; + int type; + } fs_mtx; + #else + typedef fs_pthread_mutex_t fs_mtx; + #endif +#endif + +enum +{ + fs_mtx_plain = 0x00000000, + fs_mtx_timed = 0x00000001, + fs_mtx_recursive = 0x00000002 +}; + +FS_API int fs_mtx_init(fs_mtx* mutex, int type); +FS_API void fs_mtx_destroy(fs_mtx* mutex); +FS_API int fs_mtx_lock(fs_mtx* mutex); +FS_API int fs_mtx_unlock(fs_mtx* mutex); +/* END fs_thread_mtx.h */ + + /* BEG fs_result.h */ typedef enum { @@ -1125,7 +1196,7 @@ typedef enum FS_HAS_MORE_OUTPUT = 102 /* Some stream has more output data to be read, but there's not enough room in the output buffer. */ } fs_result; -FS_API const char* fs_result_to_string(fs_result result); +FS_API const char* fs_result_description(fs_result result); /* END fs_result.h */ @@ -1160,6 +1231,7 @@ The stream vtable can support both reading and writing, but it doesn't need to s the same time. If one is not supported, simply leave the relevant `read` or `write` callback as `NULL`, or have them return FS_NOT_IMPLEMENTED. */ + typedef enum fs_seek_origin { FS_SEEK_SET = 0, @@ -1229,6 +1301,7 @@ appended to the end of the data. For flexiblity in case the backend does not support cursor retrieval or positioning, the data will be read in fixed sized chunks. */ + typedef enum fs_format { FS_FORMAT_TEXT, @@ -1443,6 +1516,42 @@ struct fs_iterator fs_file_info info; }; + +/* +Configuration structure for fs objects. + +Members +------- +pBackend + The backend to use. If NULL, the standard file system backend will be used. + +pBackendConfig + A pointer to a backend-specific configuration structure. This is passed directly to the + backend's init function. The documentation for your backend will tell you how to use this. Most + backends will allow you to set this to NULL. + +pStream + A stream to use for archive backends. This is required for any file-backed backend, such as ZIP + archives. If the backend does not need a stream, this can be NULL. + +pArchiveTypes + An array of archive types to register. This can be NULL if no archive types are to be + registered. Archive types are mapped to file extensions. See `fs_archive_type` for more + information. + +archiveTypeCount + The number of archive types in the `pArchiveTypes` array. Set this to 0 if `pArchiveTypes` is + NULL. + +onRefCountChanged + A callback that is fired when the reference count of a fs object changes. + +pRefCountChangedUserData + User data that is passed to the `onRefCountChanged` callback. + +pAllocationCallbacks + Custom allocation callbacks. If NULL, the standard malloc/realloc/free functions will be used. +*/ struct fs_config { const fs_backend* pBackend; @@ -1464,7 +1573,6 @@ struct fs_backend size_t (* alloc_size )(const void* pBackendConfig); fs_result (* init )(fs* pFS, const void* pBackendConfig, fs_stream* pStream); /* Return 0 on success or an errno result code on error. pBackendConfig is a pointer to a backend-specific struct. The documentation for your backend will tell you how to use this. You can usually pass in NULL for this. */ void (* uninit )(fs* pFS); - fs_result (* ioctl )(fs* pFS, int op, void* pArg); /* Optional. */ fs_result (* remove )(fs* pFS, const char* pFilePath); fs_result (* rename )(fs* pFS, const char* pOldPath, const char* pNewPath); /* Return FS_DIFFERENT_DEVICE if the old and new paths are on different devices and would require a copy. */ fs_result (* mkdir )(fs* pFS, const char* pPath); /* This is not recursive. Return FS_ALREADY_EXISTS if directory already exists. Return FS_DOES_NOT_EXIST if a parent directory does not exist. */ @@ -1661,34 +1769,6 @@ fs_init() FS_API void fs_uninit(fs* pFS); -/* -Performs a control operation on the file system. - -This is backend-specific. Check the documentation for the backend you are using to see what -operations are supported. - - -Parameters ----------- -pFS : (in) - A pointer to the file system object. Must not be NULL. - -op : (in) - The operation to perform. This is backend-specific. - -pArg : (in, optional) - An optional pointer to an argument struct. This is backend-specific. Can be NULL if the - operation does not require any arguments. - - -Return Value ------------- -Returns FS_SUCCESS on success; any other result code otherwise. May return FS_NOT_IMPLEMENTED if -the operation is not supported by the backend. -*/ -FS_API fs_result fs_ioctl(fs* pFS, int op, void* pArg); - - /* Removes a file or empty directory. @@ -3291,6 +3371,154 @@ FS_API fs_result fs_file_open_and_read(fs* pFS, const char* pFilePath, fs_format FS_API fs_result fs_file_open_and_write(fs* pFS, const char* pFilePath, const void* pData, size_t dataSize); +/* +Serializes a file system subdirectory to a stream. + +This function recursively serializes all files and directories within the specified directory +to a binary stream. The serialized data can later be restored using `fs_deserialize()`. + +The directory parameter specifies which directory to serialize. This function is built on top of +standard file iteration functions, i.e. `fs_first()`, `fs_next()`. The directory and options all +work the same way as they do for `fs_first()`. + +It is possible for serialization to fail partway through, in which case the stream will contain +partial data. It is the caller's responsibility to handle this case appropriately. + +When calculating offsets, it will use `fs_stream_tell()` to get the current position in the stream. +Keep this in mind if you are appending this to the end of an existing stream. This may or may not +be useful to you depending on your use case. + +The format is designed to be appendable to existing payloads using tools like `cat`. Offsets are +stored relative to the end of the archive to support this use case. The Base Offset field in the +tail specifies where file data begins relative to the end of the stream, and will always be a +negative number. Each file's offset is then relative to this base offset. To seek to a file's data, +you would add the file's offset to the base offset and then seek by that amount relative to the end +of the stream. + +Below is the format: + + |: MAIN STRUCTURE :| + |------------------------------------------------| + | n | File Data | + |------|-----------------------------------------| + | n | TOC Entries | + |------|-----------------------------------------| + | 8 | 'FSSRLZ1\0' | + | 8 | Base Offset (negative, relative to end) | + | 8 | TOC Offset (relative to Base Offset) | + | 4 | TOC Entry Count | + | 4 | Reserved (set to zero) | + + + |: TOC ENTRY :| + |------------------------------------------------| + | 4 | File Flags | + | 4 | File Path Length | + | n | File Path | + | 1 | '\0' (Null Terminator) | + | n | Padding to 8-byte alignment | + | 8 | File Size | + | 8 | File Offset (local, relative) | + + + |: FILE FLAGS :| + |------------------------------------------------| + | 0x1 | Directory | + + + Notes: + - The signature is located at the end of the stream. To parse, seek to the last 32 bytes, read + the signature, base offset, TOC offset and entry count, then calculate the absolute TOC offset + as Base Offset + TOC Offset and seek to that position, relative to the end, to read the TOC. + The base offset is always negative. + - Directories should be explicitly listed in the TOC, and also have the "Directory" flag set. + - Directories must be listed before any files that are contained within them. + - File Offset and TOC Offset are both local offsets relative to the Base Offset position. Sum + the offsets with the base offset, and then seek relative to the end. + - The Base Offset is relative to the end of the archive (negative value). + - File data is aligned to 8-byte boundaries. + - All multi-byte values are little-endian. + - File paths are stored using UTF-8 encoding. + - To calculate the padding between the null terminator and the next 8-byte aligned offset, round + the length of the file path plus one (for the null terminator) up to the next multiple of 8, + then subtract the length of the file path plus one. + + +Parameters +---------- +pFS : (in) + A pointer to the file system object. Must not be NULL. + +pDirectoryPath : (in, optional) + The path to the directory to serialize. + +options : (in) + Options for the serialization operation. These are passed in directly to `fs_first()` and + therefore have the same meaning. See `fs_first()` for details. These will also be passed into + `fs_file_open()` when opening files, combined with `FS_READ`. + +pOutputStream : (in) + A pointer to the output stream where the serialized data will be written. Must not be NULL. + + +Return Value +------------ +Returns FS_SUCCESS on success; any other result code otherwise. + + +See Also +-------- +fs_deserialize() +fs_first() +*/ +FS_API fs_result fs_serialize(fs* pFS, const char* pDirectoryPath, int options, fs_stream* pOutputStream); + +/* +Deserializes file system data from a stream. + +This function reads serialized file system data from a stream and recreates the files and +directories in the specified subdirectory. The format of the data must match that produced by +`fs_serialize()`. + +The subdirectory parameter specifies where to restore the serialized data. If NULL or empty, the +data is restored to the file system root. The path is relative to the file system root. + +Existing files with the same name will be overwritten during deserialization. If the output +directory already exists, it will not emptied before restoring the data. + +This function will not attempt to clean up any partially restored data if an error occurs during +the process. + + +Parameters +---------- +pFS : (in) + A pointer to the file system object. Must not be NULL. + +pSubdirectoryPath : (in, optional) + The path to the subdirectory where the data should be restored. Can be NULL or empty to + restore to the filesystem root. The path should use forward slashes ('/') as separators. + +options : (in) + Options for the deserialization operation. These will be passed into `fs_file_open()` when + creating files. See `fs_file_open()` for details. + +pInputStream : (in) + A pointer to the input stream containing the serialized data. Must not be NULL. + + +Return Value +------------ +Returns FS_SUCCESS on success; any other result code otherwise. + + +See Also +-------- +fs_serialize() +*/ +FS_API fs_result fs_deserialize(fs* pFS, const char* pDirectoryPath, int options, fs_stream* pInputStream); + + /* BEG fs_backend_posix.h */ extern const fs_backend* FS_BACKEND_POSIX; /* END fs_backend_posix.h */ @@ -3417,6 +3645,7 @@ read and write from different locations from the same fs_memory_stream object, y seek before doing your read or write. You cannot read and write at the same time across multiple threads for the same fs_memory_stream object. */ + typedef struct fs_memory_stream fs_memory_stream; struct fs_memory_stream