diff --git a/src/core/fex/CMakeLists.txt b/src/core/fex/CMakeLists.txt index 3f20dd06..a8394081 100644 --- a/src/core/fex/CMakeLists.txt +++ b/src/core/fex/CMakeLists.txt @@ -57,6 +57,10 @@ target_sources(vbam-fex fex/Gzip_Extractor.h fex/Gzip_Reader.cpp fex/Gzip_Reader.h + fex/BZ2_Extractor.cpp + fex/BZ2_Extractor.h + fex/BZ2_Reader.cpp + fex/BZ2_Reader.h fex/Rar_Extractor.cpp fex/Rar_Extractor.h fex/Zip7_Extractor.cpp @@ -65,6 +69,8 @@ target_sources(vbam-fex fex/Zip_Extractor.h fex/Zlib_Inflater.cpp fex/Zlib_Inflater.h + fex/BZ2_Inflater.cpp + fex/BZ2_Inflater.h unrar/archive.cpp unrar/arcread.cpp unrar/blake2s_sse.cpp @@ -105,6 +111,12 @@ target_include_directories(vbam-fex PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${ZLIB_INCLUDE_DIRS} ) +find_library(BZ2_LIBRARY bz2) + +if (BZ2_LIBRARY) + target_compile_definitions(vbam-fex PRIVATE FEX_ENABLE_BZ2=1) +endif() + target_link_libraries(vbam-fex - PRIVATE ${ZLIB_LIBRARY} + PRIVATE ${ZLIB_LIBRARY} ${BZ2_LIBRARY} ) diff --git a/src/core/fex/fex/BZ2_Extractor.cpp b/src/core/fex/fex/BZ2_Extractor.cpp new file mode 100644 index 00000000..64927182 --- /dev/null +++ b/src/core/fex/fex/BZ2_Extractor.cpp @@ -0,0 +1,103 @@ +// File_Extractor 1.0.0. http://www.slack.net/~ant/ + +#if FEX_ENABLE_BZ2 + +#include "BZ2_Extractor.h" +#include + +/* Copyright (C) 2005-2009 Shay Green. This module is free software; you +can redistribute it and/or modify it under the terms of the GNU Lesser +General Public License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. This +module is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +details. You should have received a copy of the GNU Lesser General Public +License along with this module; if not, write to the Free Software Foundation, +Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ + +#include "blargg_source.h" + +// TODO: could close file once data has been read into memory + +static blargg_err_t init_bz2_file() +{ + get_crc_table(); // initialize zlib's CRC-32 tables + return blargg_ok; +} + +static File_Extractor* new_bzip2() +{ + return BLARGG_NEW BZ2_Extractor; +} + +fex_type_t_ const fex_bz2_type [1] = {{ + ".bz2", + &new_bzip2, + "bzip2 file", + &init_bz2_file +}}; + +BZ2_Extractor::BZ2_Extractor() : + File_Extractor( fex_bz2_type ) +{ } + +BZ2_Extractor::~BZ2_Extractor() +{ + close(); +} + +blargg_err_t BZ2_Extractor::open_path_v() +{ + // skip opening file + return open_v(); +} + +blargg_err_t BZ2_Extractor::stat_v() +{ + RETURN_ERR( open_arc_file( true ) ); + if ( !gr.opened() || gr.tell() != 0 ) + RETURN_ERR( gr.open( &arc() ) ); + + set_info( gr.remain(), 0, gr.crc32() ); + return blargg_ok; +} + +blargg_err_t BZ2_Extractor::open_v() +{ + // Remove .gz suffix + size_t len = strlen( arc_path() ); + if ( fex_has_extension( arc_path(), ".bz2" ) ) + len -= 4; + + RETURN_ERR( name.resize( len + 1 ) ); + memcpy( name.begin(), arc_path(), name.size() ); + name [name.size() - 1] = '\0'; + + set_name( name.begin() ); + return blargg_ok; +} + +void BZ2_Extractor::close_v() +{ + name.clear(); + gr.close(); +} + +blargg_err_t BZ2_Extractor::next_v() +{ + return blargg_ok; +} + +blargg_err_t BZ2_Extractor::rewind_v() +{ + set_name( name.begin() ); + return blargg_ok; +} + +blargg_err_t BZ2_Extractor::extract_v( void* p, int n ) +{ + return gr.read( p, n ); +} + +#endif diff --git a/src/core/fex/fex/BZ2_Extractor.h b/src/core/fex/fex/BZ2_Extractor.h new file mode 100644 index 00000000..f6e34ff3 --- /dev/null +++ b/src/core/fex/fex/BZ2_Extractor.h @@ -0,0 +1,35 @@ +// Presents a gzipped file as an "archive" of just that file. +// Also handles non-gzipped files. + +// File_Extractor 1.0.0 +#ifndef BZ2_EXTRACTOR_H +#define BZ2_EXTRACTOR_H + +#include "File_Extractor.h" +#include "BZ2_Reader.h" + +class BZ2_Extractor : public File_Extractor +{ + public: + BZ2_Extractor(); + virtual ~BZ2_Extractor(); + + protected: + virtual blargg_err_t open_path_v(); + virtual blargg_err_t open_v(); + virtual void close_v(); + + virtual blargg_err_t next_v(); + virtual blargg_err_t rewind_v(); + + virtual blargg_err_t stat_v(); + virtual blargg_err_t extract_v(void *, int); + + private: + BZ2_Reader gr; + blargg_vector name; + + void set_info_(); +}; + +#endif diff --git a/src/core/fex/fex/BZ2_Inflater.cpp b/src/core/fex/fex/BZ2_Inflater.cpp new file mode 100644 index 00000000..998f4e9d --- /dev/null +++ b/src/core/fex/fex/BZ2_Inflater.cpp @@ -0,0 +1,255 @@ +// File_Extractor 1.0.0. http://www.slack.net/~ant/ + +#if FEX_ENABLE_BZ2 + +#include "BZ2_Inflater.h" + +/* Copyright (C) 2025 Andy Vandijck. This module is free software; you +can redistribute it and/or modify it under the terms of the GNU Lesser +General Public License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. This +module is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +details. You should have received a copy of the GNU Lesser General Public +License along with this module; if not, write to the Free Software Foundation, +Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ + +#include "blargg_source.h" + +int const block_size = 4096; + +static const char* get_bz2_err( int code ) +{ + assert( code != Z_OK ); + switch ( code ) + { + case BZ_MEM_ERROR: return blargg_err_memory; + case BZ_DATA_ERROR: return blargg_err_file_corrupt; + // TODO: handle more error codes + } + + const char* str = BLARGG_ERR( BLARGG_ERR_GENERIC, "problem unzipping data" ); + + return str; +} + +void BZ2_Inflater::end() +{ + if ( deflated_ ) + { + deflated_ = false; + if ( BZ2_bzDecompressEnd ( &zbuf ) ) + check( false ); + } + buf.clear(); + + static bz_stream const empty = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + memcpy( &zbuf, &empty, sizeof zbuf ); +} + +BZ2_Inflater::BZ2_Inflater() +{ + deflated_ = false; + end(); // initialize things +} + +BZ2_Inflater::~BZ2_Inflater() +{ + end(); +} + +blargg_err_t BZ2_Inflater::fill_buf( int count ) +{ + byte* out = buf.end() - count; + RETURN_ERR( callback( user_data, out, &count ) ); + zbuf.avail_in = count; + zbuf.next_in = (char *)out; + return blargg_ok; +} + +blargg_err_t BZ2_Inflater::begin( callback_t new_callback, void* new_user_data, + int new_buf_size, int initial_read ) +{ + callback = new_callback; + user_data = new_user_data; + + end(); + + // TODO: decide whether using different size on alloc failure is a good idea + //RETURN_ERR( buf.resize( new_buf_size ? new_buf_size : 4 * block_size ) ); + if ( new_buf_size && buf.resize( new_buf_size ) ) + { + ACK_FAILURE(); + new_buf_size = 0; + } + + if ( !new_buf_size ) + { + RETURN_ERR( buf.resize( 4 * block_size ) ); + initial_read = 0; + } + + // Fill buffer with some data, less than normal buffer size since caller might + // just be examining beginning of file. + return fill_buf( initial_read ? initial_read : block_size ); +} + +blargg_err_t BZ2_Inflater::set_mode( mode_t mode, int data_offset ) +{ + zbuf.next_in += data_offset; + zbuf.avail_in -= data_offset; + + if ( mode == mode_auto ) + { + // examine buffer for gzip header + mode = mode_copy; + unsigned const min_gzip_size = 2 + 8 + 8; + if ( zbuf.avail_in >= min_gzip_size && + zbuf.next_in [0] == 0x42 && zbuf.next_in [1] == 0x5A ) + mode = mode_unbz2; + } + + if ( mode != mode_copy ) + { + int zerr = BZ2_bzDecompressInit( &zbuf, 0, 0 ); + if ( zerr ) + { + zbuf.next_in = NULL; + return get_bz2_err( zerr ); + } + + deflated_ = true; + } + return blargg_ok; +} + +/* +// Reads/inflates entire stream. All input must be in buffer, and count must be total +// of all output. +blargg_err_t read_all( void* out, int count ); + + +// zlib automatically applies this optimization (uses inflateFast) +// TODO: remove +blargg_err_t BZ2_Inflater::read_all( void* out, int count ) +{ + if ( deflated_ ) + { + zbuf.next_out = (char*) out; + zbuf.avail_out = count; + + int err = BZ2_bzDecompress( &zbuf ); + + if ( zbuf.avail_out || err != Z_STREAM_END ) + return blargg_err_file_corrupt; + } + else + { + if ( zbuf.avail_in < count ) + return blargg_err_file_corrupt; + + memcpy( out, zbuf.next_in, count ); + + zbuf.next_in += count; + zbuf.avail_in -= count; + } + + return blargg_ok; +} +*/ + +blargg_err_t BZ2_Inflater::read( void* out, int* count_io ) +{ + int remain = *count_io; + if ( remain && zbuf.next_in ) + { + if ( deflated_ ) + { + zbuf.next_out = (char*) out; + zbuf.avail_out = remain; + + while ( 1 ) + { + unsigned int old_avail_in = (unsigned int)zbuf.avail_in; + int err = BZ2_bzDecompress( &zbuf ); + if ( err == BZ_STREAM_END ) + { + remain = zbuf.avail_out; + end(); + break; // no more data to inflate + } + + if ( err && (err != BZ_OUTBUFF_FULL || old_avail_in) ) + return get_bz2_err( err ); + + if ( !zbuf.avail_out ) + { + remain = 0; + break; // requested number of bytes inflated + } + + if ( zbuf.avail_in ) + { + // inflate() should never leave input if there's still space for output + check( false ); + return blargg_err_file_corrupt; + } + + RETURN_ERR( fill_buf( (int)buf.size() ) ); + if ( !zbuf.avail_in ) + return blargg_err_file_corrupt; // stream didn't end but there's no more data + } + } + else + { + while ( 1 ) + { + // copy buffered data + if ( zbuf.avail_in ) + { + long count = zbuf.avail_in; + if ( count > remain ) + count = remain; + memcpy( out, zbuf.next_in, count ); + zbuf.total_out_lo32 += count; + out = (char*) out + count; + remain -= count; + zbuf.next_in += count; + zbuf.avail_in -= count; + } + + if ( !zbuf.avail_in && zbuf.next_in < (char *)buf.end() ) + { + end(); + break; + } + + // read large request directly + if ( remain + zbuf.total_out_lo32 % block_size >= buf.size() ) + { + int count = remain; + RETURN_ERR( callback( user_data, out, &count ) ); + zbuf.total_out_lo32 += count; + out = (char*) out + count; + remain -= count; + + if ( remain ) + { + end(); + break; + } + } + + if ( !remain ) + break; + + RETURN_ERR( fill_buf( (int)(buf.size() - zbuf.total_out_lo32 % block_size) ) ); + } + } + } + *count_io -= remain; + return blargg_ok; +} + +#endif diff --git a/src/core/fex/fex/BZ2_Inflater.h b/src/core/fex/fex/BZ2_Inflater.h new file mode 100644 index 00000000..448ac51c --- /dev/null +++ b/src/core/fex/fex/BZ2_Inflater.h @@ -0,0 +1,88 @@ +// Simplifies use of zlib for inflating data + +// File_Extractor 1.0.0 +#ifndef BZ2_INFLATER_H +#define BZ2_INFLATER_H + +#include + +#include "Data_Reader.h" +#include "blargg_common.h" + +class BZ2_Inflater +{ + public: + // Reads at most min(*count,bytes_until_eof()) bytes into *out and set *count + // to that number, or returns error if that many can't be read. + typedef blargg_err_t (*callback_t)(void *user_data, void *out, int *count); + + // Begins by setting callback and filling buffer. Default buffer is 16K and + // filled to 4K, or specify buf_size and initial_read for custom buffer size + // and how much to read initially. + blargg_err_t begin(callback_t, void *user_data, int buf_size = 0, int initial_read = 0); + + // Data read into buffer by begin() + const unsigned char *data() const + { + return (const unsigned char *)zbuf.next_in; + } + + int filled() const + { + return zbuf.avail_in; + } + + int totalOut() const + { + return zbuf.total_out_lo32; + } + + // Begins inflation using specified mode. Using mode_auto selects between + // mode_copy and mode_ungz by examining first two bytes of buffer. Use + // buf_offset to specify where data begins in buffer, in case there is + // header data that should be skipped. + enum mode_t { mode_copy, mode_unbz2, mode_raw_deflate, mode_auto }; + blargg_err_t set_mode(mode_t, int buf_offset = 0); + + // True if set_mode() has been called with mode_ungz or mode_raw_deflate + bool deflated() const + { + return deflated_; + } + + // Reads/inflates at most *count_io bytes into *out and sets *count_io to actual + // number of bytes read (less than requested if end of data was reached). + // Buffers source data internally, even in copy mode, so input file can be + // unbuffered without sacrificing performance. + blargg_err_t read(void *out, int *count_io); + + // Total number of bytes read since begin() + int tell() const + { + return zbuf.total_out_lo32; + } + + // Ends inflation and frees memory + void end(); + + private: + // noncopyable + BZ2_Inflater(const BZ2_Inflater &); + BZ2_Inflater &operator=(const BZ2_Inflater &); + + // Implementation + public: + BZ2_Inflater(); + ~BZ2_Inflater(); + + private: + bz_stream zbuf; + blargg_vector buf; + bool deflated_; + callback_t callback; + void *user_data; + + blargg_err_t fill_buf(int count); +}; + +#endif diff --git a/src/core/fex/fex/BZ2_Reader.cpp b/src/core/fex/fex/BZ2_Reader.cpp new file mode 100644 index 00000000..0303f61c --- /dev/null +++ b/src/core/fex/fex/BZ2_Reader.cpp @@ -0,0 +1,74 @@ +// File_Extractor 1.0.0. http://www.slack.net/~ant/ + +#if FEX_ENABLE_BZ2 + +#include "BZ2_Reader.h" + +#include "blargg_endian.h" + +/* Copyright (C) 2025 Andy Vandijck. This module is free software; you +can redistribute it and/or modify it under the terms of the GNU Lesser +General Public License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. This +module is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +details. You should have received a copy of the GNU Lesser General Public +License along with this module; if not, write to the Free Software Foundation, +Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ + +#include "blargg_source.h" + +BZ2_Reader::BZ2_Reader() +{ + close(); +} + +BZ2_Reader::~BZ2_Reader() +{ } + +static blargg_err_t BZ2_reader_read( void* file, void* out, int* count ) +{ + return STATIC_CAST(File_Reader*,file)->read_avail( out, count ); +} + +blargg_err_t BZ2_Reader::calc_size() +{ + size_ = in->size() * 4; + crc32_ = 0; + return blargg_ok; +} + +blargg_err_t BZ2_Reader::open( File_Reader* new_in ) +{ + close(); + + in = new_in; + RETURN_ERR( in->seek( 0 ) ); + RETURN_ERR( inflater.begin( BZ2_reader_read, new_in ) ); + RETURN_ERR( inflater.set_mode( inflater.mode_auto ) ); + RETURN_ERR( calc_size() ); + set_remain( size_ ); + + return blargg_ok; +} + +void BZ2_Reader::close() +{ + in = NULL; + inflater.end(); +} + +blargg_err_t BZ2_Reader::read_v( void* out, int count ) +{ + assert( in ); + int actual = count; + RETURN_ERR( inflater.read( out, &actual ) ); + + if ( actual < count ) + set_remain(0); + + return blargg_ok; +} + +#endif diff --git a/src/core/fex/fex/BZ2_Reader.h b/src/core/fex/fex/BZ2_Reader.h new file mode 100644 index 00000000..77e052b3 --- /dev/null +++ b/src/core/fex/fex/BZ2_Reader.h @@ -0,0 +1,59 @@ +// Transparently decompresses gzip files, as well as uncompressed + +// File_Extractor 1.0.0 +#ifndef BZ2_READER_H +#define BZ2_READER_H + +#include "Data_Reader.h" +#include "BZ2_Inflater.h" + +class BZ2_Reader : public Data_Reader +{ + public: + // Keeps pointer to reader until close(). If + blargg_err_t open(File_Reader *); + + // True if file is open + bool opened() const + { + return in != NULL; + } + + // Frees memory + void close(); + + // True if file is compressed + bool deflated() const + { + return inflater.deflated(); + } + + // CRC-32 of data, of 0 if unavailable + unsigned int crc32() const + { + return crc32_; + } + + // Number of bytes read since opening + int tell() const + { + return size_ - remain(); + } + + public: + BZ2_Reader(); + virtual ~BZ2_Reader(); + + protected: + virtual blargg_err_t read_v(void *, int); + + private: + File_Reader *in; + unsigned crc32_; + int size_; + BZ2_Inflater inflater; + + blargg_err_t calc_size(); +}; + +#endif diff --git a/src/core/fex/fex/File_Extractor.h b/src/core/fex/fex/File_Extractor.h index c220355a..90297aaa 100644 --- a/src/core/fex/fex/File_Extractor.h +++ b/src/core/fex/fex/File_Extractor.h @@ -213,7 +213,7 @@ struct fex_type_t_ { blargg_err_t (*init)(); // Called by fex_init(). Can be NULL. }; -extern const fex_type_t_ fex_7z_type[1], fex_gz_type[1], fex_lz4_type[1], fex_rar_type[1], fex_zip_type[1], +extern const fex_type_t_ fex_7z_type[1], fex_gz_type[1], fex_bz2_type[1], fex_rar_type[1], fex_zip_type[1], fex_bin_type[1]; inline blargg_err_t File_Extractor::open_v() diff --git a/src/core/fex/fex/blargg_config.h b/src/core/fex/fex/blargg_config.h index 555c7a47..883de90a 100644 --- a/src/core/fex/fex/blargg_config.h +++ b/src/core/fex/fex/blargg_config.h @@ -21,7 +21,13 @@ #define FEX_ENABLE_RAR 1 #endif -#define FEX_TYPE_LIST fex_7z_type, fex_gz_type, fex_zip_type, fex_rar_type, +#if FEX_ENABLE_BZ2 +#define FEX_TYPE_BZ2 fex_bz2_type, +#else +#define FEX_TYPE_BZ2 +#endif + +#define FEX_TYPE_LIST fex_7z_type, fex_gz_type, fex_zip_type, fex_rar_type, FEX_TYPE_BZ2 // Use standard config.h if present #ifdef HAVE_CONFIG_H diff --git a/src/core/fex/fex/fex.cpp b/src/core/fex/fex/fex.cpp index 2c9b4ddc..2bd480a6 100644 --- a/src/core/fex/fex/fex.cpp +++ b/src/core/fex/fex/fex.cpp @@ -33,6 +33,9 @@ BLARGG_EXPORT const fex_type_t* fex_type_list( void ) // Modify blargg_config.h to change type list, NOT this file fex_7z_type, fex_gz_type, + #if FEX_ENABLE_BZ2 + fex_bz2_type, + #endif #if FEX_ENABLE_RAR fex_rar_type, #endif diff --git a/src/wx/cmdevents.cpp b/src/wx/cmdevents.cpp index 253413de..29ef845e 100644 --- a/src/wx/cmdevents.cpp +++ b/src/wx/cmdevents.cpp @@ -132,11 +132,13 @@ EVT_HANDLER(wxID_OPEN, "Open ROM...") wxString pats = _( "Game Boy Advance Files (*.agb;*.gba;*.bin;*.elf;*.mb;*.zip;*.7z;*.rar)|" "*.agb;*.gba;*.bin;*.elf;*.mb;" + "*.agb.bz2;*.gba.bz2;*.bin.bz2;*.elf.bz2;*.mb.bz2;" "*.agb.gz;*.gba.gz;*.bin.gz;*.elf.gz;*.mb.gz;" "*.agb.z;*.gba.z;*.bin.z;*.elf.z;*.mb.z;" "*.zip;*.7z;*.rar|" "Game Boy Files (*.dmg;*.gb;*.gbc;*.cgb;*.sgb;*.zip;*.7z;*.rar)|" "*.dmg;*.gb;*.gbc;*.cgb;*.sgb;" + "*.dmg.bz2;*.gb.bz2;*.gbc.bz2;*.cgb.bz2;*.sgb.bz2;" "*.dmg.gz;*.gb.gz;*.gbc.gz;*.cgb.gz;*.sgb.gz;" "*.dmg.z;*.gb.z;*.gbc.z;*.cgb.z;*.sgb.z;" "*.zip;*.7z;*.rar|"); @@ -168,6 +170,7 @@ EVT_HANDLER(OpenGB, "Open GB...") wxString pats = _( "Game Boy Files (*.dmg;*.gb;*.gbc;*.cgb;*.sgb;*.zip;*.7z;*.rar)|" "*.dmg;*.gb;*.gbc;*.cgb;*.sgb;" + "*.dmg.bz2;*.gb.bz2;*.gbc.bz2;*.cgb.bz2;*.sgb.bz2;" "*.dmg.gz;*.gb.gz;*.gbc.gz;*.cgb.gz;*.sgb.gz;" "*.dmg.z;*.gb.z;*.gbc.z;*.cgb.z;*.sgb.z;" "*.zip;*.7z;*.rar|"); @@ -197,6 +200,7 @@ EVT_HANDLER(OpenGBC, "Open GBC...") wxString pats = _( "Game Boy Color Files (*.dmg;*.gb;*.gbc;*.cgb;*.sgb;*.zip;*.7z;*.rar)|" "*.dmg;*.gb;*.gbc;*.cgb;*.sgb;" + "*.dmg.bz2;*.gb.bz2;*.gbc.bz2;*.cgb.bz2;*.sgb.bz2;" "*.dmg.gz;*.gb.gz;*.gbc.gz;*.cgb.gz;*.sgb.gz;" "*.dmg.z;*.gb.z;*.gbc.z;*.cgb.z;*.sgb.z;" "*.zip;*.7z;*.rar|");