From 4feacc0065db65e7c9ef56663d3c481c595fb0b1 Mon Sep 17 00:00:00 2001 From: fgsfds Date: Sun, 7 Jun 2020 21:05:26 +0300 Subject: [PATCH] add virtual filesystem thing w/ ZIP support similar to Quake 3: all the archives and folders get mounted to the same mountpoint in the VFS, read access to files in the VFS is transparent --- Makefile | 50 +- include/macros.h | 12 +- include/tinfl.h | 724 +++++++++++++++++++++++++++++ src/audio/load.c | 8 +- src/pc/configfile.c | 36 +- src/pc/controller/controller_sdl.c | 19 +- src/pc/fs/dirtree.c | 137 ++++++ src/pc/fs/dirtree.h | 32 ++ src/pc/fs/fs.c | 443 ++++++++++++++++++ src/pc/fs/fs.h | 136 ++++++ src/pc/fs/fs_packtype_dir.c | 117 +++++ src/pc/fs/fs_packtype_zip.c | 486 +++++++++++++++++++ src/pc/gfx/gfx_opengl.c | 4 +- src/pc/gfx/gfx_opengl_legacy.c | 9 +- src/pc/gfx/gfx_pc.c | 88 ++-- src/pc/pc_main.c | 3 + src/pc/platform.c | 247 +++------- src/pc/platform.h | 23 +- src/pc/ultra_reimplementation.c | 15 +- tools/mkzip.py | 22 + 20 files changed, 2294 insertions(+), 317 deletions(-) create mode 100644 include/tinfl.h create mode 100644 src/pc/fs/dirtree.c create mode 100644 src/pc/fs/dirtree.h create mode 100644 src/pc/fs/fs.c create mode 100644 src/pc/fs/fs.h create mode 100644 src/pc/fs/fs_packtype_dir.c create mode 100644 src/pc/fs/fs_packtype_zip.c create mode 100644 tools/mkzip.py diff --git a/Makefile b/Makefile index a111fe56..8ec64322 100644 --- a/Makefile +++ b/Makefile @@ -56,6 +56,11 @@ NO_LDIV ?= 0 LEGACY_GL ?= 0 +# Misc settings for EXTERNAL_DATA + +BASEDIR ?= res +BASEPACK ?= base.zip + # Automatic settings for PC port(s) NON_MATCHING := 1 @@ -266,7 +271,7 @@ LEVEL_DIRS := $(patsubst levels/%,%,$(dir $(wildcard levels/*/header.h))) # Directories containing source files # Hi, I'm a PC -SRC_DIRS := src src/engine src/game src/audio src/menu src/buffers actors levels bin data assets src/pc src/pc/gfx src/pc/audio src/pc/controller +SRC_DIRS := src src/engine src/game src/audio src/menu src/buffers actors levels bin data assets src/pc src/pc/gfx src/pc/audio src/pc/controller src/pc/fs src/pc/fs/packtypes ASM_DIRS := BIN_DIRS := bin bin/$(VERSION) @@ -556,8 +561,8 @@ endif # Load external textures ifeq ($(EXTERNAL_DATA),1) - CC_CHECK += -DEXTERNAL_DATA - CFLAGS += -DEXTERNAL_DATA + CC_CHECK += -DEXTERNAL_DATA -DFS_BASEDIR="\"$(BASEDIR)\"" + CFLAGS += -DEXTERNAL_DATA -DFS_BASEDIR="\"$(BASEDIR)\"" # tell skyconv to write names instead of actual texture data and save the split tiles so we can use them later SKYCONV_ARGS := --store-names --write-tiles "$(BUILD_DIR)/textures/skybox_tiles" endif @@ -619,27 +624,30 @@ all: $(EXE) ifeq ($(EXTERNAL_DATA),1) -# thank you apple very cool -ifeq ($(HOST_OS),Darwin) - CP := gcp -else - CP := cp -endif +BASEPACK_PATH := $(BUILD_DIR)/$(BASEDIR)/$(BASEPACK) +BASEPACK_LST := $(BUILD_DIR)/basepack.lst # depend on resources as well -all: res +all: $(BASEPACK_PATH) -# prepares the resource folder for external data -res: $(EXE) - @mkdir -p $(BUILD_DIR)/res/sound - @$(CP) -r -f textures/ $(BUILD_DIR)/res/ - @$(CP) -r -f $(BUILD_DIR)/textures/skybox_tiles/ $(BUILD_DIR)/res/textures/ - @$(CP) -f $(SOUND_BIN_DIR)/sound_data.ctl $(BUILD_DIR)/res/sound/ - @$(CP) -f $(SOUND_BIN_DIR)/sound_data.tbl $(BUILD_DIR)/res/sound/ - @$(CP) -f $(SOUND_BIN_DIR)/sequences.bin $(BUILD_DIR)/res/sound/ - @$(CP) -f $(SOUND_BIN_DIR)/bank_sets $(BUILD_DIR)/res/sound/ - @find actors -name \*.png -exec $(CP) --parents {} $(BUILD_DIR)/res/ \; - @find levels -name \*.png -exec $(CP) --parents {} $(BUILD_DIR)/res/ \; +# phony target for building resources +res: $(BASEPACK_PATH) + +# prepares the basepack.lst +$(BASEPACK_LST): $(EXE) + @mkdir -p $(BUILD_DIR)/$(BASEDIR) + @echo -n > $(BASEPACK_LST) + @echo "$(BUILD_DIR)/sound/bank_sets sound/bank_sets" >> $(BASEPACK_LST) + @echo "$(BUILD_DIR)/sound/sequences.bin sound/sequences.bin" >> $(BASEPACK_LST) + @echo "$(BUILD_DIR)/sound/sound_data.ctl sound/sound_data.ctl" >> $(BASEPACK_LST) + @echo "$(BUILD_DIR)/sound/sound_data.tbl sound/sound_data.tbl" >> $(BASEPACK_LST) + @find actors -name \*.png -exec echo "{} gfx/{}" >> $(BASEPACK_LST) \; + @find levels -name \*.png -exec echo "{} gfx/{}" >> $(BASEPACK_LST) \; + @find textures -name \*.png -exec echo "{} gfx/{}" >> $(BASEPACK_LST) \; + +# prepares the resource ZIP with base data +$(BASEPACK_PATH): $(BASEPACK_LST) + @$(TOOLS_DIR)/mkzip.py $(BASEPACK_LST) $(BASEPACK_PATH) endif diff --git a/include/macros.h b/include/macros.h index f93642fb..85512d23 100644 --- a/include/macros.h +++ b/include/macros.h @@ -59,11 +59,15 @@ // Convenience macros for endian conversions #if IS_BIG_ENDIAN -#define BE_TO_HOST16(x) (x) -#define BE_TO_HOST32(x) (x) +# define BE_TO_HOST16(x) (x) +# define BE_TO_HOST32(x) (x) +# define LE_TO_HOST16(x) BSWAP16(x) +# define LE_TO_HOST32(x) BSWAP32(x) #else -#define BE_TO_HOST16(x) BSWAP16(x) -#define BE_TO_HOST32(x) BSWAP32(x) +# define BE_TO_HOST16(x) BSWAP16(x) +# define BE_TO_HOST32(x) BSWAP32(x) +# define LE_TO_HOST16(x) (x) +# define LE_TO_HOST32(x) (x) #endif #endif diff --git a/include/tinfl.h b/include/tinfl.h new file mode 100644 index 00000000..b6b31d0a --- /dev/null +++ b/include/tinfl.h @@ -0,0 +1,724 @@ +/* tinfl.c v1.11 - public domain inflate with zlib header parsing/adler32 checking (inflate-only subset of miniz.c) + See "unlicense" statement at the end of this file. + Rich Geldreich , last updated May 20, 2011 + Implements RFC 1950: https://www.ietf.org/rfc/rfc1950.txt and RFC 1951: https://www.ietf.org/rfc/rfc1951.txt + + The entire decompressor coroutine is implemented in tinfl_decompress(). The other functions are optional high-level helpers. +*/ +#ifndef TINFL_HEADER_INCLUDED +#define TINFL_HEADER_INCLUDED + +#include + +typedef uint8_t mz_uint8; +typedef int16_t mz_int16; +typedef uint16_t mz_uint16; +typedef uint32_t mz_uint32; +typedef unsigned int mz_uint; +typedef uint64_t mz_uint64; + +/* For more compatibility with zlib, miniz.c uses unsigned long for some parameters/struct members. */ +typedef unsigned long mz_ulong; + +/* Heap allocation callbacks. */ +typedef void *(*mz_alloc_func)(void *opaque, unsigned int items, unsigned int size); +typedef void (*mz_free_func)(void *opaque, void *address); + +#if defined(_M_IX86) || defined(_M_X64) +/* Set MINIZ_USE_UNALIGNED_LOADS_AND_STORES to 1 if integer loads and stores to unaligned addresses are acceptable on the target platform (slightly faster). */ +#define MINIZ_USE_UNALIGNED_LOADS_AND_STORES 1 +/* Set MINIZ_LITTLE_ENDIAN to 1 if the processor is little endian. */ +#define MINIZ_LITTLE_ENDIAN 1 +#endif + +#if defined(_WIN64) || defined(__MINGW64__) || defined(_LP64) || defined(__LP64__) +/* Set MINIZ_HAS_64BIT_REGISTERS to 1 if the processor has 64-bit general purpose registers (enables 64-bit bitbuffer in inflator) */ +#define MINIZ_HAS_64BIT_REGISTERS 1 +#endif + +/* Works around MSVC's spammy "warning C4127: conditional expression is constant" message. */ +#ifdef _MSC_VER +#define MZ_MACRO_END while (0, 0) +#else +#define MZ_MACRO_END while (0) +#endif + +/* Decompression flags. */ +enum +{ + TINFL_FLAG_PARSE_ZLIB_HEADER = 1, + TINFL_FLAG_HAS_MORE_INPUT = 2, + TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF = 4, + TINFL_FLAG_COMPUTE_ADLER32 = 8 +}; + +struct tinfl_decompressor_tag; typedef struct tinfl_decompressor_tag tinfl_decompressor; + +/* Max size of LZ dictionary. */ +#define TINFL_LZ_DICT_SIZE 32768 + +/* Return status. */ +typedef enum +{ + TINFL_STATUS_BAD_PARAM = -3, + TINFL_STATUS_ADLER32_MISMATCH = -2, + TINFL_STATUS_FAILED = -1, + TINFL_STATUS_DONE = 0, + TINFL_STATUS_NEEDS_MORE_INPUT = 1, + TINFL_STATUS_HAS_MORE_OUTPUT = 2 +} tinfl_status; + +/* Initializes the decompressor to its initial state. */ +#define tinfl_init(r) do { (r)->m_state = 0; } MZ_MACRO_END +#define tinfl_get_adler32(r) (r)->m_check_adler32 + +/* Main low-level decompressor coroutine function. This is the only function actually needed for decompression. All the other functions are just high-level helpers for improved usability. */ +/* This is a universal API, i.e. it can be used as a building block to build any desired higher level decompression API. In the limit case, it can be called once per every byte input or output. */ +static tinfl_status tinfl_decompress(tinfl_decompressor *r, const mz_uint8 *pIn_buf_next, size_t *pIn_buf_size, mz_uint8 *pOut_buf_start, mz_uint8 *pOut_buf_next, size_t *pOut_buf_size, const mz_uint32 decomp_flags); + +/* Internal/private bits follow. */ +enum +{ + TINFL_MAX_HUFF_TABLES = 3, TINFL_MAX_HUFF_SYMBOLS_0 = 288, TINFL_MAX_HUFF_SYMBOLS_1 = 32, TINFL_MAX_HUFF_SYMBOLS_2 = 19, + TINFL_FAST_LOOKUP_BITS = 10, TINFL_FAST_LOOKUP_SIZE = 1 << TINFL_FAST_LOOKUP_BITS +}; + +typedef struct +{ + mz_uint8 m_code_size[TINFL_MAX_HUFF_SYMBOLS_0]; + mz_int16 m_look_up[TINFL_FAST_LOOKUP_SIZE], m_tree[TINFL_MAX_HUFF_SYMBOLS_0 * 2]; +} tinfl_huff_table; + +#if MINIZ_HAS_64BIT_REGISTERS + #define TINFL_USE_64BIT_BITBUF 1 +#endif + +#if TINFL_USE_64BIT_BITBUF + typedef mz_uint64 tinfl_bit_buf_t; + #define TINFL_BITBUF_SIZE (64) +#else + typedef mz_uint32 tinfl_bit_buf_t; + #define TINFL_BITBUF_SIZE (32) +#endif + +struct tinfl_decompressor_tag +{ + mz_uint32 m_state, m_num_bits, m_zhdr0, m_zhdr1, m_z_adler32, m_final, m_type, m_check_adler32, m_dist, m_counter, m_num_extra, m_table_sizes[TINFL_MAX_HUFF_TABLES]; + tinfl_bit_buf_t m_bit_buf; + size_t m_dist_from_out_buf_start; + tinfl_huff_table m_tables[TINFL_MAX_HUFF_TABLES]; + mz_uint8 m_raw_header[4], m_len_codes[TINFL_MAX_HUFF_SYMBOLS_0 + TINFL_MAX_HUFF_SYMBOLS_1 + 137]; +}; + +#endif /* #ifdef TINFL_HEADER_INCLUDED */ + +/* ------------------- End of Header: Implementation follows. (If you only want the header, define MINIZ_HEADER_FILE_ONLY.) */ + +#ifndef TINFL_HEADER_FILE_ONLY + +#ifdef MINIZ_NO_MALLOC + #define MZ_MALLOC(x) NULL + #define MZ_FREE(x) x, ((void)0) + #define MZ_REALLOC(p, x) NULL +#else + #define MZ_MALLOC(x) malloc(x) + #define MZ_FREE(x) free(x) + #define MZ_REALLOC(p, x) realloc(p, x) +#endif + +#define MZ_MAX(a,b) (((a)>(b))?(a):(b)) +#define MZ_MIN(a,b) (((a)<(b))?(a):(b)) +#define MZ_CLEAR_OBJ(obj) memset(&(obj), 0, sizeof(obj)) + +#if MINIZ_USE_UNALIGNED_LOADS_AND_STORES && MINIZ_LITTLE_ENDIAN + #define MZ_READ_LE16(p) *((const mz_uint16 *)(p)) + #define MZ_READ_LE32(p) *((const mz_uint32 *)(p)) +#else + #define MZ_READ_LE16(p) ((mz_uint32)(((const mz_uint8 *)(p))[0]) | ((mz_uint32)(((const mz_uint8 *)(p))[1]) << 8U)) + #define MZ_READ_LE32(p) ((mz_uint32)(((const mz_uint8 *)(p))[0]) | ((mz_uint32)(((const mz_uint8 *)(p))[1]) << 8U) | ((mz_uint32)(((const mz_uint8 *)(p))[2]) << 16U) | ((mz_uint32)(((const mz_uint8 *)(p))[3]) << 24U)) +#endif + +#define TINFL_MEMCPY(d, s, l) memcpy(d, s, l) +#define TINFL_MEMSET(p, c, l) memset(p, c, l) + +#define TINFL_CR_BEGIN switch(r->m_state) { case 0: +#define TINFL_CR_RETURN(state_index, result) do { status = result; r->m_state = state_index; goto common_exit; case state_index:; } MZ_MACRO_END +#define TINFL_CR_RETURN_FOREVER(state_index, result) do { for ( ; ; ) { TINFL_CR_RETURN(state_index, result); } } MZ_MACRO_END +#define TINFL_CR_FINISH } + +/* TODO: If the caller has indicated that there's no more input, and we attempt to read beyond the input buf, then something is wrong with the input because the inflator never */ +/* reads ahead more than it needs to. Currently TINFL_GET_BYTE() pads the end of the stream with 0's in this scenario. */ +#define TINFL_GET_BYTE(state_index, c) do { \ + if (pIn_buf_cur >= pIn_buf_end) { \ + for ( ; ; ) { \ + if (decomp_flags & TINFL_FLAG_HAS_MORE_INPUT) { \ + TINFL_CR_RETURN(state_index, TINFL_STATUS_NEEDS_MORE_INPUT); \ + if (pIn_buf_cur < pIn_buf_end) { \ + c = *pIn_buf_cur++; \ + break; \ + } \ + } else { \ + c = 0; \ + break; \ + } \ + } \ + } else c = *pIn_buf_cur++; } MZ_MACRO_END + +#define TINFL_NEED_BITS(state_index, n) do { mz_uint c; TINFL_GET_BYTE(state_index, c); bit_buf |= (((tinfl_bit_buf_t)c) << num_bits); num_bits += 8; } while (num_bits < (mz_uint)(n)) +#define TINFL_SKIP_BITS(state_index, n) do { if (num_bits < (mz_uint)(n)) { TINFL_NEED_BITS(state_index, n); } bit_buf >>= (n); num_bits -= (n); } MZ_MACRO_END +#define TINFL_GET_BITS(state_index, b, n) do { if (num_bits < (mz_uint)(n)) { TINFL_NEED_BITS(state_index, n); } b = bit_buf & ((1 << (n)) - 1); bit_buf >>= (n); num_bits -= (n); } MZ_MACRO_END + +/* TINFL_HUFF_BITBUF_FILL() is only used rarely, when the number of bytes remaining in the input buffer falls below 2. */ +/* It reads just enough bytes from the input stream that are needed to decode the next Huffman code (and absolutely no more). It works by trying to fully decode a */ +/* Huffman code by using whatever bits are currently present in the bit buffer. If this fails, it reads another byte, and tries again until it succeeds or until the */ +/* bit buffer contains >=15 bits (deflate's max. Huffman code size). */ +#define TINFL_HUFF_BITBUF_FILL(state_index, pHuff) \ + do { \ + temp = (pHuff)->m_look_up[bit_buf & (TINFL_FAST_LOOKUP_SIZE - 1)]; \ + if (temp >= 0) { \ + code_len = temp >> 9; \ + if ((code_len) && (num_bits >= code_len)) \ + break; \ + } else if (num_bits > TINFL_FAST_LOOKUP_BITS) { \ + code_len = TINFL_FAST_LOOKUP_BITS; \ + do { \ + temp = (pHuff)->m_tree[~temp + ((bit_buf >> code_len++) & 1)]; \ + } while ((temp < 0) && (num_bits >= (code_len + 1))); if (temp >= 0) break; \ + } TINFL_GET_BYTE(state_index, c); bit_buf |= (((tinfl_bit_buf_t)c) << num_bits); num_bits += 8; \ + } while (num_bits < 15); + +/* TINFL_HUFF_DECODE() decodes the next Huffman coded symbol. It's more complex than you would initially expect because the zlib API expects the decompressor to never read */ +/* beyond the final byte of the deflate stream. (In other words, when this macro wants to read another byte from the input, it REALLY needs another byte in order to fully */ +/* decode the next Huffman code.) Handling this properly is particularly important on raw deflate (non-zlib) streams, which aren't followed by a byte aligned adler-32. */ +/* The slow path is only executed at the very end of the input buffer. */ +#define TINFL_HUFF_DECODE(state_index, sym, pHuff) do { \ + int temp; mz_uint code_len, c; \ + if (num_bits < 15) { \ + if ((pIn_buf_end - pIn_buf_cur) < 2) { \ + TINFL_HUFF_BITBUF_FILL(state_index, pHuff); \ + } else { \ + bit_buf |= (((tinfl_bit_buf_t)pIn_buf_cur[0]) << num_bits) | (((tinfl_bit_buf_t)pIn_buf_cur[1]) << (num_bits + 8)); pIn_buf_cur += 2; num_bits += 16; \ + } \ + } \ + if ((temp = (pHuff)->m_look_up[bit_buf & (TINFL_FAST_LOOKUP_SIZE - 1)]) >= 0) \ + code_len = temp >> 9, temp &= 511; \ + else { \ + code_len = TINFL_FAST_LOOKUP_BITS; do { temp = (pHuff)->m_tree[~temp + ((bit_buf >> code_len++) & 1)]; } while (temp < 0); \ + } sym = temp; bit_buf >>= code_len; num_bits -= code_len; } MZ_MACRO_END + +static void tinfl_def_free_func(void *opaque, void *address) { + (void)opaque, (void)address; + MZ_FREE(address); +} + +static void *tinfl_def_alloc_func(void *opaque, unsigned int items, unsigned int size) { + (void)opaque, (void)items, (void)size; + return MZ_MALLOC(items * size); +} + +static tinfl_status tinfl_decompress(tinfl_decompressor *r, const mz_uint8 *pIn_buf_next, size_t *pIn_buf_size, mz_uint8 *pOut_buf_start, mz_uint8 *pOut_buf_next, size_t *pOut_buf_size, const mz_uint32 decomp_flags) +{ + static const int s_length_base[31] = { 3,4,5,6,7,8,9,10,11,13, 15,17,19,23,27,31,35,43,51,59, 67,83,99,115,131,163,195,227,258,0,0 }; + static const int s_length_extra[31]= { 0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0 }; + static const int s_dist_base[32] = { 1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193, 257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,0,0}; + static const int s_dist_extra[32] = { 0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13}; + static const mz_uint8 s_length_dezigzag[19] = { 16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15 }; + static const int s_min_table_sizes[3] = { 257, 1, 4 }; + + tinfl_status status = TINFL_STATUS_FAILED; mz_uint32 num_bits, dist, counter, num_extra; tinfl_bit_buf_t bit_buf; + const mz_uint8 *pIn_buf_cur = pIn_buf_next, *const pIn_buf_end = pIn_buf_next + *pIn_buf_size; + mz_uint8 *pOut_buf_cur = pOut_buf_next, *const pOut_buf_end = pOut_buf_next + *pOut_buf_size; + size_t out_buf_size_mask = (decomp_flags & TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF) ? (size_t)-1 : ((pOut_buf_next - pOut_buf_start) + *pOut_buf_size) - 1, dist_from_out_buf_start; + + /* Ensure the output buffer's size is a power of 2, unless the output buffer is large enough to hold the entire output file (in which case it doesn't matter). */ + if (((out_buf_size_mask + 1) & out_buf_size_mask) || (pOut_buf_next < pOut_buf_start)) { *pIn_buf_size = *pOut_buf_size = 0; return TINFL_STATUS_BAD_PARAM; } + + num_bits = r->m_num_bits; bit_buf = r->m_bit_buf; dist = r->m_dist; counter = r->m_counter; num_extra = r->m_num_extra; dist_from_out_buf_start = r->m_dist_from_out_buf_start; + TINFL_CR_BEGIN + + bit_buf = num_bits = dist = counter = num_extra = r->m_zhdr0 = r->m_zhdr1 = 0; r->m_z_adler32 = r->m_check_adler32 = 1; + if (decomp_flags & TINFL_FLAG_PARSE_ZLIB_HEADER) + { + TINFL_GET_BYTE(1, r->m_zhdr0); TINFL_GET_BYTE(2, r->m_zhdr1); + counter = (((r->m_zhdr0 * 256 + r->m_zhdr1) % 31 != 0) || (r->m_zhdr1 & 32) || ((r->m_zhdr0 & 15) != 8)); + if (!(decomp_flags & TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF)) counter |= (((1U << (8U + (r->m_zhdr0 >> 4))) > 32768U) || ((out_buf_size_mask + 1) < (size_t)(1U << (8U + (r->m_zhdr0 >> 4))))); + if (counter) { TINFL_CR_RETURN_FOREVER(36, TINFL_STATUS_FAILED); } + } + + do + { + TINFL_GET_BITS(3, r->m_final, 3); r->m_type = r->m_final >> 1; + if (r->m_type == 0) + { + TINFL_SKIP_BITS(5, num_bits & 7); + for (counter = 0; counter < 4; ++counter) { if (num_bits) TINFL_GET_BITS(6, r->m_raw_header[counter], 8); else TINFL_GET_BYTE(7, r->m_raw_header[counter]); } + if ((counter = (r->m_raw_header[0] | (r->m_raw_header[1] << 8))) != (mz_uint)(0xFFFF ^ (r->m_raw_header[2] | (r->m_raw_header[3] << 8)))) { TINFL_CR_RETURN_FOREVER(39, TINFL_STATUS_FAILED); } + while ((counter) && (num_bits)) + { + TINFL_GET_BITS(51, dist, 8); + while (pOut_buf_cur >= pOut_buf_end) { TINFL_CR_RETURN(52, TINFL_STATUS_HAS_MORE_OUTPUT); } + *pOut_buf_cur++ = (mz_uint8)dist; + counter--; + } + while (counter) + { + size_t n; while (pOut_buf_cur >= pOut_buf_end) { TINFL_CR_RETURN(9, TINFL_STATUS_HAS_MORE_OUTPUT); } + while (pIn_buf_cur >= pIn_buf_end) + { + if (decomp_flags & TINFL_FLAG_HAS_MORE_INPUT) + { + TINFL_CR_RETURN(38, TINFL_STATUS_NEEDS_MORE_INPUT); + } + else + { + TINFL_CR_RETURN_FOREVER(40, TINFL_STATUS_FAILED); + } + } + n = MZ_MIN(MZ_MIN((size_t)(pOut_buf_end - pOut_buf_cur), (size_t)(pIn_buf_end - pIn_buf_cur)), counter); + TINFL_MEMCPY(pOut_buf_cur, pIn_buf_cur, n); pIn_buf_cur += n; pOut_buf_cur += n; counter -= (mz_uint)n; + } + } + else if (r->m_type == 3) + { + TINFL_CR_RETURN_FOREVER(10, TINFL_STATUS_FAILED); + } + else + { + if (r->m_type == 1) + { + mz_uint8 *p = r->m_tables[0].m_code_size; mz_uint i; + r->m_table_sizes[0] = 288; r->m_table_sizes[1] = 32; TINFL_MEMSET(r->m_tables[1].m_code_size, 5, 32); + for ( i = 0; i <= 143; ++i) *p++ = 8; + for ( ; i <= 255; ++i) *p++ = 9; + for ( ; i <= 279; ++i) *p++ = 7; + for ( ; i <= 287; ++i) *p++ = 8; + } + else + { + for (counter = 0; counter < 3; counter++) { TINFL_GET_BITS(11, r->m_table_sizes[counter], "\05\05\04"[counter]); r->m_table_sizes[counter] += s_min_table_sizes[counter]; } + MZ_CLEAR_OBJ(r->m_tables[2].m_code_size); for (counter = 0; counter < r->m_table_sizes[2]; counter++) { mz_uint s; TINFL_GET_BITS(14, s, 3); r->m_tables[2].m_code_size[s_length_dezigzag[counter]] = (mz_uint8)s; } + r->m_table_sizes[2] = 19; + } + for ( ; (int)r->m_type >= 0; r->m_type--) + { + int tree_next, tree_cur; tinfl_huff_table *pTable; + mz_uint i, j, used_syms, total, sym_index, next_code[17], total_syms[16]; pTable = &r->m_tables[r->m_type]; MZ_CLEAR_OBJ(total_syms); MZ_CLEAR_OBJ(pTable->m_look_up); MZ_CLEAR_OBJ(pTable->m_tree); + for (i = 0; i < r->m_table_sizes[r->m_type]; ++i) total_syms[pTable->m_code_size[i]]++; + used_syms = 0, total = 0; next_code[0] = next_code[1] = 0; + for (i = 1; i <= 15; ++i) { used_syms += total_syms[i]; next_code[i + 1] = (total = ((total + total_syms[i]) << 1)); } + if ((65536 != total) && (used_syms > 1)) + { + TINFL_CR_RETURN_FOREVER(35, TINFL_STATUS_FAILED); + } + for (tree_next = -1, sym_index = 0; sym_index < r->m_table_sizes[r->m_type]; ++sym_index) + { + mz_uint rev_code = 0, l, cur_code, code_size = pTable->m_code_size[sym_index]; if (!code_size) continue; + cur_code = next_code[code_size]++; for (l = code_size; l > 0; l--, cur_code >>= 1) rev_code = (rev_code << 1) | (cur_code & 1); + if (code_size <= TINFL_FAST_LOOKUP_BITS) { mz_int16 k = (mz_int16)((code_size << 9) | sym_index); while (rev_code < TINFL_FAST_LOOKUP_SIZE) { pTable->m_look_up[rev_code] = k; rev_code += (1 << code_size); } continue; } + if (0 == (tree_cur = pTable->m_look_up[rev_code & (TINFL_FAST_LOOKUP_SIZE - 1)])) { pTable->m_look_up[rev_code & (TINFL_FAST_LOOKUP_SIZE - 1)] = (mz_int16)tree_next; tree_cur = tree_next; tree_next -= 2; } + rev_code >>= (TINFL_FAST_LOOKUP_BITS - 1); + for (j = code_size; j > (TINFL_FAST_LOOKUP_BITS + 1); j--) + { + tree_cur -= ((rev_code >>= 1) & 1); + if (!pTable->m_tree[-tree_cur - 1]) { pTable->m_tree[-tree_cur - 1] = (mz_int16)tree_next; tree_cur = tree_next; tree_next -= 2; } else tree_cur = pTable->m_tree[-tree_cur - 1]; + } + tree_cur -= ((rev_code >>= 1) & 1); pTable->m_tree[-tree_cur - 1] = (mz_int16)sym_index; + } + if (r->m_type == 2) + { + for (counter = 0; counter < (r->m_table_sizes[0] + r->m_table_sizes[1]); ) + { + mz_uint s; TINFL_HUFF_DECODE(16, dist, &r->m_tables[2]); if (dist < 16) { r->m_len_codes[counter++] = (mz_uint8)dist; continue; } + if ((dist == 16) && (!counter)) + { + TINFL_CR_RETURN_FOREVER(17, TINFL_STATUS_FAILED); + } + num_extra = "\02\03\07"[dist - 16]; TINFL_GET_BITS(18, s, num_extra); s += "\03\03\013"[dist - 16]; + TINFL_MEMSET(r->m_len_codes + counter, (dist == 16) ? r->m_len_codes[counter - 1] : 0, s); counter += s; + } + if ((r->m_table_sizes[0] + r->m_table_sizes[1]) != counter) + { + TINFL_CR_RETURN_FOREVER(21, TINFL_STATUS_FAILED); + } + TINFL_MEMCPY(r->m_tables[0].m_code_size, r->m_len_codes, r->m_table_sizes[0]); TINFL_MEMCPY(r->m_tables[1].m_code_size, r->m_len_codes + r->m_table_sizes[0], r->m_table_sizes[1]); + } + } + for ( ; ; ) + { + mz_uint8 *pSrc; + for ( ; ; ) + { + if (((pIn_buf_end - pIn_buf_cur) < 4) || ((pOut_buf_end - pOut_buf_cur) < 2)) + { + TINFL_HUFF_DECODE(23, counter, &r->m_tables[0]); + if (counter >= 256) + break; + while (pOut_buf_cur >= pOut_buf_end) { TINFL_CR_RETURN(24, TINFL_STATUS_HAS_MORE_OUTPUT); } + *pOut_buf_cur++ = (mz_uint8)counter; + } + else + { + int sym2; mz_uint code_len; +#if TINFL_USE_64BIT_BITBUF + if (num_bits < 30) { bit_buf |= (((tinfl_bit_buf_t)MZ_READ_LE32(pIn_buf_cur)) << num_bits); pIn_buf_cur += 4; num_bits += 32; } +#else + if (num_bits < 15) { bit_buf |= (((tinfl_bit_buf_t)MZ_READ_LE16(pIn_buf_cur)) << num_bits); pIn_buf_cur += 2; num_bits += 16; } +#endif + if ((sym2 = r->m_tables[0].m_look_up[bit_buf & (TINFL_FAST_LOOKUP_SIZE - 1)]) >= 0) + code_len = sym2 >> 9; + else + { + code_len = TINFL_FAST_LOOKUP_BITS; do { sym2 = r->m_tables[0].m_tree[~sym2 + ((bit_buf >> code_len++) & 1)]; } while (sym2 < 0); + } + counter = sym2; bit_buf >>= code_len; num_bits -= code_len; + if (counter & 256) + break; + +#if !TINFL_USE_64BIT_BITBUF + if (num_bits < 15) { bit_buf |= (((tinfl_bit_buf_t)MZ_READ_LE16(pIn_buf_cur)) << num_bits); pIn_buf_cur += 2; num_bits += 16; } +#endif + if ((sym2 = r->m_tables[0].m_look_up[bit_buf & (TINFL_FAST_LOOKUP_SIZE - 1)]) >= 0) + code_len = sym2 >> 9; + else + { + code_len = TINFL_FAST_LOOKUP_BITS; do { sym2 = r->m_tables[0].m_tree[~sym2 + ((bit_buf >> code_len++) & 1)]; } while (sym2 < 0); + } + bit_buf >>= code_len; num_bits -= code_len; + + pOut_buf_cur[0] = (mz_uint8)counter; + if (sym2 & 256) + { + pOut_buf_cur++; + counter = sym2; + break; + } + pOut_buf_cur[1] = (mz_uint8)sym2; + pOut_buf_cur += 2; + } + } + if ((counter &= 511) == 256) break; + + num_extra = s_length_extra[counter - 257]; counter = s_length_base[counter - 257]; + if (num_extra) { mz_uint extra_bits; TINFL_GET_BITS(25, extra_bits, num_extra); counter += extra_bits; } + + TINFL_HUFF_DECODE(26, dist, &r->m_tables[1]); + num_extra = s_dist_extra[dist]; dist = s_dist_base[dist]; + if (num_extra) { mz_uint extra_bits; TINFL_GET_BITS(27, extra_bits, num_extra); dist += extra_bits; } + + dist_from_out_buf_start = pOut_buf_cur - pOut_buf_start; + if ((dist > dist_from_out_buf_start) && (decomp_flags & TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF)) + { + TINFL_CR_RETURN_FOREVER(37, TINFL_STATUS_FAILED); + } + + pSrc = pOut_buf_start + ((dist_from_out_buf_start - dist) & out_buf_size_mask); + + if ((MZ_MAX(pOut_buf_cur, pSrc) + counter) > pOut_buf_end) + { + while (counter--) + { + while (pOut_buf_cur >= pOut_buf_end) { TINFL_CR_RETURN(53, TINFL_STATUS_HAS_MORE_OUTPUT); } + *pOut_buf_cur++ = pOut_buf_start[(dist_from_out_buf_start++ - dist) & out_buf_size_mask]; + } + continue; + } +#if MINIZ_USE_UNALIGNED_LOADS_AND_STORES + else if ((counter >= 9) && (counter <= dist)) + { + const mz_uint8 *pSrc_end = pSrc + (counter & ~7); + do + { + ((mz_uint32 *)pOut_buf_cur)[0] = ((const mz_uint32 *)pSrc)[0]; + ((mz_uint32 *)pOut_buf_cur)[1] = ((const mz_uint32 *)pSrc)[1]; + pOut_buf_cur += 8; + } while ((pSrc += 8) < pSrc_end); + if ((counter &= 7) < 3) + { + if (counter) + { + pOut_buf_cur[0] = pSrc[0]; + if (counter > 1) + pOut_buf_cur[1] = pSrc[1]; + pOut_buf_cur += counter; + } + continue; + } + } +#endif + do + { + pOut_buf_cur[0] = pSrc[0]; + pOut_buf_cur[1] = pSrc[1]; + pOut_buf_cur[2] = pSrc[2]; + pOut_buf_cur += 3; pSrc += 3; + } while ((int)(counter -= 3) > 2); + if ((int)counter > 0) + { + pOut_buf_cur[0] = pSrc[0]; + if ((int)counter > 1) + pOut_buf_cur[1] = pSrc[1]; + pOut_buf_cur += counter; + } + } + } + } while (!(r->m_final & 1)); + if (decomp_flags & TINFL_FLAG_PARSE_ZLIB_HEADER) + { + TINFL_SKIP_BITS(32, num_bits & 7); for (counter = 0; counter < 4; ++counter) { mz_uint s; if (num_bits) TINFL_GET_BITS(41, s, 8); else TINFL_GET_BYTE(42, s); r->m_z_adler32 = (r->m_z_adler32 << 8) | s; } + } + TINFL_CR_RETURN_FOREVER(34, TINFL_STATUS_DONE); + TINFL_CR_FINISH + +common_exit: + r->m_num_bits = num_bits; r->m_bit_buf = bit_buf; r->m_dist = dist; r->m_counter = counter; r->m_num_extra = num_extra; r->m_dist_from_out_buf_start = dist_from_out_buf_start; + *pIn_buf_size = pIn_buf_cur - pIn_buf_next; *pOut_buf_size = pOut_buf_cur - pOut_buf_next; + if ((decomp_flags & (TINFL_FLAG_PARSE_ZLIB_HEADER | TINFL_FLAG_COMPUTE_ADLER32)) && (status >= 0)) + { + const mz_uint8 *ptr = pOut_buf_next; size_t buf_len = *pOut_buf_size; + mz_uint32 i, s1 = r->m_check_adler32 & 0xffff, s2 = r->m_check_adler32 >> 16; size_t block_len = buf_len % 5552; + while (buf_len) + { + for (i = 0; i + 7 < block_len; i += 8, ptr += 8) + { + s1 += ptr[0], s2 += s1; s1 += ptr[1], s2 += s1; s1 += ptr[2], s2 += s1; s1 += ptr[3], s2 += s1; + s1 += ptr[4], s2 += s1; s1 += ptr[5], s2 += s1; s1 += ptr[6], s2 += s1; s1 += ptr[7], s2 += s1; + } + for ( ; i < block_len; ++i) s1 += *ptr++, s2 += s1; + s1 %= 65521U, s2 %= 65521U; buf_len -= block_len; block_len = 5552; + } + r->m_check_adler32 = (s2 << 16) + s1; if ((status == TINFL_STATUS_DONE) && (decomp_flags & TINFL_FLAG_PARSE_ZLIB_HEADER) && (r->m_check_adler32 != r->m_z_adler32)) status = TINFL_STATUS_ADLER32_MISMATCH; + } + return status; +} + +/* Flush values. For typical usage you only need MZ_NO_FLUSH and MZ_FINISH. The other stuff is for advanced use. */ +enum { MZ_NO_FLUSH = 0, MZ_PARTIAL_FLUSH = 1, MZ_SYNC_FLUSH = 2, MZ_FULL_FLUSH = 3, MZ_FINISH = 4, MZ_BLOCK = 5 }; + +/* Return status codes. MZ_PARAM_ERROR is non-standard. */ +enum { MZ_OK = 0, MZ_STREAM_END = 1, MZ_NEED_DICT = 2, MZ_ERRNO = -1, MZ_STREAM_ERROR = -2, MZ_DATA_ERROR = -3, MZ_MEM_ERROR = -4, MZ_BUF_ERROR = -5, MZ_VERSION_ERROR = -6, MZ_PARAM_ERROR = -10000 }; + +/* Compression levels. */ +enum { MZ_NO_COMPRESSION = 0, MZ_BEST_SPEED = 1, MZ_BEST_COMPRESSION = 9, MZ_DEFAULT_COMPRESSION = -1 }; + +/* Window bits */ +#define MZ_DEFAULT_WINDOW_BITS 15 + +struct mz_internal_state; + +/* Compression/decompression stream struct. */ +typedef struct mz_stream_s +{ + const unsigned char *next_in; /* pointer to next byte to read */ + unsigned int avail_in; /* number of bytes available at next_in */ + mz_ulong total_in; /* total number of bytes consumed so far */ + + unsigned char *next_out; /* pointer to next byte to write */ + unsigned int avail_out; /* number of bytes that can be written to next_out */ + mz_ulong total_out; /* total number of bytes produced so far */ + + char *msg; /* error msg (unused) */ + struct mz_internal_state *state; /* internal state, allocated by zalloc/zfree */ + + mz_alloc_func zalloc; /* optional heap allocation function (defaults to malloc) */ + mz_free_func zfree; /* optional heap free function (defaults to free) */ + void *opaque; /* heap alloc function user pointer */ + + int data_type; /* data_type (unused) */ + mz_ulong adler; /* adler32 of the source or uncompressed data */ + mz_ulong reserved; /* not used */ +} mz_stream; + +typedef mz_stream *mz_streamp; + + +typedef struct +{ + tinfl_decompressor m_decomp; + mz_uint m_dict_ofs, m_dict_avail, m_first_call, m_has_flushed; int m_window_bits; + mz_uint8 m_dict[TINFL_LZ_DICT_SIZE]; + tinfl_status m_last_status; +} inflate_state; + +static int mz_inflateInit2(mz_streamp pStream, int window_bits) +{ + inflate_state *pDecomp; + if (!pStream) return MZ_STREAM_ERROR; + if ((window_bits != MZ_DEFAULT_WINDOW_BITS) && (-window_bits != MZ_DEFAULT_WINDOW_BITS)) return MZ_PARAM_ERROR; + + pStream->data_type = 0; + pStream->adler = 0; + pStream->msg = NULL; + pStream->total_in = 0; + pStream->total_out = 0; + pStream->reserved = 0; + if (!pStream->zalloc) pStream->zalloc = tinfl_def_alloc_func; + if (!pStream->zfree) pStream->zfree = tinfl_def_free_func; + + pDecomp = (inflate_state*)pStream->zalloc(pStream->opaque, 1, sizeof(inflate_state)); + if (!pDecomp) return MZ_MEM_ERROR; + + pStream->state = (struct mz_internal_state *)pDecomp; + + tinfl_init(&pDecomp->m_decomp); + pDecomp->m_dict_ofs = 0; + pDecomp->m_dict_avail = 0; + pDecomp->m_last_status = TINFL_STATUS_NEEDS_MORE_INPUT; + pDecomp->m_first_call = 1; + pDecomp->m_has_flushed = 0; + pDecomp->m_window_bits = window_bits; + + return MZ_OK; +} + +static int mz_inflate(mz_streamp pStream, int flush) +{ + inflate_state* pState; + mz_uint n, first_call, decomp_flags = TINFL_FLAG_COMPUTE_ADLER32; + size_t in_bytes, out_bytes, orig_avail_in; + tinfl_status status; + + if ((!pStream) || (!pStream->state)) return MZ_STREAM_ERROR; + if (flush == MZ_PARTIAL_FLUSH) flush = MZ_SYNC_FLUSH; + if ((flush) && (flush != MZ_SYNC_FLUSH) && (flush != MZ_FINISH)) return MZ_STREAM_ERROR; + + pState = (inflate_state*)pStream->state; + if (pState->m_window_bits > 0) decomp_flags |= TINFL_FLAG_PARSE_ZLIB_HEADER; + orig_avail_in = pStream->avail_in; + + first_call = pState->m_first_call; pState->m_first_call = 0; + if (pState->m_last_status < 0) return MZ_DATA_ERROR; + + if (pState->m_has_flushed && (flush != MZ_FINISH)) return MZ_STREAM_ERROR; + pState->m_has_flushed |= (flush == MZ_FINISH); + + if ((flush == MZ_FINISH) && (first_call)) + { + /* MZ_FINISH on the first call implies that the input and output buffers are large enough to hold the entire compressed/decompressed file. */ + decomp_flags |= TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF; + in_bytes = pStream->avail_in; out_bytes = pStream->avail_out; + status = tinfl_decompress(&pState->m_decomp, pStream->next_in, &in_bytes, pStream->next_out, pStream->next_out, &out_bytes, decomp_flags); + pState->m_last_status = status; + pStream->next_in += (mz_uint)in_bytes; pStream->avail_in -= (mz_uint)in_bytes; pStream->total_in += (mz_uint)in_bytes; + pStream->adler = tinfl_get_adler32(&pState->m_decomp); + pStream->next_out += (mz_uint)out_bytes; pStream->avail_out -= (mz_uint)out_bytes; pStream->total_out += (mz_uint)out_bytes; + + if (status < 0) + return MZ_DATA_ERROR; + else if (status != TINFL_STATUS_DONE) + { + pState->m_last_status = TINFL_STATUS_FAILED; + return MZ_BUF_ERROR; + } + return MZ_STREAM_END; + } + /* flush != MZ_FINISH then we must assume there's more input. */ + if (flush != MZ_FINISH) decomp_flags |= TINFL_FLAG_HAS_MORE_INPUT; + + if (pState->m_dict_avail) + { + n = MZ_MIN(pState->m_dict_avail, pStream->avail_out); + memcpy(pStream->next_out, pState->m_dict + pState->m_dict_ofs, n); + pStream->next_out += n; pStream->avail_out -= n; pStream->total_out += n; + pState->m_dict_avail -= n; pState->m_dict_ofs = (pState->m_dict_ofs + n) & (TINFL_LZ_DICT_SIZE - 1); + return ((pState->m_last_status == TINFL_STATUS_DONE) && (!pState->m_dict_avail)) ? MZ_STREAM_END : MZ_OK; + } + + for ( ; ; ) + { + in_bytes = pStream->avail_in; + out_bytes = TINFL_LZ_DICT_SIZE - pState->m_dict_ofs; + + status = tinfl_decompress(&pState->m_decomp, pStream->next_in, &in_bytes, pState->m_dict, pState->m_dict + pState->m_dict_ofs, &out_bytes, decomp_flags); + pState->m_last_status = status; + + pStream->next_in += (mz_uint)in_bytes; pStream->avail_in -= (mz_uint)in_bytes; + pStream->total_in += (mz_uint)in_bytes; pStream->adler = tinfl_get_adler32(&pState->m_decomp); + + pState->m_dict_avail = (mz_uint)out_bytes; + + n = MZ_MIN(pState->m_dict_avail, pStream->avail_out); + memcpy(pStream->next_out, pState->m_dict + pState->m_dict_ofs, n); + pStream->next_out += n; pStream->avail_out -= n; pStream->total_out += n; + pState->m_dict_avail -= n; pState->m_dict_ofs = (pState->m_dict_ofs + n) & (TINFL_LZ_DICT_SIZE - 1); + + if (status < 0) + return MZ_DATA_ERROR; /* Stream is corrupted (there could be some uncompressed data left in the output dictionary - oh well). */ + else if ((status == TINFL_STATUS_NEEDS_MORE_INPUT) && (!orig_avail_in)) + return MZ_BUF_ERROR; /* Signal caller that we can't make forward progress without supplying more input or by setting flush to MZ_FINISH. */ + else if (flush == MZ_FINISH) + { + /* The output buffer MUST be large to hold the remaining uncompressed data when flush==MZ_FINISH. */ + if (status == TINFL_STATUS_DONE) + return pState->m_dict_avail ? MZ_BUF_ERROR : MZ_STREAM_END; + /* status here must be TINFL_STATUS_HAS_MORE_OUTPUT, which means there's at least 1 more byte on the way. If there's no more room left in the output buffer then something is wrong. */ + else if (!pStream->avail_out) + return MZ_BUF_ERROR; + } + else if ((status == TINFL_STATUS_DONE) || (!pStream->avail_in) || (!pStream->avail_out) || (pState->m_dict_avail)) + break; + } + + return ((status == TINFL_STATUS_DONE) && (!pState->m_dict_avail)) ? MZ_STREAM_END : MZ_OK; +} + +static int mz_inflateEnd(mz_streamp pStream) +{ + if (!pStream) + return MZ_STREAM_ERROR; + if (pStream->state) + { + pStream->zfree(pStream->opaque, pStream->state); + pStream->state = NULL; + } + return MZ_OK; +} + +/* make this a drop-in replacement for zlib... */ + #define voidpf void* + #define uInt unsigned int + #define z_stream mz_stream + #define inflateInit2 mz_inflateInit2 + #define inflate mz_inflate + #define inflateEnd mz_inflateEnd + #define Z_SYNC_FLUSH MZ_SYNC_FLUSH + #define Z_FINISH MZ_FINISH + #define Z_OK MZ_OK + #define Z_STREAM_END MZ_STREAM_END + #define Z_NEED_DICT MZ_NEED_DICT + #define Z_ERRNO MZ_ERRNO + #define Z_STREAM_ERROR MZ_STREAM_ERROR + #define Z_DATA_ERROR MZ_DATA_ERROR + #define Z_MEM_ERROR MZ_MEM_ERROR + #define Z_BUF_ERROR MZ_BUF_ERROR + #define Z_VERSION_ERROR MZ_VERSION_ERROR + #define MAX_WBITS 15 + +#endif /* #ifndef TINFL_HEADER_FILE_ONLY */ + +/* + This is free and unencumbered software released into the public domain. + + Anyone is free to copy, modify, publish, use, compile, sell, or + distribute this software, either in source code form or as a compiled + binary, for any purpose, commercial or non-commercial, and by any + means. + + In jurisdictions that recognize copyright laws, the author or authors + of this software dedicate any and all copyright interest in the + software to the public domain. We make this dedication for the benefit + of the public at large and to the detriment of our heirs and + successors. We intend this dedication to be an overt act of + relinquishment in perpetuity of all present and future rights to this + software under copyright law. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR + OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + + For more information, please refer to +*/ + diff --git a/src/audio/load.c b/src/audio/load.c index affc6081..e85fcdf5 100644 --- a/src/audio/load.c +++ b/src/audio/load.c @@ -7,6 +7,7 @@ #include "seqplayer.h" #include "../pc/platform.h" +#include "../pc/fs/fs.h" #define ALIGN16(val) (((val) + 0xF) & ~0xF) @@ -875,11 +876,8 @@ void load_sequence_internal(u32 player, u32 seqId, s32 loadAsync) { # include # include static inline void *load_sound_res(const char *path) { - void *data = sys_load_res(path); - if (!data) { - fprintf(stderr, "could not load sound data from '%s'\n", path); - abort(); - } + void *data = fs_load_file(path, NULL); + if (!data) sys_fatal("could not load sound data from '%s'", path); // FIXME: figure out where it is safe to free this shit // can't free it immediately after in audio_init() return data; diff --git a/src/pc/configfile.c b/src/pc/configfile.c index 79deadb6..ab0f166f 100644 --- a/src/pc/configfile.c +++ b/src/pc/configfile.c @@ -5,13 +5,20 @@ #include #include #include -#include + +#if USE_SDL == 2 +# include +# define WINDOWPOS_CENTERED SDL_WINDOWPOS_CENTERED +#else +# define WINDOWPOS_CENTERED 0 +#endif #include "platform.h" #include "configfile.h" #include "cliopts.h" #include "gfx/gfx_screen_config.h" #include "controller/controller_api.h" +#include "fs/fs.h" #define ARRAY_LEN(arr) (sizeof(arr) / sizeof(arr[0])) @@ -38,8 +45,8 @@ struct ConfigOption { // Video/audio stuff ConfigWindow configWindow = { - .x = SDL_WINDOWPOS_CENTERED, - .y = SDL_WINDOWPOS_CENTERED, + .x = WINDOWPOS_CENTERED, + .y = WINDOWPOS_CENTERED, .w = DESIRED_SCREEN_WIDTH, .h = DESIRED_SCREEN_HEIGHT, .vsync = 1, @@ -130,7 +137,7 @@ static const struct ConfigOption options[] = { // Reads an entire line from a file (excluding the newline character) and returns an allocated string // Returns NULL if no lines could be read from the file -static char *read_file_line(FILE *file) { +static char *read_file_line(fs_file_t *file) { char *buffer; size_t bufferSize = 8; size_t offset = 0; // offset in buffer to write @@ -138,7 +145,7 @@ static char *read_file_line(FILE *file) { buffer = malloc(bufferSize); while (1) { // Read a line from the file - if (fgets(buffer + offset, bufferSize - offset, file) == NULL) { + if (fs_readline(file, buffer + offset, bufferSize - offset) == NULL) { free(buffer); return NULL; // Nothing could be read. } @@ -151,7 +158,7 @@ static char *read_file_line(FILE *file) { break; } - if (feof(file)) // EOF was reached + if (fs_eof(file)) // EOF was reached break; // If no newline or EOF was reached, then the whole line wasn't read. @@ -205,24 +212,17 @@ static unsigned int tokenize_string(char *str, int maxTokens, char **tokens) { // Gets the config file path and caches it const char *configfile_name(void) { - static char cfgpath[SYS_MAX_PATH] = { 0 }; - if (!cfgpath[0]) { - if (gCLIOpts.ConfigFile[0]) - snprintf(cfgpath, sizeof(cfgpath), "%s", gCLIOpts.ConfigFile); - else - snprintf(cfgpath, sizeof(cfgpath), "%s/%s", sys_save_path(), CONFIGFILE_DEFAULT); - } - return cfgpath; + return (gCLIOpts.ConfigFile[0]) ? gCLIOpts.ConfigFile : CONFIGFILE_DEFAULT; } // Loads the config file specified by 'filename' void configfile_load(const char *filename) { - FILE *file; + fs_file_t *file; char *line; printf("Loading configuration from '%s'\n", filename); - file = fopen(filename, "r"); + file = fs_open(filename); if (file == NULL) { // Create a new config file and save defaults printf("Config file '%s' not found. Creating it.\n", filename); @@ -286,7 +286,7 @@ void configfile_load(const char *filename) { free(line); } - fclose(file); + fs_close(file); } // Writes the config file to 'filename' @@ -295,7 +295,7 @@ void configfile_save(const char *filename) { printf("Saving configuration to '%s'\n", filename); - file = fopen(filename, "w"); + file = fopen(fs_get_write_path(filename), "w"); if (file == NULL) { // error return; diff --git a/src/pc/controller/controller_sdl.c b/src/pc/controller/controller_sdl.c index 9345daf1..10fe9d71 100644 --- a/src/pc/controller/controller_sdl.c +++ b/src/pc/controller/controller_sdl.c @@ -14,6 +14,7 @@ #include "controller_sdl.h" #include "../configfile.h" #include "../platform.h" +#include "../fs/fs.h" #include "game/level_update.h" @@ -92,15 +93,17 @@ static void controller_sdl_init(void) { } // try loading an external gamecontroller mapping file - char gcpath[SYS_MAX_PATH]; - snprintf(gcpath, sizeof(gcpath), "%s/gamecontrollerdb.txt", sys_save_path()); - int nummaps = SDL_GameControllerAddMappingsFromFile(gcpath); - if (nummaps < 0) { - snprintf(gcpath, sizeof(gcpath), "%s/gamecontrollerdb.txt", sys_data_path()); - nummaps = SDL_GameControllerAddMappingsFromFile(gcpath); + uint64_t gcsize = 0; + void *gcdata = fs_load_file("gamecontrollerdb.txt", &gcsize); + if (gcdata && gcsize) { + SDL_RWops *rw = SDL_RWFromConstMem(gcdata, gcsize); + if (rw) { + int nummaps = SDL_GameControllerAddMappingsFromRW(rw, SDL_TRUE); + if (nummaps >= 0) + printf("loaded %d controller mappings from 'gamecontrollerdb.txt'\n", nummaps); + } + free(gcdata); } - if (nummaps >= 0) - printf("loaded %d controller mappings from '%s'\n", nummaps, gcpath); #ifdef BETTERCAMERA if (newcam_mouse == 1) diff --git a/src/pc/fs/dirtree.c b/src/pc/fs/dirtree.c new file mode 100644 index 00000000..3ebdc5fe --- /dev/null +++ b/src/pc/fs/dirtree.c @@ -0,0 +1,137 @@ +#include +#include +#include +#include + +#include "../platform.h" +#include "fs.h" +#include "dirtree.h" + +static inline uint32_t dirtree_hash(const char *s, size_t len) { + // djb hash + uint32_t hash = 5381; + while (len--) hash = ((hash << 5) + hash) ^ *(s++); + return hash & (FS_NUMBUCKETS - 1); +} + +bool fs_dirtree_init(fs_dirtree_t *tree, const size_t entry_len) { + memset(tree, 0, sizeof(*tree)); + + tree->root = malloc(entry_len); + if (!tree->root) return false; + + tree->root->name = ""; // root + tree->root->is_dir = true; + tree->entry_len = entry_len; + + return true; +} + +void fs_dirtree_free(fs_dirtree_t *tree) { + if (!tree) return; + if (tree->root) free(tree->root); + for (int i = 0; i < FS_NUMBUCKETS; ++i) { + fs_dirtree_entry_t *ent, *next; + for (ent = tree->buckets[i]; ent; ent = next) { + next = ent->next_hash; + free(ent); + } + } +} + +static inline fs_dirtree_entry_t *dirtree_add_ancestors(fs_dirtree_t *tree, char *name) { + fs_dirtree_entry_t *ent = tree->root; + + // look for parent directory + char *last_sep = strrchr(name, '/'); + if (!last_sep) return ent; + *last_sep = 0; + ent = fs_dirtree_find(tree, name); + + if (ent) { + *last_sep = '/'; // put the separator back + return ent; // parent directory already in tree + } + + // add the parent directory + ent = fs_dirtree_add(tree, name, true); + *last_sep = '/'; + + return ent; +} + +fs_dirtree_entry_t *fs_dirtree_add(fs_dirtree_t *tree, char *name, const bool is_dir) { + fs_dirtree_entry_t *ent = fs_dirtree_find(tree, name); + if (ent) return ent; + + // add the parent directory into the tree first + fs_dirtree_entry_t *parent = dirtree_add_ancestors(tree, name); + if (!parent) return NULL; + + // we'll plaster the name at the end of the allocated chunk, after the actual entry + const size_t name_len = strlen(name); + const size_t allocsize = tree->entry_len + name_len + 1; + ent = calloc(1, allocsize); + if (!ent) return NULL; + + ent->name = (const char *)ent + tree->entry_len; + strcpy((char *)ent->name, name); + + const uint32_t hash = dirtree_hash(name, name_len); + ent->next_hash = tree->buckets[hash]; + tree->buckets[hash] = ent; + ent->next_sibling = parent->next_child; + ent->is_dir = is_dir; + parent->next_child = ent; + + return ent; +} + +fs_dirtree_entry_t *fs_dirtree_find(fs_dirtree_t *tree, const char *name) { + if (!name) return NULL; + if (!*name) return tree->root; + + const uint32_t hash = dirtree_hash(name, strlen(name)); + + fs_dirtree_entry_t *ent, *prev = NULL; + for (ent = tree->buckets[hash]; ent; ent = ent->next_hash) { + if (!strcmp(ent->name, name)) { + // if this path is not in front of the hash list, move it to the front + // in case of reccurring searches + if (prev) { + prev->next_hash = ent->next_hash; + ent->next_hash = tree->buckets[hash]; + tree->buckets[hash] = ent; + } + return ent; + } + prev = ent; + } + + return NULL; +} + +static fs_walk_result_t dirtree_walk_impl(fs_dirtree_entry_t *ent, walk_fn_t walkfn, void *user, const bool recur) { + fs_walk_result_t res = FS_WALK_SUCCESS;; + ent = ent->next_child; + while (ent && (res == FS_WALK_SUCCESS)) { + if (ent->is_dir) { + if (recur && ent->next_child) + res = dirtree_walk_impl(ent, walkfn, user, recur); + } else if (!walkfn(user, ent->name)) { + res = FS_WALK_INTERRUPTED; + break; + } + ent = ent->next_sibling; + } + return res; +} + +fs_walk_result_t fs_dirtree_walk(void *pack, const char *base, walk_fn_t walkfn, void *user, const bool recur) { + fs_dirtree_t *tree = (fs_dirtree_t *)pack; + + fs_dirtree_entry_t *ent = fs_dirtree_find(tree, base); + if (!ent) return FS_WALK_NOTFOUND; + + return dirtree_walk_impl(ent, walkfn, user, recur); +} diff --git a/src/pc/fs/dirtree.h b/src/pc/fs/dirtree.h new file mode 100644 index 00000000..6793a3a0 --- /dev/null +++ b/src/pc/fs/dirtree.h @@ -0,0 +1,32 @@ +#ifndef _SM64_DIRTREE_H_ +#define _SM64_DIRTREE_H_ + +#include +#include + +#include "fs.h" + +#define FS_NUMBUCKETS 64 + +typedef struct fs_dirtree_entry_s { + const char *name; + bool is_dir; + struct fs_dirtree_entry_s *next_hash, *next_child, *next_sibling; +} fs_dirtree_entry_t; + +typedef struct { + fs_dirtree_entry_t *root; + fs_dirtree_entry_t *buckets[FS_NUMBUCKETS]; + size_t entry_len; +} fs_dirtree_t; + +bool fs_dirtree_init(fs_dirtree_t *tree, const size_t entry_len); +void fs_dirtree_free(fs_dirtree_t *tree); + +fs_dirtree_entry_t *fs_dirtree_add(fs_dirtree_t *tree, char *name, const bool is_dir); +fs_dirtree_entry_t *fs_dirtree_find(fs_dirtree_t *tree, const char *name); + +// the first arg is void* so this could be used in walk() methods of various packtypes +fs_walk_result_t fs_dirtree_walk(void *tree, const char *base, walk_fn_t walkfn, void *user, const bool recur); + +#endif // _SM64_DIRTREE_H_ diff --git a/src/pc/fs/fs.c b/src/pc/fs/fs.c new file mode 100644 index 00000000..d125a60b --- /dev/null +++ b/src/pc/fs/fs.c @@ -0,0 +1,443 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#endif + +#include "macros.h" +#include "../platform.h" +#include "fs.h" + +char fs_gamedir[SYS_MAX_PATH] = ""; +char fs_writepath[SYS_MAX_PATH] = ""; + +struct fs_dir_s { + void *pack; + const char *realpath; + fs_packtype_t *packer; + struct fs_dir_s *prev, *next; +}; + +extern fs_packtype_t fs_packtype_dir; +extern fs_packtype_t fs_packtype_zip; + +static fs_packtype_t *fs_packers[] = { + &fs_packtype_dir, + &fs_packtype_zip, +}; + +static fs_dir_t *fs_searchpaths = NULL; + +static inline fs_dir_t *fs_find_dir(const char *realpath) { + for (fs_dir_t *dir = fs_searchpaths; dir; dir = dir->next) + if (!sys_strcasecmp(realpath, dir->realpath)) + return dir; + return NULL; +} + +static int mount_cmp(const void *p1, const void *p2) { + const char *s1 = *(const char **)p1; + const char *s2 = *(const char **)p2; + + // check if one or both of these are basepacks + const int plen = strlen(FS_BASEPACK_PREFIX); + const bool is_base1 = !strncmp(s1, FS_BASEPACK_PREFIX, plen); + const bool is_base2 = !strncmp(s2, FS_BASEPACK_PREFIX, plen); + + // if both are basepacks, compare the postfixes only + if (is_base1 && is_base2) return strcmp(s1 + plen, s2 + plen); + // if only one is a basepack, it goes first + if (is_base1) return -1; + if (is_base2) return 1; + // otherwise strcmp order + return strcmp(s1, s2); +} + +static void scan_path_dir(const char *ropath, const char *dir) { + char dirpath[SYS_MAX_PATH]; + snprintf(dirpath, sizeof(dirpath), "%s/%s", ropath, dir); + + if (!fs_sys_dir_exists(dirpath)) return; + + // since filename order in readdir() isn't guaranteed, collect paths and sort them in strcmp() order + // (but with basepacks first) + fs_pathlist_t plist = fs_sys_enumerate(dirpath, false); + if (plist.paths) { + qsort(plist.paths, plist.numpaths, sizeof(char *), mount_cmp); + for (int i = 0; i < plist.numpaths; ++i) + fs_mount(plist.paths[i]); + fs_pathlist_free(&plist); + } + + // mount the directory itself + fs_mount(dirpath); +} + +bool fs_init(const char **rodirs, const char *gamedir, const char *writepath) { + char buf[SYS_MAX_PATH]; + + // expand and remember the write path + strncpy(fs_writepath, fs_convert_path(buf, sizeof(buf), writepath), sizeof(fs_writepath)); + fs_writepath[sizeof(fs_writepath)-1] = 0; + printf("fs: writepath set to `%s`\n", fs_writepath); + + // remember the game directory name + strncpy(fs_gamedir, gamedir, sizeof(fs_gamedir)); + fs_gamedir[sizeof(fs_gamedir)-1] = 0; + printf("fs: gamedir set to `%s`\n", fs_gamedir); + + // first, scan all possible paths and mount all basedirs in them + for (const char **p = rodirs; p && *p; ++p) + scan_path_dir(fs_convert_path(buf, sizeof(buf), *p), FS_BASEDIR); + scan_path_dir(fs_writepath, FS_BASEDIR); + + // then mount all the gamedirs in them, if the game dir isn't the same + if (sys_strcasecmp(FS_BASEDIR, fs_gamedir)) { + for (const char **p = rodirs; p && *p; ++p) + scan_path_dir(fs_convert_path(buf, sizeof(buf), *p), fs_gamedir); + scan_path_dir(fs_writepath, fs_gamedir); + } + + // as a special case, mount writepath itself + fs_mount(fs_writepath); + + return true; +} + +bool fs_mount(const char *realpath) { + if (fs_find_dir(realpath)) + return false; // already mounted + + const char *ext = sys_file_extension(realpath); + void *pack = NULL; + fs_packtype_t *packer = NULL; + bool tried = false; + for (unsigned int i = 0; i < sizeof(fs_packers) / sizeof(fs_packers[0]); ++i) { + if (ext && sys_strcasecmp(ext, fs_packers[i]->extension)) + continue; + tried = true; + pack = fs_packers[i]->mount(realpath); + if (pack) { + packer = fs_packers[i]; + break; + } + } + + if (!pack || !packer) { + if (tried) + fprintf(stderr, "fs: could not mount '%s'\n", realpath); + return false; + } + + fs_dir_t *dir = calloc(1, sizeof(fs_dir_t)); + if (!dir) { + packer->unmount(pack); + return false; + } + + dir->pack = pack; + dir->realpath = sys_strdup(realpath); + dir->packer = packer; + dir->prev = NULL; + dir->next = fs_searchpaths; + if (fs_searchpaths) + fs_searchpaths->prev = dir; + fs_searchpaths = dir; + + printf("fs: mounting '%s'\n", realpath); + + return true; +} + +bool fs_unmount(const char *realpath) { + fs_dir_t *dir = fs_find_dir(realpath); + if (dir) { + dir->packer->unmount(dir->pack); + free((void *)dir->realpath); + if (dir->prev) dir->prev->next = dir->next; + if (dir->next) dir->next->prev = dir->prev; + if (dir == fs_searchpaths) fs_searchpaths = dir->next; + free(dir); + return true; + } + return false; +} + +fs_walk_result_t fs_walk(const char *base, walk_fn_t walkfn, void *user, const bool recur) { + bool found = false; + for (fs_dir_t *dir = fs_searchpaths; dir; dir = dir->next) { + fs_walk_result_t res = dir->packer->walk(dir->pack, base, walkfn, user, recur); + if (res == FS_WALK_INTERRUPTED) + return res; + if (res != FS_WALK_NOTFOUND) + found = true; + } + return found ? FS_WALK_SUCCESS : FS_WALK_NOTFOUND; +} + +bool fs_is_file(const char *fname) { + for (fs_dir_t *dir = fs_searchpaths; dir; dir = dir->next) { + if (dir->packer->is_file(dir->pack, fname)) + return true; + } + return false; +} + +bool fs_is_dir(const char *fname) { + for (fs_dir_t *dir = fs_searchpaths; dir; dir = dir->next) { + if (dir->packer->is_dir(dir->pack, fname)) + return true; + } + return false; +} + +fs_file_t *fs_open(const char *vpath) { + for (fs_dir_t *dir = fs_searchpaths; dir; dir = dir->next) { + fs_file_t *f = dir->packer->open(dir->pack, vpath); + if (f) { + f->parent = dir; + return f; + } + } + return NULL; +} + +void fs_close(fs_file_t *file) { + if (!file) return; + file->parent->packer->close(file->parent->pack, file); +} + +int64_t fs_read(fs_file_t *file, void *buf, const uint64_t size) { + if (!file) return -1; + return file->parent->packer->read(file->parent->pack, file, buf, size); +} + +bool fs_seek(fs_file_t *file, const int64_t ofs) { + if (!file) return -1; + return file->parent->packer->seek(file->parent->pack, file, ofs); +} + +int64_t fs_tell(fs_file_t *file) { + if (!file) return -1; + return file->parent->packer->tell(file->parent->pack, file); +} + +int64_t fs_size(fs_file_t *file) { + if (!file) return -1; + return file->parent->packer->size(file->parent->pack, file); +} + +bool fs_eof(fs_file_t *file) { + if (!file) return true; + return file->parent->packer->eof(file->parent->pack, file); +} + +struct matchdata_s { + const char *prefix; + size_t prefix_len; + char *dst; + size_t dst_len; +}; + +static bool match_walk(void *user, const char *path) { + struct matchdata_s *data = (struct matchdata_s *)user; + if (!strncmp(path, data->prefix, data->prefix_len)) { + // found our lad, copy path to destination and terminate + strncpy(data->dst, path, data->dst_len); + data->dst[data->dst_len - 1] = 0; + return false; + } + return true; +} + +const char *fs_match(char *outname, const size_t outlen, const char *prefix) { + struct matchdata_s data = { + .prefix = prefix, + .prefix_len = strlen(prefix), + .dst = outname, + .dst_len = outlen, + }; + + if (fs_walk("", match_walk, &data, true) == FS_WALK_INTERRUPTED) + return outname; + + return NULL; +} + +static bool enumerate_walk(void *user, const char *path) { + fs_pathlist_t *data = (fs_pathlist_t *)user; + + if (data->listcap == data->numpaths) { + data->listcap *= 2; + char **newpaths = realloc(data->paths, data->listcap * sizeof(char *)); + if (!newpaths) return false; + data->paths = newpaths; + } + + data->paths[data->numpaths++] = sys_strdup(path); + return true; +} + +fs_pathlist_t fs_enumerate(const char *base, const bool recur) { + char **paths = malloc(sizeof(char *) * 32); + fs_pathlist_t pathlist = { paths, 0, 32 }; + + if (!paths) return pathlist; + + if (fs_walk(base, enumerate_walk, &pathlist, recur) == FS_WALK_INTERRUPTED) + fs_pathlist_free(&pathlist); + + return pathlist; +} + +void fs_pathlist_free(fs_pathlist_t *pathlist) { + if (!pathlist || !pathlist->paths) return; + for (int i = 0; i < pathlist->numpaths; ++i) + free(pathlist->paths[i]); + free(pathlist->paths); + pathlist->paths = NULL; + pathlist->numpaths = 0; +} + +const char *fs_readline(fs_file_t *file, char *dst, uint64_t size) { + int64_t rx = 0; + char chr, *p; + + // assume we got buffered input + for (p = dst, size--; size > 0; size--) { + if ((rx = fs_read(file, &chr, 1)) <= 0) + break; + *p++ = chr; + if (chr == '\n') + break; + } + + *p = 0; + if (p == dst || rx <= 0) + return NULL; + + return p; +} + +void *fs_load_file(const char *vpath, uint64_t *outsize) { + fs_file_t *f = fs_open(vpath); + if (!f) return NULL; + + int64_t size = fs_size(f); + if (size <= 0) { + fs_close(f); + return NULL; + } + + void *buf = malloc(size); + if (!buf) { + fs_close(f); + return NULL; + } + + int64_t rx = fs_read(f, buf, size); + fs_close(f); + + if (rx < size) { + free(buf); + return NULL; + } + + if (outsize) *outsize = size; + return buf; +} + +const char *fs_get_write_path(const char *vpath) { + static char path[SYS_MAX_PATH]; + snprintf(path, sizeof(path), "%s/%s", fs_writepath, vpath); + return path; +} + +const char *fs_convert_path(char *buf, const size_t bufsiz, const char *path) { + // ! means "executable directory" + if (path[0] == '!') { + snprintf(buf, bufsiz, "%s%s", sys_exe_path(), path + 1); + } else { + strncpy(buf, path, bufsiz); + buf[bufsiz-1] = 0; + } + + // change all backslashes + for (char *p = buf; *p; ++p) + if (*p == '\\') *p = '/'; + + return buf; +} + +/* these operate on the real file system */ + +bool fs_sys_file_exists(const char *name) { + struct stat st; + return (stat(name, &st) == 0 && S_ISREG(st.st_mode)); +} + +bool fs_sys_dir_exists(const char *name) { + struct stat st; + return (stat(name, &st) == 0 && S_ISDIR(st.st_mode)); +} + +bool fs_sys_walk(const char *base, walk_fn_t walk, void *user, const bool recur) { + char fullpath[SYS_MAX_PATH]; + DIR *dir; + struct dirent *ent; + + if (!(dir = opendir(base))) { + fprintf(stderr, "fs_dir_walk(): could not open `%s`\n", base); + return false; + } + + bool ret = true; + + while ((ent = readdir(dir)) != NULL) { + if (ent->d_name[0] == 0 || ent->d_name[0] == '.') continue; // skip ./.. and hidden files + snprintf(fullpath, sizeof(fullpath), "%s/%s", base, ent->d_name); + if (fs_sys_dir_exists(fullpath)) { + if (recur) { + if (!fs_sys_walk(fullpath, walk, user, recur)) { + ret = false; + break; + } + } + } else { + if (!walk(user, fullpath)) { + ret = false; + break; + } + } + } + + closedir(dir); + return ret; +} + +fs_pathlist_t fs_sys_enumerate(const char *base, const bool recur) { + char **paths = malloc(sizeof(char *) * 32); + fs_pathlist_t pathlist = { paths, 0, 32 }; + + if (!paths) return pathlist; + + if (!fs_sys_walk(base, enumerate_walk, &pathlist, recur)) + fs_pathlist_free(&pathlist); + + return pathlist; +} + +bool fs_sys_mkdir(const char *name) { + #ifdef _WIN32 + return _mkdir(name) == 0; + #else + return mkdir(name, 0777) == 0; + #endif +} diff --git a/src/pc/fs/fs.h b/src/pc/fs/fs.h new file mode 100644 index 00000000..e479cfbf --- /dev/null +++ b/src/pc/fs/fs.h @@ -0,0 +1,136 @@ +#ifndef _SM64_FS_H_ +#define _SM64_FS_H_ + +#include +#include +#include +#include + +#include "../platform.h" + +// FS_BASEDIR is usually defined in the build script +#ifndef FS_BASEDIR +# define FS_BASEDIR "res" +#endif + +#ifndef FS_BASEPACK_PREFIX +# define FS_BASEPACK_PREFIX "base" +#endif + +#define FS_TEXTUREDIR "gfx" +#define FS_SOUNDDIR "sound" + +extern char fs_gamedir[]; +extern char fs_userdir[]; +extern const char *fs_ropaths[]; + +// receives the full path +// should return `true` if traversal should continue +// first arg is user data +typedef bool (*walk_fn_t)(void *, const char *); + +typedef enum { + FS_WALK_SUCCESS = 0, + FS_WALK_INTERRUPTED = 1, + FS_WALK_NOTFOUND = 2, + FS_WALK_ERROR = 4, +} fs_walk_result_t; + +// opaque searchpath directory type +typedef struct fs_dir_s fs_dir_t; + +// virtual file handle +typedef struct fs_file_s { + void *handle; // opaque packtype-defined data + fs_dir_t *parent; // directory containing this file +} fs_file_t; + +// list of paths; returned by fs_enumerate() +typedef struct { + char **paths; + int numpaths; + int listcap; +} fs_pathlist_t; + +typedef struct { + const char *extension; // file extensions of this pack type + + void *(*mount)(const char *rpath); // open and initialize pack at real path `rpath` + void (*unmount)(void *pack); // free pack + + // walks the specified directory inside this pack, calling walkfn for each file + // returns FS_WALK_SUCCESS if the directory was successfully opened and walk() didn't ever return false + // returns FS_WALK_INTERRUPTED if the traversal started but walk() returned false at some point + // if recur is true, will recurse into subfolders + fs_walk_result_t (*walk)(void *pack, const char *base, walk_fn_t walkfn, void *user, const bool recur); + + bool (*is_file)(void *pack, const char *path); // returns true if `path` exists in this pack and is a file + bool (*is_dir)(void *pack, const char *path); // returns true if `path` exists in this pack and is a directory + + // file I/O functions; paths are virtual + fs_file_t *(*open)(void *pack, const char *path); // opens a virtual file contained in this pack for reading, returns NULL in case of error + int64_t (*read)(void *pack, fs_file_t *file, void *buf, const uint64_t size); // returns -1 in case of error + bool (*seek)(void *pack, fs_file_t *file, const int64_t ofs); // returns true if seek succeeded + int64_t (*tell)(void *pack, fs_file_t *file); // returns -1 in case of error, current virtual file position otherwise + int64_t (*size)(void *pack, fs_file_t *file); // returns -1 in case of error, size of the (uncompressed) file otherwise + bool (*eof)(void *pack, fs_file_t *file); // returns true if there's nothing more to read + void (*close)(void *pack, fs_file_t *file); // closes a virtual file previously opened with ->open() +} fs_packtype_t; + +// takes the supplied NULL-terminated list of read-only directories and mounts all the packs in them, +// then mounts the directories themselves, then mounts all the packs in `gamedir`, then mounts `gamedir` itself, +// then does the same with `userdir` +// initializes the `fs_gamedir` and `fs_userdir` variables +bool fs_init(const char **rodirs, const char *gamedir, const char *userdir); + +// mounts the pack at physical path `realpath` to the root of the filesystem +// packs mounted later take priority over packs mounted earlier +bool fs_mount(const char *realpath); + +// removes the pack at physical path from the virtual filesystem +bool fs_unmount(const char *realpath); + +/* generalized filesystem functions that call matching packtype functions for each pack in the searchpath */ + +// FIXME: this can walk in unorthodox patterns, since it goes through mountpoints linearly +fs_walk_result_t fs_walk(const char *base, walk_fn_t walkfn, void *user, const bool recur); + +// returns a list of files in the `base` directory +fs_pathlist_t fs_enumerate(const char *base, const bool recur); +// call this on a list returned by fs_enumerate() to free it +void fs_pathlist_free(fs_pathlist_t *pathlist); + +bool fs_is_file(const char *fname); +bool fs_is_dir(const char *fname); + +fs_file_t *fs_open(const char *vpath); +void fs_close(fs_file_t *file); +int64_t fs_read(fs_file_t *file, void *buf, const uint64_t size); +const char *fs_readline(fs_file_t *file, char *dst, const uint64_t size); +bool fs_seek(fs_file_t *file, const int64_t ofs); +int64_t fs_tell(fs_file_t *file); +int64_t fs_size(fs_file_t *file); +bool fs_eof(fs_file_t *file); + +void *fs_load_file(const char *vpath, uint64_t *outsize); +const char *fs_readline(fs_file_t *file, char *dst, uint64_t size); + +// tries to find the first file with the filename that starts with `prefix` +// puts full filename into `outname` and returns it or returns NULL if nothing matches +const char *fs_match(char *outname, const size_t outlen, const char *prefix); + +// takes a virtual path and prepends the write path to it +const char *fs_get_write_path(const char *vpath); + +// expands special chars in paths and changes backslashes to forward slashes +const char *fs_convert_path(char *buf, const size_t bufsiz, const char *path); + +/* these operate on the real filesystem and are used by fs_packtype_dir */ + +bool fs_sys_walk(const char *base, walk_fn_t walk, void *user, const bool recur); +fs_pathlist_t fs_sys_enumerate(const char *base, const bool recur); +bool fs_sys_file_exists(const char *name); +bool fs_sys_dir_exists(const char *name); +bool fs_sys_mkdir(const char *name); // creates with 0777 by default + +#endif // _SM64_FS_H_ diff --git a/src/pc/fs/fs_packtype_dir.c b/src/pc/fs/fs_packtype_dir.c new file mode 100644 index 00000000..afd0c97a --- /dev/null +++ b/src/pc/fs/fs_packtype_dir.c @@ -0,0 +1,117 @@ +#include +#include +#include +#include +#include + +#include "macros.h" +#include "../platform.h" +#include "fs.h" + +static void *pack_dir_mount(const char *realpath) { + if (!fs_sys_dir_exists(realpath)) + return NULL; + // the pack is actually just the real folder path + void *pack = (void *)sys_strdup(realpath); + return pack; +} + +static void pack_dir_unmount(void *pack) { + free(pack); +} + +struct walkdata_s { + size_t baselen; + walk_fn_t userwalk; + void *userdata; +}; + +// wrap the actual user walk function to return virtual paths instead of real paths +static bool packdir_walkfn(void *userdata, const char *path) { + struct walkdata_s *walk = (struct walkdata_s *)userdata; + return walk->userwalk(walk->userdata, path + walk->baselen); +} + +static fs_walk_result_t pack_dir_walk(void *pack, const char *base, walk_fn_t walkfn, void *user, const bool recur) { + char path[SYS_MAX_PATH]; + snprintf(path, SYS_MAX_PATH, "%s/%s", (const char *)pack, base); + + if (!fs_sys_dir_exists(path)) + return FS_WALK_NOTFOUND; + + struct walkdata_s walkdata = { strlen((const char *)pack) + 1, walkfn, user }; + return fs_sys_walk(path, packdir_walkfn, &walkdata, recur); +} + +static bool pack_dir_is_file(void *pack, const char *fname) { + char path[SYS_MAX_PATH]; + snprintf(path, sizeof(path), "%s/%s", (const char *)pack, fname); + return fs_sys_dir_exists(path); +} + +static bool pack_dir_is_dir(void *pack, const char *fname) { + char path[SYS_MAX_PATH]; + snprintf(path, sizeof(path), "%s/%s", (const char *)pack, fname); + return fs_sys_file_exists(path); +} + +static fs_file_t *pack_dir_open(void *pack, const char *vpath) { + char path[SYS_MAX_PATH]; + snprintf(path, sizeof(path), "%s/%s", (const char *)pack, vpath); + + FILE *f = fopen(path, "rb"); + if (!f) return NULL; + + fs_file_t *fsfile = malloc(sizeof(fs_file_t)); + if (!fsfile) { fclose(f); return NULL; } + + fsfile->parent = NULL; + fsfile->handle = f; + + return fsfile; +} + +static void pack_dir_close(UNUSED void *pack, fs_file_t *file) { + fclose((FILE *)file->handle); + free(file); +} + +static int64_t pack_dir_read(UNUSED void *pack, fs_file_t *file, void *buf, const uint64_t size) { + return fread(buf, 1, size, (FILE *)file->handle); +} + +static bool pack_dir_seek(UNUSED void *pack, fs_file_t *file, const int64_t ofs) { + return fseek((FILE *)file->handle, ofs, SEEK_SET) == 0; +} + +static int64_t pack_dir_tell(UNUSED void *pack, fs_file_t *file) { + return ftell((FILE *)file->handle); +} + +static int64_t pack_dir_size(UNUSED void *pack, fs_file_t *file) { + int64_t oldofs = ftell((FILE *)file->handle); + fseek((FILE *)file->handle, 0, SEEK_END); + int64_t size = ftell((FILE *)file->handle); + fseek((FILE *)file->handle, oldofs, SEEK_SET); + return size; +} + +static bool pack_dir_eof(UNUSED void *pack, fs_file_t *file) { + return feof((FILE *)file->handle); +} + +fs_packtype_t fs_packtype_dir = { + "", + pack_dir_mount, + pack_dir_unmount, + pack_dir_walk, + pack_dir_is_file, + pack_dir_is_dir, + pack_dir_open, + pack_dir_read, + pack_dir_seek, + pack_dir_tell, + pack_dir_size, + pack_dir_eof, + pack_dir_close, +}; diff --git a/src/pc/fs/fs_packtype_zip.c b/src/pc/fs/fs_packtype_zip.c new file mode 100644 index 00000000..d331cbe1 --- /dev/null +++ b/src/pc/fs/fs_packtype_zip.c @@ -0,0 +1,486 @@ +#include +#include +#include +#include +#include +#include + +#include + +#include "macros.h" +#include "../platform.h" +#include "fs.h" +#include "dirtree.h" + +#define ZIP_BUFSIZE 16384 +#define ZIP_EOCD_BUFSIZE 65578 + +#define ZIP_LFH_SIG 0x04034b50 +#define ZIP_CDH_SIG 0x02014b50 +#define ZIP_EOCD_SIG 0x06054b50 + +typedef struct { + fs_dirtree_t tree; // this should always be first, so this could be used as a dirtree root + const char *realpath; // physical path to the zip file + FILE *zipf; // open zip file handle, if any +} zip_pack_t; + +typedef struct { + fs_dirtree_entry_t tree; // this should always be first, so this could be used as a dirtree entry + uint64_t ofs; // offset to compressed data in zip + uint16_t bits; // general purpose zip flags + uint16_t comptype; // compression method + uint32_t crc; // CRC-32 + uint64_t comp_size; // size of compressed data in zip + uint64_t uncomp_size; // size of decompressed data + uint16_t attr_int; // internal attributes + uint32_t attr_ext; // external attributes + bool ofs_fixed; // if true, `ofs` points to the file data, otherwise to LFH +} zip_entry_t; + +typedef struct { + zip_entry_t *entry; // the dirtree entry of this file + uint32_t comp_pos; // read position in compressed data + uint32_t uncomp_pos; // read position in uncompressed data + uint8_t *buffer; // decompression buffer (if compressed) + z_stream zstream; // tinfl zlib stream + FILE *fstream; // duplicate of zipf of the parent zip file +} zip_file_t; + +static int64_t zip_find_eocd(FILE *f, int64_t *outlen) { + // the EOCD is somewhere in the last 65557 bytes of the file + // get the total file size + fseek(f, 0, SEEK_END); + const int64_t fsize = ftell(f); + if (fsize <= 16) return -1; // probably not a zip + + const int64_t rx = (fsize < ZIP_EOCD_BUFSIZE ? fsize : ZIP_EOCD_BUFSIZE); + uint8_t *buf = malloc(rx); + if (!buf) return -1; + + // read that entire chunk and search for EOCD backwards from the end + fseek(f, fsize - rx, SEEK_SET); + if (fread(buf, rx, 1, f)) { + for (int64_t i = rx - 8; i >= 0; --i) { + if ((buf[i + 0] == 0x50) && (buf[i + 1] == 0x4B) && + (buf[i + 2] == 0x05) && (buf[i + 3] == 0x06)) { + // gotem + free(buf); + if (outlen) *outlen = fsize; + return fsize - rx + i; + } + } + } + + free(buf); + return -1; +} + +static bool zip_parse_eocd(FILE *f, uint64_t *cdir_ofs, uint64_t *data_ofs, uint64_t *count) { + int64_t fsize = 0; + + // EOCD record struct + struct eocd_s { + uint32_t sig; + uint16_t this_disk; + uint16_t cdir_disk; + uint16_t disk_entry_count; + uint16_t total_entry_count; + uint32_t cdir_size; + uint32_t cdir_ofs; + uint16_t comment_len; + // zip comment follows + } __attribute__((__packed__)); + struct eocd_s eocd; + + // find the EOCD and seek to it + int64_t pos = zip_find_eocd(f, &fsize); + if (pos < 0) return false; + fseek(f, pos, SEEK_SET); + + // read it + if (!fread(&eocd, sizeof(eocd), 1, f)) return false; + + // double check the sig + if (LE_TO_HOST32(eocd.sig) != ZIP_EOCD_SIG) return false; + + // disks should all be 0 + if (eocd.this_disk || eocd.cdir_disk) return false; + + // total entry count should be the same as disk entry count + if (eocd.disk_entry_count != eocd.total_entry_count) return false; + + *count = LE_TO_HOST16(eocd.total_entry_count); + *cdir_ofs = LE_TO_HOST32(eocd.cdir_ofs); + eocd.cdir_size = LE_TO_HOST32(eocd.cdir_size); + + // end of central dir can't be before central dir + if ((uint64_t)pos < *cdir_ofs + eocd.cdir_size) return false; + + *data_ofs = (uint64_t)(pos - (*cdir_ofs + eocd.cdir_size)); + *cdir_ofs += *data_ofs; + + // make sure end of comment matches end of file + eocd.comment_len = LE_TO_HOST16(eocd.comment_len); + return ((pos + 22 + eocd.comment_len) == fsize); +} + +static bool zip_fixup_offset(zip_file_t *zipfile) { + // LFH record struct + struct lfh_s { + uint32_t sig; + uint16_t version_required; + uint16_t bits; + uint16_t comptype; + uint16_t mod_time; + uint16_t mod_date; + uint32_t crc; + uint32_t comp_size; + uint32_t uncomp_size; + uint16_t fname_len; + uint16_t extra_len; + // file name, extra field and data follow + } __attribute__((__packed__)); + + struct lfh_s lfh; + + zip_entry_t *ent = zipfile->entry; + + fseek(zipfile->fstream, ent->ofs, SEEK_SET); + if (!fread(&lfh, sizeof(lfh), 1, zipfile->fstream)) return false; + + // we only need these two + lfh.fname_len = LE_TO_HOST16(lfh.fname_len); + lfh.extra_len = LE_TO_HOST16(lfh.extra_len); + + // ofs will now point to actual data + ent->ofs += sizeof(lfh) + lfh.fname_len + lfh.extra_len; + ent->ofs_fixed = true; // only need to do this once + + return true; +} + +static zip_entry_t *zip_load_entry(FILE *f, fs_dirtree_t *tree, const uint64_t data_ofs) { + // CDH record struct + struct cdh_s { + uint32_t sig; + uint16_t version_used; + uint16_t version_required; + uint16_t bits; + uint16_t comptype; + uint16_t mod_time; + uint16_t mod_date; + uint32_t crc; + uint32_t comp_size; + uint32_t uncomp_size; + uint16_t fname_len; + uint16_t extra_len; + uint16_t comment_len; + uint16_t start_disk; + uint16_t attr_int; + uint32_t attr_ext; + uint32_t lfh_ofs; + // file name, extra field and comment follow + } __attribute__((__packed__)); + + struct cdh_s cdh; + zip_entry_t zipent; + + memset(&zipent, 0, sizeof(zipent)); + + if (!fread(&cdh, sizeof(cdh), 1, f)) return NULL; + + // check cdir entry header signature + if (LE_TO_HOST32(cdh.sig) != ZIP_CDH_SIG) return NULL; + + // byteswap and copy some important fields + zipent.bits = LE_TO_HOST16(cdh.bits); + zipent.comptype = LE_TO_HOST16(cdh.comptype); + zipent.crc = LE_TO_HOST32(cdh.crc); + zipent.comp_size = LE_TO_HOST32(cdh.comp_size); + zipent.uncomp_size = LE_TO_HOST32(cdh.uncomp_size); + zipent.ofs = LE_TO_HOST32(cdh.lfh_ofs); + zipent.attr_int = LE_TO_HOST16(cdh.attr_int); + zipent.attr_ext = LE_TO_HOST32(cdh.attr_ext); + cdh.fname_len = LE_TO_HOST16(cdh.fname_len); + cdh.comment_len = LE_TO_HOST16(cdh.comment_len); + cdh.extra_len = LE_TO_HOST16(cdh.extra_len); + + // read the name + char *name = calloc(1, cdh.fname_len + 1); + if (!name) return NULL; + if (!fread(name, cdh.fname_len, 1, f)) { free(name); return NULL; } + + // this is a directory if the name ends in a path separator + bool is_dir = false; + if (name[cdh.fname_len - 1] == '/') { + is_dir = true; + name[cdh.fname_len - 1] = 0; + } + name[cdh.fname_len] = 0; + + // add to directory tree + zip_entry_t *retent = (zip_entry_t *)fs_dirtree_add(tree, name, is_dir); + free(name); + if (!retent) return NULL; + + // copy the data we read into the new entry + zipent.tree = retent->tree; + memcpy(retent, &zipent, sizeof(zipent)); + + // this points to the LFH now; will be fixed up on file open + // while the CDH includes an "extra field length" field, it's usually different + retent->ofs += data_ofs; + + // skip to the next CDH + fseek(f, cdh.extra_len + cdh.comment_len, SEEK_CUR); + + return retent; +} + +static inline bool zip_load_entries(FILE *f, fs_dirtree_t *tree, const uint64_t cdir_ofs, const uint64_t data_ofs, const uint64_t count) { + fseek(f, cdir_ofs, SEEK_SET); + for (uint64_t i = 0; i < count; ++i) { + if (!zip_load_entry(f, tree, data_ofs)) + return false; + } + return true; +} + +static inline bool is_zip(FILE *f) { + uint32_t sig = 0; + if (fread(&sig, sizeof(sig), 1, f)) { + // the first LFH might be at the start of the zip + if (LE_TO_HOST32(sig) == ZIP_LFH_SIG) + return true; + // no signature, might still be a zip because fuck you + // the only way now is to try and find the end of central directory + return zip_find_eocd(f, NULL) >= 0; + } + return false; +} + +static void *pack_zip_mount(const char *realpath) { + uint64_t cdir_ofs, data_ofs, count; + zip_pack_t *pack = NULL; + FILE *f = NULL; + + f = fopen(realpath, "rb"); + if (!f) goto _fail; + + if (!is_zip(f)) goto _fail; + + pack = calloc(1, sizeof(zip_pack_t)); + if (!pack) goto _fail; + + if (!zip_parse_eocd(f, &cdir_ofs, &data_ofs, &count)) + goto _fail; + + if (!fs_dirtree_init(&pack->tree, sizeof(zip_entry_t))) + goto _fail; + + if (!zip_load_entries(f, &pack->tree, cdir_ofs, data_ofs, count)) + goto _fail; + + pack->realpath = sys_strdup(realpath); + pack->zipf = f; + + return pack; + +_fail: + if (f) fclose(f); + if (pack) free(pack); + return NULL; +} + +static void pack_zip_unmount(void *pack) { + zip_pack_t *zip = (zip_pack_t *)pack; + fs_dirtree_free(&zip->tree); + if (zip->realpath) free((void *)zip->realpath); + if (zip->zipf) fclose(zip->zipf); + free(zip); +} + +static bool pack_zip_is_file(void *pack, const char *fname) { + zip_entry_t *ent = (zip_entry_t *)fs_dirtree_find((fs_dirtree_t *)pack, fname); + return ent && !ent->tree.is_dir; +} + +static bool pack_zip_is_dir(void *pack, const char *fname) { + zip_entry_t *ent = (zip_entry_t *)fs_dirtree_find((fs_dirtree_t *)pack, fname); + return ent && ent->tree.is_dir; +} + +static inline void pack_zip_close_zipfile(zip_file_t *zipfile) { + if (zipfile->buffer) { + inflateEnd(&zipfile->zstream); + free(zipfile->buffer); + } + if (zipfile->fstream) fclose(zipfile->fstream); + free(zipfile); +} + +static fs_file_t *pack_zip_open(void *pack, const char *vpath) { + fs_file_t *fsfile = NULL; + zip_file_t *zipfile = NULL; + zip_pack_t *zip = (zip_pack_t *)pack; + zip_entry_t *ent = (zip_entry_t *)fs_dirtree_find((fs_dirtree_t *)zip, vpath); + if (!ent || ent->tree.is_dir) goto _fail; // we're expecting a fucking file here + + zipfile = calloc(1, sizeof(zip_file_t)); + if (!zipfile) goto _fail; + zipfile->entry = ent; + + // obtain an additional file descriptor + // fdopen(dup(fileno())) is not very portable and might not create separate state + zipfile->fstream = fopen(zip->realpath, "rb"); + if (!zipfile->fstream) goto _fail; + + // make ent->ofs point to the actual file data if it doesn't already + if (!ent->ofs_fixed) + if (!zip_fixup_offset(zipfile)) + goto _fail; // this shouldn't generally happen but oh well + + // if there's compression, assume it's zlib + if (ent->comptype != 0) { + zipfile->buffer = malloc(ZIP_BUFSIZE); + if (!zipfile->buffer) + goto _fail; + if (inflateInit2(&zipfile->zstream, -MAX_WBITS) != Z_OK) + goto _fail; + } + + fsfile = malloc(sizeof(fs_file_t)); + if (!fsfile) goto _fail; + fsfile->handle = zipfile; + fsfile->parent = NULL; + + // point to the start of the file data + fseek(zipfile->fstream, ent->ofs, SEEK_SET); + + return fsfile; + +_fail: + if (zipfile) pack_zip_close_zipfile(zipfile); + if (fsfile) free(fsfile); + return NULL; +} + +static void pack_zip_close(UNUSED void *pack, fs_file_t *file) { + if (!file) return; + + zip_file_t *zipfile = (zip_file_t *)file->handle; + if (zipfile) pack_zip_close_zipfile(zipfile); + + free(file); +} + +static int64_t pack_zip_read(UNUSED void *pack, fs_file_t *file, void *buf, const uint64_t size) { + zip_file_t *zipfile = (zip_file_t *)file->handle; + zip_entry_t *ent = zipfile->entry; + + int64_t avail = ent->uncomp_size - zipfile->uncomp_pos; + int64_t max_read = ((int64_t)size > avail) ? avail : (int64_t)size; + int64_t rx = 0; + int err = 0; + + if (max_read == 0) return 0; + + if (ent->comptype == 0) { + // no compression, just read + rx = fread(buf, 1, size, zipfile->fstream); + } else { + zipfile->zstream.next_out = buf; + zipfile->zstream.avail_out = (unsigned int)max_read; + while (rx < max_read) { + const uint32_t before = (uint32_t)zipfile->zstream.total_out; + // check if we ran out of compressed bytes and read more if we did + if (zipfile->zstream.avail_in == 0) { + int32_t comp_rx = ent->comp_size - zipfile->comp_pos; + if (comp_rx > 0) { + if (comp_rx > ZIP_BUFSIZE) comp_rx = ZIP_BUFSIZE; + comp_rx = fread(zipfile->buffer, 1, comp_rx, zipfile->fstream); + if (comp_rx == 0) break; + zipfile->comp_pos += (uint32_t)comp_rx; + zipfile->zstream.next_in = zipfile->buffer; + zipfile->zstream.avail_in = (unsigned int)comp_rx; + } + } + // inflate + err = inflate(&zipfile->zstream, Z_SYNC_FLUSH); + rx += zipfile->zstream.total_out - before; + if (err != Z_OK) break; + } + } + + zipfile->uncomp_pos += rx; + return rx; +} + +static bool pack_zip_seek(UNUSED void *pack, fs_file_t *file, const int64_t ofs) { + zip_file_t *zipfile = (zip_file_t *)file->handle; + zip_entry_t *ent = zipfile->entry; + uint8_t buf[512]; + + if (ofs > (int64_t)ent->uncomp_size) return false; + + if (ent->comptype == 0) { + if (fseek(zipfile->fstream, ofs + ent->ofs, SEEK_SET) == 0) + zipfile->uncomp_pos = ofs; + } else { + // if seeking backwards, gotta redecode the stream from the start until that point + // so we make a copy of the zstream and clear it with a new one + if (ofs < zipfile->uncomp_pos) { + z_stream zstream; + memset(&zstream, 0, sizeof(zstream)); + if (inflateInit2(&zstream, -MAX_WBITS) != Z_OK) + return false; + // reset the underlying file handle back to the start + if (fseek(zipfile->fstream, ent->ofs, SEEK_SET) != 0) + return false; + // free and replace the old one + inflateEnd(&zipfile->zstream); + memcpy(&zipfile->zstream, &zstream, sizeof(zstream)); + zipfile->uncomp_pos = zipfile->comp_pos = 0; + } + // continue decoding the stream until we hit the new offset + while (zipfile->uncomp_pos != ofs) { + uint32_t max_read = (uint32_t)(ofs - zipfile->uncomp_pos); + if (max_read > sizeof(buf)) max_read = sizeof(buf); + if (pack_zip_read(pack, file, buf, max_read) != max_read) + return false; + } + } + + return true; +} + +static int64_t pack_zip_tell(UNUSED void *pack, fs_file_t *file) { + return ((zip_file_t *)file->handle)->uncomp_pos; +} + +static int64_t pack_zip_size(UNUSED void *pack, fs_file_t *file) { + zip_file_t *zipfile = (zip_file_t *)file->handle; + return zipfile->entry->uncomp_size; +} + +static bool pack_zip_eof(UNUSED void *pack, fs_file_t *file) { + zip_file_t *zipfile = (zip_file_t *)file->handle; + return zipfile->uncomp_pos >= zipfile->entry->uncomp_size; +} + +fs_packtype_t fs_packtype_zip = { + "zip", + pack_zip_mount, + pack_zip_unmount, + fs_dirtree_walk, + pack_zip_is_file, + pack_zip_is_dir, + pack_zip_open, + pack_zip_read, + pack_zip_seek, + pack_zip_tell, + pack_zip_size, + pack_zip_eof, + pack_zip_close, +}; diff --git a/src/pc/gfx/gfx_opengl.c b/src/pc/gfx/gfx_opengl.c index 7909d341..a0c27bed 100644 --- a/src/pc/gfx/gfx_opengl.c +++ b/src/pc/gfx/gfx_opengl.c @@ -317,7 +317,7 @@ static struct ShaderProgram *gfx_opengl_create_and_load_new_shader(uint32_t shad fprintf(stderr, "Vertex shader compilation failed\n"); glGetShaderInfoLog(vertex_shader, max_length, &max_length, &error_log[0]); fprintf(stderr, "%s\n", &error_log[0]); - abort(); + sys_fatal("vertex shader compilation failed (see terminal)"); } GLuint fragment_shader = glCreateShader(GL_FRAGMENT_SHADER); @@ -331,7 +331,7 @@ static struct ShaderProgram *gfx_opengl_create_and_load_new_shader(uint32_t shad fprintf(stderr, "Fragment shader compilation failed\n"); glGetShaderInfoLog(fragment_shader, max_length, &max_length, &error_log[0]); fprintf(stderr, "%s\n", &error_log[0]); - abort(); + sys_fatal("fragment shader compilation failed (see terminal)"); } GLuint shader_program = glCreateProgram(); diff --git a/src/pc/gfx/gfx_opengl_legacy.c b/src/pc/gfx/gfx_opengl_legacy.c index 21dc4a00..6ff04a78 100644 --- a/src/pc/gfx/gfx_opengl_legacy.c +++ b/src/pc/gfx/gfx_opengl_legacy.c @@ -516,17 +516,16 @@ static void gfx_opengl_init(void) { int vmajor, vminor; bool is_es = false; gl_get_version(&vmajor, &vminor, &is_es); - if (vmajor < 2 && vminor < 2 && !is_es) { - fprintf(stderr, "OpenGL 1.2+ is required. Reported version: %s%d.%d\n", is_es ? "ES" : "", vmajor, vminor); - abort(); - } + if (vmajor < 2 && vminor < 2 && !is_es) + sys_fatal("OpenGL 1.2+ is required. Reported version: %s%d.%d\n", is_es ? "ES" : "", vmajor, vminor); // check extensions that we need const bool supported = gl_check_ext("GL_ARB_multitexture") && gl_check_ext("GL_ARB_texture_env_combine"); - if (!supported) abort(); + if (!supported) + sys_fatal("required GL extensions are not supported"); gl_adv_fog = false; diff --git a/src/pc/gfx/gfx_pc.c b/src/pc/gfx/gfx_pc.c index 1e86ec5a..8e8a7e51 100644 --- a/src/pc/gfx/gfx_pc.c +++ b/src/pc/gfx/gfx_pc.c @@ -26,6 +26,7 @@ #include "../platform.h" #include "../configfile.h" +#include "../fs/fs.h" #define SUPPORT_CHECK(x) assert(x) @@ -494,6 +495,28 @@ static void import_texture_ci8(int tile) { #else // EXTERNAL_DATA +static inline void load_texture(const char *fullpath) { + int w, h; + u64 imgsize = 0; + u8 *imgdata = fs_load_file(fullpath, &imgsize); + if (!imgdata) { + fprintf(stderr, "could not open texture: `%s`\n", fullpath); + return; + } + + // TODO: implement stbi_callbacks or some shit instead of loading the whole texture + u8 *data = stbi_load_from_memory(imgdata, imgsize, &w, &h, NULL, 4); + free(imgdata); + if (!data) { + fprintf(stderr, "could not load texture: `%s`\n", fullpath); + return; + } + + gfx_rapi->upload_texture(data, w, h); + stbi_image_free(data); // don't need this anymore +} + + // this is taken straight from n64graphics static bool texname_to_texformat(const char *name, u8 *fmt, u8 *siz) { static const struct { @@ -531,7 +554,7 @@ static bool texname_to_texformat(const char *name, u8 *fmt, u8 *siz) { // calls import_texture() on every texture in the res folder // we can get the format and size from the texture files // and then cache them using gfx_texture_cache_lookup -static bool preload_texture(const char *path) { +static bool preload_texture(void *user, const char *path) { // strip off the extension char texname[SYS_MAX_PATH]; strncpy(texname, path, sizeof(texname)); @@ -546,55 +569,20 @@ static bool preload_texture(const char *path) { return true; // just skip it, might be a stray skybox or something } - // strip off the data path - const char *datapath = sys_data_path(); - const unsigned int datalen = strlen(datapath); - const char *actualname = (!strncmp(texname, datapath, datalen)) ? - texname + datalen + 1 : texname; - // skip any separators - while (*actualname == '/' || *actualname == '\\') ++actualname; + char *actualname = texname; + // strip off the prefix // TODO: make a fs_ function for this shit + if (!strncmp(FS_TEXTUREDIR "/", actualname, 4)) actualname += 4; // this will be stored in the hashtable, so make a copy actualname = sys_strdup(actualname); assert(actualname); struct TextureHashmapNode *n; - if (!gfx_texture_cache_lookup(0, &n, actualname, fmt, siz)) { - // new texture, load it - int w, h; - u8 *data = stbi_load(path, &w, &h, NULL, 4); - if (!data) { - fprintf(stderr, "could not load texture: `%s`\n", path); - return false; - } - // upload it - gfx_rapi->upload_texture(data, w, h); - stbi_image_free(data); - } + if (!gfx_texture_cache_lookup(0, &n, actualname, fmt, siz)) + load_texture(path); // new texture, load it return true; } -static inline void load_texture(const char *name) { - static char fpath[SYS_MAX_PATH]; - int w, h; - const char *texname = name; - - if (!texname[0]) { - fprintf(stderr, "empty texture name at %p\n", texname); - return; - } - - snprintf(fpath, sizeof(fpath), "%s/%s.png", sys_data_path(), texname); - u8 *data = stbi_load(fpath, &w, &h, NULL, 4); - if (!data) { - fprintf(stderr, "could not load texture: `%s`\n", fpath); - return; - } - - gfx_rapi->upload_texture(data, w, h); - stbi_image_free(data); // don't need this anymore -} - #endif // EXTERNAL_DATA static void import_texture(int tile) { @@ -614,7 +602,9 @@ static void import_texture(int tile) { #ifdef EXTERNAL_DATA // the "texture data" is actually a C string with the path to our texture in it // load it from an external image in our data path - load_texture((const char*)rdp.loaded_texture[tile].addr); + char texname[SYS_MAX_PATH]; + snprintf(texname, sizeof(texname), FS_TEXTUREDIR "/%s.png", (const char*)rdp.loaded_texture[tile].addr); + load_texture(texname); #else // the texture data is actual texture data int t0 = get_time(); @@ -625,7 +615,7 @@ static void import_texture(int tile) { else if (siz == G_IM_SIZ_16b) { import_texture_rgba16(tile); } else { - abort(); + sys_fatal("unsupported RGBA texture size: %u", siz); } } else if (fmt == G_IM_FMT_IA) { if (siz == G_IM_SIZ_4b) { @@ -635,7 +625,7 @@ static void import_texture(int tile) { } else if (siz == G_IM_SIZ_16b) { import_texture_ia16(tile); } else { - abort(); + sys_fatal("unsupported IA texture size: %u", siz); } } else if (fmt == G_IM_FMT_CI) { if (siz == G_IM_SIZ_4b) { @@ -643,7 +633,7 @@ static void import_texture(int tile) { } else if (siz == G_IM_SIZ_8b) { import_texture_ci8(tile); } else { - abort(); + sys_fatal("unsupported CI texture size: %u", siz); } } else if (fmt == G_IM_FMT_I) { if (siz == G_IM_SIZ_4b) { @@ -651,10 +641,10 @@ static void import_texture(int tile) { } else if (siz == G_IM_SIZ_8b) { import_texture_i8(tile); } else { - abort(); + sys_fatal("unsupported I texture size: %u", siz); } } else { - abort(); + sys_fatal("unsupported texture format: %u", fmt); } int t1 = get_time(); //printf("Time diff: %d\n", t1 - t0); @@ -1764,8 +1754,8 @@ void gfx_init(struct GfxWindowManagerAPI *wapi, struct GfxRenderingAPI *rapi) { #ifdef EXTERNAL_DATA // preload all textures if needed if (configPrecacheRes) { - printf("Precaching textures from `%s`\n", sys_data_path()); - sys_dir_walk(sys_data_path(), preload_texture, true); + printf("Precaching textures\n"); + fs_walk(FS_TEXTUREDIR, preload_texture, NULL, true); } #endif } diff --git a/src/pc/pc_main.c b/src/pc/pc_main.c index da548635..3a2d6613 100644 --- a/src/pc/pc_main.c +++ b/src/pc/pc_main.c @@ -22,6 +22,7 @@ #include "cliopts.h" #include "configfile.h" #include "controller/controller_api.h" +#include "fs/fs.h" #include "game/main.h" #include "game/thread6.h" @@ -153,6 +154,8 @@ void main_func(void) { main_pool_init(pool, pool + sizeof(pool) / sizeof(pool[0])); gEffectsMemoryPool = mem_pool_init(0x4000, MEMORY_POOL_LEFT); + fs_init(sys_ropaths, FS_BASEDIR, sys_user_path()); + configfile_load(configfile_name()); wm_api = &gfx_sdl; diff --git a/src/pc/platform.c b/src/pc/platform.c index 80c0eb74..fb4b08ec 100644 --- a/src/pc/platform.c +++ b/src/pc/platform.c @@ -1,17 +1,26 @@ #include #include #include +#include #include -#include -#include -#include -#include #include -#ifdef _WIN32 -#include -#endif #include "cliopts.h" +#include "fs/fs.h" + +/* NULL terminated list of platform specific read-only data paths */ +/* priority is top first */ +const char *sys_ropaths[] = { + ".", // working directory + "!", // executable directory +#if defined(__linux__) || defined(__unix__) + // some common UNIX directories for read only stuff + "/usr/local/share/sm64pc", + "/usr/share/sm64pc", + "/opt/sm64pc", +#endif + NULL, +}; /* these are not available on some platforms, so might as well */ @@ -40,81 +49,24 @@ int sys_strcasecmp(const char *s1, const char *s2) { return result; } -/* file system stuff */ - -bool sys_file_exists(const char *name) { - struct stat st; - return (stat(name, &st) == 0 && S_ISREG(st.st_mode)); +const char *sys_file_extension(const char *fname) { + const char *dot = strrchr(fname, '.'); + if (!dot || !dot[1]) return NULL; + return dot + 1; } -bool sys_dir_exists(const char *name) { - struct stat st; - return (stat(name, &st) == 0 && S_ISDIR(st.st_mode)); -} +/* this calls a platform-specific impl function after forming the error message */ -bool sys_dir_walk(const char *base, walk_fn_t walk, const bool recur) { - char fullpath[SYS_MAX_PATH]; - DIR *dir; - struct dirent *ent; +static void sys_fatal_impl(const char *msg) __attribute__ ((noreturn)); - if (!(dir = opendir(base))) { - fprintf(stderr, "sys_dir_walk(): could not open `%s`\n", base); - return false; - } - - bool ret = true; - - while ((ent = readdir(dir)) != NULL) { - if (ent->d_name[0] == 0 || ent->d_name[0] == '.') continue; // skip ./.. and hidden files - snprintf(fullpath, sizeof(fullpath), "%s/%s", base, ent->d_name); - if (sys_dir_exists(fullpath)) { - if (recur) { - if (!sys_dir_walk(fullpath, walk, recur)) { - ret = false; - break; - } - } - } else { - if (!walk(fullpath)) { - ret = false; - break; - } - } - } - - closedir(dir); - return ret; -} - -void *sys_load_res(const char *name) { - char path[SYS_MAX_PATH] = { 0 }; - snprintf(path, sizeof(path), "%s/%s", sys_data_path(), name); - - FILE *f = fopen(path, "rb"); - if (!f) return NULL; - - fseek(f, 0, SEEK_END); - size_t size = ftell(f); - fseek(f, 0, SEEK_SET); - - void *buf = malloc(size); - if (!buf) { - fclose(f); - return NULL; - } - - fread(buf, 1, size, f); - fclose(f); - - return buf; -} - -bool sys_mkdir(const char *name) { - #ifdef _WIN32 - return _mkdir(name) == 0; - #else - return mkdir(name, 0777) == 0; - #endif +void sys_fatal(const char *fmt, ...) { + static char msg[2048]; + va_list args; + va_start(args, fmt); + vsnprintf(msg, sizeof(msg), fmt, args); + va_end(args); + fflush(stdout); // push all crap out + sys_fatal_impl(msg); } #if USE_SDL @@ -122,125 +74,50 @@ bool sys_mkdir(const char *name) { // we can just ask SDL for most of this shit if we have it #include -const char *sys_data_path(void) { +const char *sys_user_path(void) { static char path[SYS_MAX_PATH] = { 0 }; - - if (!path[0]) { - // prefer the override, if it is set - // "!" expands to executable path - if (gCLIOpts.DataPath[0]) { - if (gCLIOpts.DataPath[0] == '!') - snprintf(path, sizeof(path), "%s%s", sys_exe_path(), gCLIOpts.DataPath + 1); - else - snprintf(path, sizeof(path), "%s", gCLIOpts.DataPath); - if (sys_dir_exists(path)) return path; - printf("Warning: Specified data path ('%s') doesn't exist\n", path); - } - - // then the executable directory - snprintf(path, sizeof(path), "%s/" DATADIR, sys_exe_path()); - if (sys_dir_exists(path)) return path; - - // then the save path - snprintf(path, sizeof(path), "%s/" DATADIR, sys_save_path()); - if (sys_dir_exists(path)) return path; - - #if defined(__linux__) || defined(__unix__) - // on Linux/BSD try some common paths for read-only data - const char *try[] = { - "/usr/local/share/sm64pc/" DATADIR, - "/usr/share/sm64pc/" DATADIR, - "/opt/sm64pc/" DATADIR, - }; - for (unsigned i = 0; i < sizeof(try) / sizeof(try[0]); ++i) { - if (sys_dir_exists(try[i])) { - strcpy(path, try[i]); - return path; - } - } - #endif - - // hope for the best - strcpy(path, "./" DATADIR); + // get it from SDL + char *sdlpath = SDL_GetPrefPath("", "sm64pc"); + if (sdlpath) { + const unsigned int len = strlen(sdlpath); + strncpy(path, sdlpath, sizeof(path)); + path[sizeof(path)-1] = 0; + SDL_free(sdlpath); + if (path[len-1] == '/' || path[len-1] == '\\') + path[len-1] = 0; // strip the trailing separator + if (!fs_sys_dir_exists(path) && !fs_sys_mkdir(path)) + path[0] = 0; } - - return path; -} - -const char *sys_save_path(void) { - static char path[SYS_MAX_PATH] = { 0 }; - - if (!path[0]) { - // if the override is set, use that - // "!" expands to executable path - if (gCLIOpts.SavePath[0]) { - if (gCLIOpts.SavePath[0] == '!') - snprintf(path, sizeof(path), "%s%s", sys_exe_path(), gCLIOpts.SavePath + 1); - else - snprintf(path, sizeof(path), "%s", gCLIOpts.SavePath); - if (!sys_dir_exists(path) && !sys_mkdir(path)) { - printf("Warning: Specified save path ('%s') doesn't exist and can't be created\n", path); - path[0] = 0; // doesn't exist and no write access - } - } - - // didn't work? get it from SDL - if (!path[0]) { - char *sdlpath = SDL_GetPrefPath("", "sm64pc"); - if (sdlpath) { - const unsigned int len = strlen(sdlpath); - strncpy(path, sdlpath, sizeof(path)); - path[sizeof(path)-1] = 0; - SDL_free(sdlpath); - if (path[len-1] == '/' || path[len-1] == '\\') - path[len-1] = 0; // strip the trailing separator - if (!sys_dir_exists(path) && !sys_mkdir(path)) - path[0] = 0; - } - } - - // if all else fails, just store near the EXE - if (!path[0]) - strcpy(path, sys_exe_path()); - - printf("Save path set to '%s'\n", path); - } - return path; } const char *sys_exe_path(void) { static char path[SYS_MAX_PATH] = { 0 }; - - if (!path[0]) { - char *sdlpath = SDL_GetBasePath(); - if (sdlpath) { - // use the SDL path if it exists - const unsigned int len = strlen(sdlpath); - strncpy(path, sdlpath, sizeof(path)); - path[sizeof(path)-1] = 0; - SDL_free(sdlpath); - if (path[len-1] == '/' || path[len-1] == '\\') - path[len-1] = 0; // strip the trailing separator - } else { - // hope for the best - strcpy(path, "."); - } - printf("Executable path set to '%s'\n", path); + char *sdlpath = SDL_GetBasePath(); + if (sdlpath && sdlpath[0]) { + // use the SDL path if it exists + const unsigned int len = strlen(sdlpath); + strncpy(path, sdlpath, sizeof(path)); + path[sizeof(path)-1] = 0; + SDL_free(sdlpath); + if (path[len-1] == '/' || path[len-1] == '\\') + path[len-1] = 0; // strip the trailing separator } - return path; } +static void sys_fatal_impl(const char *msg) { + SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR , "Fatal error", msg, NULL); + fprintf(stderr, "FATAL ERROR:\n%s\n", msg); + fflush(stderr); + exit(1); +} + #else #warning "You might want to implement these functions for your platform" -const char *sys_data_path(void) { - return "."; -} - -const char *sys_save_path(void) { +const char *sys_user_path(void) { return "."; } @@ -248,4 +125,10 @@ const char *sys_exe_path(void) { return "."; } +static void sys_fatal_impl(const char *msg) { + fprintf(stderr, "FATAL ERROR:\n%s\n", msg); + fflush(stderr); + exit(1); +} + #endif // platform switch diff --git a/src/pc/platform.h b/src/pc/platform.h index 30b33dcc..53b2ad16 100644 --- a/src/pc/platform.h +++ b/src/pc/platform.h @@ -7,29 +7,22 @@ /* Platform-specific functions and whatnot */ -#define DATADIR "res" #define SYS_MAX_PATH 1024 // FIXME: define this on different platforms +// NULL terminated list of platform specific read-only data paths +extern const char *sys_ropaths[]; + // crossplatform impls of misc stuff char *sys_strdup(const char *src); char *sys_strlwr(char *src); int sys_strcasecmp(const char *s1, const char *s2); -// filesystem stuff -bool sys_mkdir(const char *name); // creates with 0777 by default -bool sys_file_exists(const char *name); -bool sys_dir_exists(const char *name); -void *sys_load_res(const char *name); - -// receives the full path -// should return `true` if traversal should continue -typedef bool (*walk_fn_t)(const char *); -// returns `true` if the directory was successfully opened and walk() didn't ever return false -bool sys_dir_walk(const char *base, walk_fn_t walk, const bool recur); - // path stuff -const char *sys_data_path(void); -const char *sys_save_path(void); +const char *sys_user_path(void); const char *sys_exe_path(void); +const char *sys_file_extension(const char *fname); + +// shows an error message in some way and terminates the game +void sys_fatal(const char *fmt, ...) __attribute__ ((noreturn)); #endif // _SM64_PLATFORM_H_ diff --git a/src/pc/ultra_reimplementation.c b/src/pc/ultra_reimplementation.c index c28894f6..65e5f01b 100644 --- a/src/pc/ultra_reimplementation.c +++ b/src/pc/ultra_reimplementation.c @@ -3,6 +3,9 @@ #include "lib/src/libultra_internal.h" #include "macros.h" #include "platform.h" +#include "fs/fs.h" + +#define SAVE_FILENAME "sm64_save_file.bin" #ifdef TARGET_WEB #include @@ -120,17 +123,15 @@ s32 osEepromLongRead(UNUSED OSMesgQueue *mq, u8 address, u8 *buffer, int nbytes) ret = 0; } #else - char save_path[SYS_MAX_PATH] = { 0 }; - snprintf(save_path, sizeof(save_path), "%s/sm64_save_file.bin", sys_save_path()); - FILE *fp = fopen(save_path, "rb"); + fs_file_t *fp = fs_open(SAVE_FILENAME); if (fp == NULL) { return -1; } - if (fread(content, 1, 512, fp) == 512) { + if (fs_read(fp, content, 512) == 512) { memcpy(buffer, content + address * 8, nbytes); ret = 0; } - fclose(fp); + fs_close(fp); #endif return ret; } @@ -152,9 +153,7 @@ s32 osEepromLongWrite(UNUSED OSMesgQueue *mq, u8 address, u8 *buffer, int nbytes }, content); s32 ret = 0; #else - char save_path[SYS_MAX_PATH] = { 0 }; - snprintf(save_path, sizeof(save_path), "%s/sm64_save_file.bin", sys_save_path()); - FILE *fp = fopen(save_path, "wb"); + FILE *fp = fopen(fs_get_write_path(SAVE_FILENAME), "wb"); if (fp == NULL) { return -1; } diff --git a/tools/mkzip.py b/tools/mkzip.py new file mode 100644 index 00000000..5481bc59 --- /dev/null +++ b/tools/mkzip.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +import sys +import os +import zipfile + +if len(sys.argv) < 3: + print('usage: mkzip ') + sys.exit(1) + +lst = [] +with open(sys.argv[1], 'r') as f: + for line in f: + line = line.strip() + if line == '' or line[0] == '#': + continue + tok = line.split() + lst.append((tok[0], tok[1])) + +with zipfile.ZipFile(sys.argv[2], 'w', allowZip64=False) as zipf: + for (fname, aname) in lst: + zipf.write(fname, arcname=aname)