diff options
Diffstat (limited to 'src/lib/util/unzip.cpp')
-rw-r--r-- | src/lib/util/unzip.cpp | 786 |
1 files changed, 565 insertions, 221 deletions
diff --git a/src/lib/util/unzip.cpp b/src/lib/util/unzip.cpp index 7e92650aaa1..ad51400d9d1 100644 --- a/src/lib/util/unzip.cpp +++ b/src/lib/util/unzip.cpp @@ -11,9 +11,9 @@ #include "unzip.h" #include "corestr.h" +#include "hashing.h" #include "osdcore.h" - #include <algorithm> #include <array> #include <cassert> @@ -25,6 +25,8 @@ #include <zlib.h> +#include "lzma/C/LzmaDec.h" + namespace util { namespace { @@ -46,10 +48,8 @@ public: , m_cd_pos(0) , m_header() , m_curr_is_dir(false) - , m_curr_name() , m_buffer() { - std::memset(&m_header, 0, sizeof(m_header)); std::fill(m_buffer.begin(), m_buffer.end(), 0); } @@ -63,6 +63,7 @@ public: { ptr result; std::swap(s_cache[cachenum], result); + osd_printf_verbose("unzip: found %s in cache\n", filename.c_str()); return result; } } @@ -84,23 +85,51 @@ public: return ziperr; // verify that we can work with this zipfile (no disk spanning allowed) - if ((m_ecd.disk_number != m_ecd.cd_start_disk_number) || - (m_ecd.cd_disk_entries != m_ecd.cd_total_entries) || - (std::size_t(m_ecd.cd_size) != m_ecd.cd_size)) + if (m_ecd.disk_number != m_ecd.cd_start_disk_number) + { + osd_printf_error("unzip: %s central directory starts in another segment\n", m_filename.c_str()); + return archive_file::error::UNSUPPORTED; + } + if (m_ecd.cd_disk_entries != m_ecd.cd_total_entries) + { + osd_printf_error("unzip: %s not all central directory entries reside in this segment\n", m_filename.c_str()); return archive_file::error::UNSUPPORTED; + } + if (std::size_t(m_ecd.cd_size) != m_ecd.cd_size) + { + osd_printf_error("unzip: %s central directory too large to load\n", m_filename.c_str()); + return archive_file::error::UNSUPPORTED; + } // allocate memory for the central directory try { m_cd.resize(std::size_t(m_ecd.cd_size)); } - catch (...) { return archive_file::error::OUT_OF_MEMORY; } + catch (...) + { + osd_printf_error("unzip: %s failed to allocate memory for central directory\n", m_filename.c_str()); + return archive_file::error::OUT_OF_MEMORY; + } // read the central directory - // FIXME: central directory can theoretically be bigger than maximum 32-bit integer value, should use a loop - std::uint32_t read_length; - auto const filerr = m_file->read(&m_cd[0], m_ecd.cd_start_disk_offset, m_ecd.cd_size, read_length); - if (filerr != osd_file::error::NONE) - return archive_file::error::FILE_ERROR; - if (read_length != m_ecd.cd_size) - return archive_file::error::FILE_TRUNCATED; + auto cd_remaining(m_ecd.cd_size); + std::size_t cd_offs(0); + while (cd_remaining) + { + std::uint32_t const chunk(std::uint32_t((std::min<std::uint64_t>)(std::numeric_limits<std::uint32_t>::max(), cd_remaining))); + std::uint32_t read_length(0); + auto const filerr = m_file->read(&m_cd[cd_offs], m_ecd.cd_start_disk_offset + cd_offs, chunk, read_length); + if (filerr != osd_file::error::NONE) + { + osd_printf_error("unzip: %s error reading central directory (%d)\n", m_filename.c_str(), int(filerr)); + return archive_file::error::FILE_ERROR; + } + if (!read_length) + { + osd_printf_error("unzip: %s unexpectedly reached end-of-file while reading central directory\n", m_filename.c_str()); + return archive_file::error::FILE_TRUNCATED; + } + cd_remaining -= read_length; + cd_offs += read_length; + } return archive_file::error::NONE; } @@ -132,7 +161,7 @@ public: } bool current_is_directory() const { return m_curr_is_dir; } - const std::string ¤t_name() const { return m_curr_name; } + const std::string ¤t_name() const { return m_header.file_name; } std::uint64_t current_uncompressed_length() const { return m_header.uncompressed_length; } std::uint32_t current_crc() const { return m_header.crc; } @@ -152,7 +181,15 @@ private: { auto const filerr = osd_file::open(m_filename, OPEN_FLAG_READ, m_file, m_length); if (filerr != osd_file::error::NONE) + { + // this would spam every time it looks for a non-existent archive, which is a lot + //osd_printf_error("unzip: error reopening archive file %s (%d)\n", m_filename.c_str(), int(filerr)); return archive_file::error::FILE_ERROR; + } + else + { + osd_printf_verbose("unzip: opened archive file %s\n", m_filename.c_str()); + } } return archive_file::error::NONE; } @@ -164,6 +201,7 @@ private: // decompression interfaces archive_file::error decompress_data_type_0(std::uint64_t offset, void *buffer, std::uint32_t length); archive_file::error decompress_data_type_8(std::uint64_t offset, void *buffer, std::uint32_t length); + archive_file::error decompress_data_type_14(std::uint64_t offset, void *buffer, std::uint32_t length); struct file_header { @@ -176,6 +214,7 @@ private: std::uint64_t uncompressed_length; // uncompressed size std::uint32_t start_disk_number; // disk number start std::uint64_t local_header_offset; // relative offset of local header + std::string file_name; // file name }; // contains extracted end of central directory information @@ -204,7 +243,6 @@ private: std::uint32_t m_cd_pos; // position in central directory file_header m_header; // current file header bool m_curr_is_dir; // current file is directory - std::string m_curr_name; // current file name std::array<std::uint8_t, DECOMPRESS_BUFSIZE> m_buffer; // buffer for decompression }; @@ -283,6 +321,10 @@ protected: { return std::string(reinterpret_cast<char const *>(m_buffer + offs), len); } + void read_string(std::string &result, std::size_t offs, std::string::size_type len) const + { + result.assign(reinterpret_cast<char const *>(m_buffer + offs), len); + } std::uint8_t const *m_buffer; }; @@ -308,52 +350,28 @@ private: }; -class zip64_ext_info_reader : private reader_base -{ -public: - zip64_ext_info_reader(void const *buf) : reader_base(buf) { } - - std::uint64_t uncompressed_size() const { return read_qword(0x00); } - std::uint64_t compressed_size() const { return read_qword(0x08); } - std::uint64_t header_offset() const { return read_qword(0x10); } - std::uint32_t start_disk() const { return read_dword(0x18); } - - std::size_t total_length() const { return minimum_length(); } - static std::size_t minimum_length() { return 0x1c; } -}; - - class local_file_header_reader : private reader_base { public: local_file_header_reader(void const *buf) : reader_base(buf) { } - std::uint32_t signature() const { return read_dword(0x00); } - std::uint8_t version_needed() const { return m_buffer[0x04]; } - std::uint8_t os_needed() const { return m_buffer[0x05]; } - std::uint16_t general_flag() const { return read_word(0x06); } - std::uint16_t compression_method() const { return read_word(0x08); } - std::uint16_t modified_time() const { return read_word(0x0a); } - std::uint16_t modified_date() const { return read_word(0x0c); } - std::uint32_t crc32() const { return read_dword(0x0e); } - std::uint32_t compressed_size() const { return read_dword(0x12); } - std::uint32_t uncompressed_size() const { return read_dword(0x16); } - std::uint16_t file_name_length() const { return read_word(0x1a); } - std::uint16_t extra_field_length() const { return read_word(0x1c); } - std::string file_name() const { return read_string(0x1e, file_name_length()); } - extra_field_reader extra_field() const { return extra_field_reader(m_buffer + 0x1e + file_name_length(), extra_field_length()); } - - bool signature_correct() const { return signature() == 0x04034b50; } - - bool encrypted() const { return bool(general_flag() & 0x0001); } - bool implode_8k_dict() const { return bool(general_flag() & 0x0002); } - bool implode_3_trees() const { return bool(general_flag() & 0x0004); } - unsigned deflate_option() const { return unsigned((general_flag() >> 1) & 0x0003); } - bool use_descriptor() const { return bool(general_flag() & 0x0008); } - bool patch_data() const { return bool(general_flag() & 0x0020); } - bool strong_encryption() const { return bool(general_flag() & 0x0040); } - bool utf8_encoding() const { return bool(general_flag() & 0x0800); } - bool directory_encryption() const { return bool(general_flag() & 0x2000); } + std::uint32_t signature() const { return read_dword(0x00); } + std::uint8_t version_needed() const { return m_buffer[0x04]; } + std::uint8_t os_needed() const { return m_buffer[0x05]; } + std::uint16_t general_flag() const { return read_word(0x06); } + std::uint16_t compression_method() const { return read_word(0x08); } + std::uint16_t modified_time() const { return read_word(0x0a); } + std::uint16_t modified_date() const { return read_word(0x0c); } + std::uint32_t crc32() const { return read_dword(0x0e); } + std::uint32_t compressed_size() const { return read_dword(0x12); } + std::uint32_t uncompressed_size() const { return read_dword(0x16); } + std::uint16_t file_name_length() const { return read_word(0x1a); } + std::uint16_t extra_field_length() const { return read_word(0x1c); } + std::string file_name() const { return read_string(0x1e, file_name_length()); } + void file_name(std::string &result) const { read_string(result, 0x1e, file_name_length()); } + extra_field_reader extra_field() const { return extra_field_reader(m_buffer + 0x1e + file_name_length(), extra_field_length()); } + + bool signature_correct() const { return signature() == 0x04034b50; } std::size_t total_length() const { return minimum_length() + file_name_length() + extra_field_length(); } static std::size_t minimum_length() { return 0x1e; } @@ -365,40 +383,32 @@ class central_dir_entry_reader : private reader_base public: central_dir_entry_reader(void const *buf) : reader_base(buf) { } - std::uint32_t signature() const { return read_dword(0x00); } - std::uint8_t version_created() const { return m_buffer[0x04]; } - std::uint8_t os_created() const { return m_buffer[0x05]; } - std::uint8_t version_needed() const { return m_buffer[0x06]; } - std::uint8_t os_needed() const { return m_buffer[0x07]; } - std::uint16_t general_flag() const { return read_word(0x08); } - std::uint16_t compression_method() const { return read_word(0x0a); } - std::uint16_t modified_time() const { return read_word(0x0c); } - std::uint16_t modified_date() const { return read_word(0x0e); } - std::uint32_t crc32() const { return read_dword(0x10); } - std::uint32_t compressed_size() const { return read_dword(0x14); } - std::uint32_t uncompressed_size() const { return read_dword(0x18); } - std::uint16_t file_name_length() const { return read_word(0x1c); } - std::uint16_t extra_field_length() const { return read_word(0x1e); } - std::uint16_t file_comment_length() const { return read_word(0x20); } - std::uint16_t start_disk() const { return read_word(0x22); } - std::uint16_t int_file_attr() const { return read_word(0x24); } - std::uint32_t ext_file_attr() const { return read_dword(0x26); } - std::uint32_t header_offset() const { return read_dword(0x2a); } - std::string file_name() const { return read_string(0x2e, file_name_length()); } - extra_field_reader extra_field() const { return extra_field_reader(m_buffer + 0x2e + file_name_length(), extra_field_length()); } - std::string file_comment() const { return read_string(0x2e + file_name_length() + extra_field_length(), file_comment_length()); } - - bool signature_correct() const { return signature() == 0x02014b50; } - - bool encrypted() const { return bool(general_flag() & 0x0001); } - bool implode_8k_dict() const { return bool(general_flag() & 0x0002); } - bool implode_3_trees() const { return bool(general_flag() & 0x0004); } - unsigned deflate_option() const { return unsigned((general_flag() >> 1) & 0x0003); } - bool use_descriptor() const { return bool(general_flag() & 0x0008); } - bool patch_data() const { return bool(general_flag() & 0x0020); } - bool strong_encryption() const { return bool(general_flag() & 0x0040); } - bool utf8_encoding() const { return bool(general_flag() & 0x0800); } - bool directory_encryption() const { return bool(general_flag() & 0x2000); } + std::uint32_t signature() const { return read_dword(0x00); } + std::uint8_t version_created() const { return m_buffer[0x04]; } + std::uint8_t os_created() const { return m_buffer[0x05]; } + std::uint8_t version_needed() const { return m_buffer[0x06]; } + std::uint8_t os_needed() const { return m_buffer[0x07]; } + std::uint16_t general_flag() const { return read_word(0x08); } + std::uint16_t compression_method() const { return read_word(0x0a); } + std::uint16_t modified_time() const { return read_word(0x0c); } + std::uint16_t modified_date() const { return read_word(0x0e); } + std::uint32_t crc32() const { return read_dword(0x10); } + std::uint32_t compressed_size() const { return read_dword(0x14); } + std::uint32_t uncompressed_size() const { return read_dword(0x18); } + std::uint16_t file_name_length() const { return read_word(0x1c); } + std::uint16_t extra_field_length() const { return read_word(0x1e); } + std::uint16_t file_comment_length() const { return read_word(0x20); } + std::uint16_t start_disk() const { return read_word(0x22); } + std::uint16_t int_file_attr() const { return read_word(0x24); } + std::uint32_t ext_file_attr() const { return read_dword(0x26); } + std::uint32_t header_offset() const { return read_dword(0x2a); } + std::string file_name() const { return read_string(0x2e, file_name_length()); } + void file_name(std::string &result) const { read_string(result, 0x2e, file_name_length()); } + extra_field_reader extra_field() const { return extra_field_reader(m_buffer + 0x2e + file_name_length(), extra_field_length()); } + std::string file_comment() const { return read_string(0x2e + file_name_length() + extra_field_length(), file_comment_length()); } + void file_comment(std::string &result) const { read_string(result, 0x2e + file_name_length() + extra_field_length(), file_comment_length()); } + + bool signature_correct() const { return signature() == 0x02014b50; } std::size_t total_length() const { return minimum_length() + file_name_length() + extra_field_length() + file_comment_length(); } static std::size_t minimum_length() { return 0x2e; } @@ -453,23 +463,103 @@ class ecd_reader : private reader_base public: ecd_reader(void const *buf) : reader_base(buf) { } - std::uint32_t signature() const { return read_dword(0x00); } - std::uint16_t this_disk_no() const { return read_word(0x04); } - std::uint16_t dir_start_disk() const { return read_word(0x06); } - std::uint16_t dir_disk_entries() const { return read_word(0x08); } - std::uint16_t dir_total_entries() const { return read_word(0x0a); } - std::uint32_t dir_size() const { return read_dword(0x0c); } - std::uint32_t dir_offset() const { return read_dword(0x10); } - std::uint16_t comment_length() const { return read_word(0x14); } - std::string comment() const { return read_string(0x16, comment_length()); } + std::uint32_t signature() const { return read_dword(0x00); } + std::uint16_t this_disk_no() const { return read_word(0x04); } + std::uint16_t dir_start_disk() const { return read_word(0x06); } + std::uint16_t dir_disk_entries() const { return read_word(0x08); } + std::uint16_t dir_total_entries() const { return read_word(0x0a); } + std::uint32_t dir_size() const { return read_dword(0x0c); } + std::uint32_t dir_offset() const { return read_dword(0x10); } + std::uint16_t comment_length() const { return read_word(0x14); } + std::string comment() const { return read_string(0x16, comment_length()); } + void comment(std::string &result) const { read_string(result, 0x16, comment_length()); } - bool signature_correct() const { return signature() == 0x06054b50; } + bool signature_correct() const { return signature() == 0x06054b50; } std::size_t total_length() const { return minimum_length() + comment_length(); } static std::size_t minimum_length() { return 0x16; } }; +class zip64_ext_info_reader : private reader_base +{ +public: + template <typename T> + zip64_ext_info_reader( + T const &header, + extra_field_reader const &field) + : reader_base(field.data()) + , m_uncompressed_size(header.uncompressed_size()) + , m_compressed_size(header.compressed_size()) + , m_header_offset(header.header_offset()) + , m_start_disk(header.start_disk()) + , m_offs_compressed_size(~m_uncompressed_size ? 0 : 8) + , m_offs_header_offset(m_offs_compressed_size + (~m_compressed_size ? 0 : 8)) + , m_offs_start_disk(m_offs_header_offset + (~m_header_offset ? 0 : 8)) + , m_offs_end(m_offs_start_disk + (~m_start_disk ? 0 : 4)) + { + } + + std::uint64_t uncompressed_size() const { return ~m_uncompressed_size ? m_uncompressed_size : read_qword(0x00); } + std::uint64_t compressed_size() const { return ~m_compressed_size ? m_compressed_size : read_qword(m_offs_compressed_size); } + std::uint64_t header_offset() const { return ~m_header_offset ? m_header_offset : read_qword(m_offs_header_offset); } + std::uint32_t start_disk() const { return ~m_start_disk ? m_start_disk : read_dword(m_offs_start_disk); } + + std::size_t total_length() const { return minimum_length() + m_offs_end; } + static std::size_t minimum_length() { return 0x00; } + +private: + std::uint32_t m_uncompressed_size; + std::uint32_t m_compressed_size; + std::uint32_t m_header_offset; + std::uint16_t m_start_disk; + + std::size_t m_offs_compressed_size; + std::size_t m_offs_header_offset; + std::size_t m_offs_start_disk; + std::size_t m_offs_end; +}; + + +class utf8_path_reader : private reader_base +{ +public: + utf8_path_reader(extra_field_reader const &field) : reader_base(field.data()), m_length(field.data_size()) { } + + std::uint8_t version() const { return m_buffer[0]; } + std::uint32_t name_crc32() const { return read_dword(0x01); } + std::string unicode_name() const { return read_string(0x05, m_length - 0x05); } + void unicode_name(std::string &result) const { return read_string(result, 0x05, m_length - 0x05); } + + std::size_t total_length() const { return m_length; } + static std::size_t minimum_length() { return 0x05; } + +private: + std::size_t m_length; +}; + + +class general_flag_reader +{ +public: + general_flag_reader(std::uint16_t val) : m_value(val) { } + + bool encrypted() const { return bool(m_value & 0x0001); } + bool implode_8k_dict() const { return bool(m_value & 0x0002); } + bool implode_3_trees() const { return bool(m_value & 0x0004); } + unsigned deflate_option() const { return unsigned((m_value >> 1) & 0x0003); } + bool lzma_eos_mark() const { return bool(m_value & 0x0002); } + bool use_descriptor() const { return bool(m_value & 0x0008); } + bool patch_data() const { return bool(m_value & 0x0020); } + bool strong_encryption() const { return bool(m_value & 0x0040); } + bool utf8_encoding() const { return bool(m_value & 0x0800); } + bool directory_encryption() const { return bool(m_value & 0x2000); } + +private: + std::uint16_t m_value; +}; + + /*************************************************************************** GLOBAL VARIABLES @@ -486,14 +576,6 @@ std::mutex zip_file_impl::s_cache_mutex; to the cache -------------------------------------------------*/ -/** - * @fn void zip_file_close(zip_file *zip) - * - * @brief Zip file close. - * - * @param [in,out] zip If non-null, the zip. - */ - void zip_file_impl::close(ptr &&zip) { if (!zip) return; @@ -510,7 +592,11 @@ void zip_file_impl::close(ptr &&zip) // if no room left in the cache, free the bottommost entry if (cachenum == s_cache.size()) - s_cache[--cachenum].reset(); + { + cachenum--; + osd_printf_verbose("unzip: removing %s from cache to make space\n", s_cache[cachenum]->m_filename.c_str()); + s_cache[cachenum].reset(); + } // move everyone else down and place us at the top for ( ; cachenum > 0; cachenum--) @@ -524,19 +610,13 @@ void zip_file_impl::close(ptr &&zip) ***************************************************************************/ /*------------------------------------------------- - zip_file_first_entry - return the first entry - in the ZIP --------------------------------------------------*/ - -/*------------------------------------------------- - zip_file_next_entry - return the next entry - in the ZIP + zip_file_search - return the next matching + entry in the ZIP -------------------------------------------------*/ int zip_file_impl::search(std::uint32_t search_crc, const std::string &search_filename, bool matchcrc, bool matchname, bool partialpath) { // if we're at or past the end, we're done - std::string filename; while ((m_cd_pos + central_dir_entry_reader::minimum_length()) <= m_ecd.cd_size) { // make sure we have enough data @@ -555,43 +635,65 @@ int zip_file_impl::search(std::uint32_t search_crc, const std::string &search_fi m_header.start_disk_number = reader.start_disk(); m_header.local_header_offset = reader.header_offset(); - // look for ZIP64 extended info + // advance the position + m_cd_pos += reader.total_length(); + + // copy the filename + bool is_utf8(general_flag_reader(m_header.bit_flag).utf8_encoding()); + reader.file_name(m_header.file_name); + + // walk the extra data for (auto extra = reader.extra_field(); extra.length_sufficient(); extra = extra.next()) { + // look for ZIP64 extended info if ((extra.header_id() == 0x0001) && (extra.data_size() >= zip64_ext_info_reader::minimum_length())) { - zip64_ext_info_reader const ext64(extra.data()); + zip64_ext_info_reader const ext64(reader, extra); if (extra.data_size() >= ext64.total_length()) { m_header.compressed_length = ext64.compressed_size(); m_header.uncompressed_length = ext64.uncompressed_size(); m_header.start_disk_number = ext64.start_disk(); - break; + m_header.local_header_offset = ext64.header_offset(); + } + } + + // look for Info-ZIP UTF-8 path + if (!is_utf8 && (extra.header_id() == 0x7075) && (extra.data_size() >= utf8_path_reader::minimum_length())) + { + utf8_path_reader const utf8path(extra); + if (utf8path.version() == 1) + { + auto const addr(m_header.file_name.empty() ? nullptr : &m_header.file_name[0]); + auto const length(m_header.file_name.empty() ? 0 : m_header.file_name.length() * sizeof(m_header.file_name[0])); + auto const crc(crc32_creator::simple(addr, length)); + if (utf8path.name_crc32() == crc.m_raw) + { + utf8path.unicode_name(m_header.file_name); + is_utf8 = true; + } } } } - // advance the position - m_cd_pos += reader.total_length(); + // FIXME: if (!is_utf8) convert filename to UTF8 (assume CP437 or something) - // copy the filename filename - filename = reader.file_name(); - bool const is_dir((filename.length() > 0) && (filename[filename.length() - 1] == '/')); - if (is_dir) filename.resize(filename.length() - 1); + // chop off trailing slash for directory entries + bool const is_dir(!m_header.file_name.empty() && (m_header.file_name.back() == '/')); + if (is_dir) m_header.file_name.resize(m_header.file_name.length() - 1); // check to see if it matches query bool const crcmatch(search_crc == m_header.crc); - auto const partialoffset(filename.length() - search_filename.length()); - bool const partialpossible((filename.length() > search_filename.length()) && (filename[partialoffset - 1] == '/')); + auto const partialoffset(m_header.file_name.length() - search_filename.length()); + bool const partialpossible((m_header.file_name.length() > search_filename.length()) && (m_header.file_name[partialoffset - 1] == '/')); const bool namematch( - !core_stricmp(search_filename.c_str(), filename.c_str()) || - (partialpath && partialpossible && !core_stricmp(search_filename.c_str(), filename.c_str() + partialoffset))); + !core_stricmp(search_filename.c_str(), m_header.file_name.c_str()) || + (partialpath && partialpossible && !core_stricmp(search_filename.c_str(), m_header.file_name.c_str() + partialoffset))); bool const found = ((!matchcrc && !matchname) || !is_dir) && (!matchcrc || crcmatch) && (!matchname || namematch); if (found) { m_curr_is_dir = is_dir; - m_curr_name = std::move(filename); return 0; } } @@ -604,18 +706,6 @@ int zip_file_impl::search(std::uint32_t search_crc, const std::string &search_fi from a ZIP into the target buffer -------------------------------------------------*/ -/** - * @fn zip_error zip_file_decompress(zip_file *zip, void *buffer, UINT32 length) - * - * @brief Zip file decompress. - * - * @param [in,out] zip If non-null, the zip. - * @param [in,out] buffer If non-null, the buffer. - * @param length The length. - * - * @return A zip_error. - */ - archive_file::error zip_file_impl::decompress(void *buffer, std::uint32_t length) { archive_file::error ziperr; @@ -623,11 +713,17 @@ archive_file::error zip_file_impl::decompress(void *buffer, std::uint32_t length // if we don't have enough buffer, error if (length < m_header.uncompressed_length) + { + osd_printf_error("unzip: buffer too small to decompress %s from %s\n", m_header.file_name.c_str(), m_filename.c_str()); return archive_file::error::BUFFER_TOO_SMALL; + } // make sure the info in the header aligns with what we know if (m_header.start_disk_number != m_ecd.disk_number) + { + osd_printf_error("unzip: %s does not reside in segment %s\n", m_header.file_name.c_str(), m_filename.c_str()); return archive_file::error::UNSUPPORTED; + } // get the compressed data offset ziperr = get_compressed_data_offset(offset); @@ -645,7 +741,14 @@ archive_file::error zip_file_impl::decompress(void *buffer, std::uint32_t length ziperr = decompress_data_type_8(offset, buffer, length); break; + case 14: + ziperr = decompress_data_type_14(offset, buffer, length); + break; + default: + osd_printf_error( + "unzip: %s in %s uses unsupported compression method %u\n", + m_header.file_name.c_str(), m_filename.c_str(), m_header.compression); ziperr = archive_file::error::UNSUPPORTED; break; } @@ -662,16 +765,6 @@ archive_file::error zip_file_impl::decompress(void *buffer, std::uint32_t length read_ecd - read the ECD data -------------------------------------------------*/ -/** - * @fn static zip_error read_ecd(zip_file *zip) - * - * @brief Reads an ecd. - * - * @param [in,out] zip If non-null, the zip. - * - * @return The ecd. - */ - archive_file::error zip_file_impl::read_ecd() { // make sure the file handle is open @@ -692,12 +785,19 @@ archive_file::error zip_file_impl::read_ecd() // allocate buffer std::unique_ptr<std::uint8_t []> buffer; try { buffer.reset(new std::uint8_t[buflen + 1]); } - catch (...) { return archive_file::error::OUT_OF_MEMORY; } + catch (...) + { + osd_printf_error("unzip: %s failed to allocate memory for ECD search\n", m_filename.c_str()); + return archive_file::error::OUT_OF_MEMORY; + } // read in one buffers' worth of data error = m_file->read(&buffer[0], m_length - buflen, buflen, read_length); if ((error != osd_file::error::NONE) || (read_length != buflen)) + { + osd_printf_error("unzip: %s error reading file to search for ECD (%d)\n", m_filename.c_str(), int(error)); return archive_file::error::FILE_ERROR; + } // find the ECD signature std::int32_t offset; @@ -722,7 +822,10 @@ archive_file::error zip_file_impl::read_ecd() // is the file too small to contain a ZIP64 ECD locator? if ((m_length - buflen + offset) < ecd64_locator_reader::minimum_length()) + { + osd_printf_verbose("unzip: %s too small to contain ZIP64 ECD locator\n", m_filename.c_str()); return archive_file::error::NONE; + } // try to read the ZIP64 ECD locator error = m_file->read( @@ -731,32 +834,61 @@ archive_file::error zip_file_impl::read_ecd() ecd64_locator_reader::minimum_length(), read_length); if ((error != osd_file::error::NONE) || (read_length != ecd64_locator_reader::minimum_length())) + { + osd_printf_error("unzip: error reading %s to search for ZIP64 ECD locator (%d)\n", m_filename.c_str(), int(error)); return archive_file::error::FILE_ERROR; + } // if the signature isn't correct, it's not a ZIP64 archive ecd64_locator_reader const ecd64_loc_rd(buffer.get()); if (!ecd64_loc_rd.signature_correct()) + { + osd_printf_verbose("unzip: %s has no ZIP64 ECD locator\n", m_filename.c_str()); return archive_file::error::NONE; + } // check that the ZIP64 ECD is in this segment (assuming this segment is the last segment) if ((ecd64_loc_rd.ecd64_disk() + 1) != ecd64_loc_rd.total_disks()) + { + osd_printf_error("unzip: %s ZIP64 ECD resides in a different segment\n", m_filename.c_str()); return archive_file::error::UNSUPPORTED; + } // try to read the ZIP64 ECD error = m_file->read(&buffer[0], ecd64_loc_rd.ecd64_offset(), ecd64_reader::minimum_length(), read_length); if (error != osd_file::error::NONE) + { + osd_printf_error("unzip: error reading %s ZIP64 ECD (%d)\n", m_filename.c_str(), int(error)); return archive_file::error::FILE_ERROR; + } if (read_length != ecd64_reader::minimum_length()) + { + osd_printf_error("unzip: unexpectedly reached end-of-file while reading %s ZIP64 ECD\n", m_filename.c_str()); return archive_file::error::FILE_TRUNCATED; + } // check ZIP64 ECD ecd64_reader const ecd64_rd(buffer.get()); if (!ecd64_rd.signature_correct()) + { + osd_printf_error( + "unzip: %s ZIP64 ECD has incorrect signature 0x%08lx\n", + m_filename.c_str(), long(ecd64_rd.signature())); return archive_file::error::BAD_SIGNATURE; + } if (ecd64_rd.total_length() < ecd64_reader::minimum_length()) + { + osd_printf_error("unzip: %s ZIP64 ECD appears to be too small\n", m_filename.c_str()); return archive_file::error::UNSUPPORTED; + } if (ecd64_rd.version_needed() > 63) + { + osd_printf_error( + "unzip: %s ZIP64 ECD requires unsupported version %u.%u\n", + m_filename.c_str(), unsigned(ecd64_rd.version_needed() / 10), unsigned(ecd64_rd.version_needed() % 10)); return archive_file::error::UNSUPPORTED; + } + osd_printf_verbose("unzip: found %s ZIP64 ECD\n", m_filename.c_str()); // extract ZIP64 ECD info m_ecd.disk_number = ecd64_rd.this_disk_no(); @@ -771,10 +903,16 @@ archive_file::error zip_file_impl::read_ecd() // didn't find it; free this buffer and expand our search if (buflen < m_length) + { buflen *= 2; + } else + { + osd_printf_error("unzip: %s couldn't find ECD\n", m_filename.c_str()); return archive_file::error::BAD_SIGNATURE; + } } + osd_printf_error("unzip: %s couldn't find ECD in last 64KiB\n", m_filename.c_str()); return archive_file::error::OUT_OF_MEMORY; } @@ -784,27 +922,32 @@ archive_file::error zip_file_impl::read_ecd() offset of the compressed data -------------------------------------------------*/ -/** - * @fn static zip_error get_compressed_data_offset(zip_file *zip, UINT64 *offset) - * - * @brief Gets compressed data offset. - * - * @param [in,out] zip If non-null, the zip. - * @param [in,out] offset If non-null, the offset. - * - * @return The compressed data offset. - */ - archive_file::error zip_file_impl::get_compressed_data_offset(std::uint64_t &offset) { // don't support a number of features - if ((m_header.start_disk_number != m_ecd.disk_number) || // in a different segment - (m_header.version_needed > 63) || // future version of specification - (m_header.bit_flag & 0x0001) || // encrypted - (m_header.bit_flag & 0x0020) || // compressed patch data - (m_header.bit_flag & 0x0040) || // strong encryption - (m_header.bit_flag & 0x2000)) // directory encryption + general_flag_reader const flags(m_header.bit_flag); + if (m_header.start_disk_number != m_ecd.disk_number) + { + osd_printf_error("unzip: %s does not reside in segment %s\n", m_header.file_name.c_str(), m_filename.c_str()); + return archive_file::error::UNSUPPORTED; + } + if (m_header.version_needed > 63) + { + osd_printf_error( + "unzip: %s in %s requires unsupported version %u.%u\n", + m_header.file_name.c_str(), m_filename.c_str(), unsigned(m_header.version_needed / 10), unsigned(m_header.version_needed % 10)); return archive_file::error::UNSUPPORTED; + } + if (flags.encrypted() || flags.strong_encryption()) + { + osd_printf_error("unzip: %s in %s is encrypted\n", m_header.file_name.c_str(), m_filename.c_str()); + return archive_file::error::UNSUPPORTED; + } + if (flags.patch_data()) + { + osd_printf_error("unzip: %s in %s is compressed patch data\n", m_header.file_name.c_str(), m_filename.c_str()); + return archive_file::error::UNSUPPORTED; + } // make sure the file handle is open auto const ziperr = reopen(); @@ -815,14 +958,29 @@ archive_file::error zip_file_impl::get_compressed_data_offset(std::uint64_t &off std::uint32_t read_length; auto const error = m_file->read(&m_buffer[0], m_header.local_header_offset, local_file_header_reader::minimum_length(), read_length); if (error != osd_file::error::NONE) + { + osd_printf_error( + "unzip: error reading local file header for %s in %s (%d)\n", + m_header.file_name.c_str(), m_filename.c_str(), int(error)); return archive_file::error::FILE_ERROR; + } if (read_length != local_file_header_reader::minimum_length()) + { + osd_printf_error( + "unzip: unexpectedly reached end-of-file while reading local file header for %s in %s\n", + m_header.file_name.c_str(), m_filename.c_str()); return archive_file::error::FILE_TRUNCATED; + } // compute the final offset local_file_header_reader reader(&m_buffer[0]); if (!reader.signature_correct()) + { + osd_printf_error( + "unzip: local file header for %s in %s has incorrect signature %04lx\n", + m_header.file_name.c_str(), m_filename.c_str(), long(reader.signature())); return archive_file::error::BAD_SIGNATURE; + } offset = m_header.local_header_offset + reader.total_length(); return archive_file::error::NONE; @@ -839,31 +997,25 @@ archive_file::error zip_file_impl::get_compressed_data_offset(std::uint64_t &off type 0 data (which is uncompressed) -------------------------------------------------*/ -/** - * @fn static zip_error decompress_data_type_0(zip_file *zip, UINT64 offset, void *buffer, UINT32 length) - * - * @brief Decompress the data type 0. - * - * @param [in,out] zip If non-null, the zip. - * @param offset The offset. - * @param [in,out] buffer If non-null, the buffer. - * @param length The length. - * - * @return A zip_error. - */ - archive_file::error zip_file_impl::decompress_data_type_0(std::uint64_t offset, void *buffer, std::uint32_t length) { - std::uint32_t read_length; - // the data is uncompressed; just read it + std::uint32_t read_length(0); auto const filerr = m_file->read(buffer, offset, m_header.compressed_length, read_length); if (filerr != osd_file::error::NONE) + { + osd_printf_error("unzip: error reading %s from %s (%d)\n", m_header.file_name.c_str(), m_filename.c_str(), int(filerr)); return archive_file::error::FILE_ERROR; + } else if (read_length != m_header.compressed_length) + { + osd_printf_error("unzip: unexpectedly reached end-of-file while reading %s from %s\n", m_header.file_name.c_str(), m_filename.c_str()); return archive_file::error::FILE_TRUNCATED; + } else + { return archive_file::error::NONE; + } } @@ -872,51 +1024,56 @@ archive_file::error zip_file_impl::decompress_data_type_0(std::uint64_t offset, type 8 data (which is deflated) -------------------------------------------------*/ -/** - * @fn static zip_error decompress_data_type_8(zip_file *zip, UINT64 offset, void *buffer, UINT32 length) - * - * @brief Decompress the data type 8. - * - * @param [in,out] zip If non-null, the zip. - * @param offset The offset. - * @param [in,out] buffer If non-null, the buffer. - * @param length The length. - * - * @return A zip_error. - */ - archive_file::error zip_file_impl::decompress_data_type_8(std::uint64_t offset, void *buffer, std::uint32_t length) { - std::uint32_t input_remaining = m_header.compressed_length; + std::uint64_t input_remaining = m_header.compressed_length; int zerr; - /* reset the stream */ + // reset the stream z_stream stream; - memset(&stream, 0, sizeof(stream)); - stream.next_out = (Bytef *)buffer; + stream.zalloc = Z_NULL; + stream.zfree = Z_NULL; + stream.opaque = Z_NULL; + stream.avail_in = 0; + stream.next_out = reinterpret_cast<Bytef *>(buffer); stream.avail_out = length; // initialize the decompressor zerr = inflateInit2(&stream, -MAX_WBITS); if (zerr != Z_OK) + { + osd_printf_error( + "unzip: error allocating zlib stream to inflate %s from %s (%d)\n", + m_header.file_name.c_str(), m_filename.c_str(), zerr); return archive_file::error::DECOMPRESS_ERROR; + } // loop until we're done while (1) { // read in the next chunk of data - std::uint32_t read_length; - auto const filerr = m_file->read(&m_buffer[0], offset, (std::min<std::uint32_t>)(input_remaining, m_buffer.size()), read_length); + std::uint32_t read_length(0); + auto const filerr = m_file->read( + &m_buffer[0], + offset, + std::uint32_t((std::min<std::uint64_t>)(input_remaining, m_buffer.size())), + read_length); if (filerr != osd_file::error::NONE) { + osd_printf_error( + "unzip: error reading compressed data for %s in %s (%d)\n", + m_header.file_name.c_str(), m_filename.c_str(), int(filerr)); inflateEnd(&stream); return archive_file::error::FILE_ERROR; } offset += read_length; // if we read nothing, but still have data left, the file is truncated - if (read_length == 0 && input_remaining > 0) + if ((read_length == 0) && (input_remaining > 0)) { + osd_printf_error( + "unzip: unexpectedly reached end-of-file while reading compressed data for %s in %s\n", + m_header.file_name.c_str(), m_filename.c_str()); inflateEnd(&stream); return archive_file::error::FILE_TRUNCATED; } @@ -933,9 +1090,12 @@ archive_file::error zip_file_impl::decompress_data_type_8(std::uint64_t offset, // now inflate zerr = inflate(&stream, Z_NO_FLUSH); if (zerr == Z_STREAM_END) + { break; - if (zerr != Z_OK) + } + else if (zerr != Z_OK) { + osd_printf_error("unzip: error inflating %s from %s (%d)\n", m_header.file_name.c_str(), m_filename.c_str(), zerr); inflateEnd(&stream); return archive_file::error::DECOMPRESS_ERROR; } @@ -944,15 +1104,210 @@ archive_file::error zip_file_impl::decompress_data_type_8(std::uint64_t offset, // finish decompression zerr = inflateEnd(&stream); if (zerr != Z_OK) + { + osd_printf_error("unzip: error finishing inflation of %s from %s (%d)\n", m_header.file_name.c_str(), m_filename.c_str(), zerr); return archive_file::error::DECOMPRESS_ERROR; + } - /* if anything looks funny, report an error */ - if (stream.avail_out > 0 || input_remaining > 0) + // if anything looks funny, report an error + if ((stream.avail_out > 0) || (input_remaining > 0)) + { + osd_printf_error( + "unzip: inflation of %s from %s doesn't appear to have completed correctly\n", + m_header.file_name.c_str(), m_filename.c_str()); return archive_file::error::DECOMPRESS_ERROR; + } return archive_file::error::NONE; } + +/*------------------------------------------------- + decompress_data_type_14 - decompress + type 14 data (LZMA) +-------------------------------------------------*/ + +archive_file::error zip_file_impl::decompress_data_type_14(std::uint64_t offset, void *buffer, std::uint32_t length) +{ + // two-byte version + // two-byte properties size (little-endian) + // properties + // compressed data + + assert(4 <= m_buffer.size()); + assert(LZMA_PROPS_SIZE <= m_buffer.size()); + + bool const eos_mark(general_flag_reader(m_header.bit_flag).lzma_eos_mark()); + Byte *const output(reinterpret_cast<Byte *>(buffer)); + SizeT output_remaining(length); + std::uint64_t input_remaining(m_header.compressed_length); + + std::uint32_t read_length; + osd_file::error filerr; + SRes lzerr; + ELzmaStatus lzstatus(LZMA_STATUS_MAYBE_FINISHED_WITHOUT_MARK); + + // reset the stream + ISzAlloc alloc_imp; + alloc_imp.Alloc = [](void *p, std::size_t size) -> void * { return (size == 0) ? nullptr : std::malloc(size); }; + alloc_imp.Free = [](void *p, void *address) -> void { std::free(address); }; + CLzmaDec stream; + LzmaDec_Construct(&stream); + + // need to read LZMA properties before we can initialise the decompressor + if (4 > input_remaining) + { + osd_printf_error( + "unzip:compressed data for %s in %s is too small to hold LZMA properties header\n", + m_header.file_name.c_str(), m_filename.c_str()); + return archive_file::error::DECOMPRESS_ERROR; + } + filerr = m_file->read(&m_buffer[0], offset, 4, read_length); + if (filerr != osd_file::error::NONE) + { + osd_printf_error( + "unzip: error reading LZMA properties header for %s in %s (%d)\n", + m_header.file_name.c_str(), m_filename.c_str(), int(filerr)); + return archive_file::error::FILE_ERROR; + } + offset += read_length; + input_remaining -= read_length; + if (4 != read_length) + { + osd_printf_error( + "unzip: unexpectedly reached end-of-file while reading LZMA properties header for %s in %s\n", + m_header.file_name.c_str(), m_filename.c_str()); + return archive_file::error::FILE_TRUNCATED; + } + std::uint16_t const props_size((std::uint16_t(m_buffer[3]) << 8) | std::uint16_t(m_buffer[2])); + if (props_size > m_buffer.size()) + { + osd_printf_error( + "unzip: %s in %s has excessively large LZMA properties\n", + m_header.file_name.c_str(), m_filename.c_str()); + return archive_file::error::UNSUPPORTED; + } + else if (props_size > input_remaining) + { + osd_printf_error( + "unzip:compressed data for %s in %s is too small to hold LZMA properties\n", + m_header.file_name.c_str(), m_filename.c_str()); + return archive_file::error::DECOMPRESS_ERROR; + } + filerr = m_file->read(&m_buffer[0], offset, props_size, read_length); + if (filerr != osd_file::error::NONE) + { + osd_printf_error( + "unzip: error reading LZMA properties for %s in %s (%d)\n", + m_header.file_name.c_str(), m_filename.c_str(), int(filerr)); + return archive_file::error::FILE_ERROR; + } + offset += read_length; + input_remaining -= read_length; + if (props_size != read_length) + { + osd_printf_error( + "unzip: unexpectedly reached end-of-file while reading LZMA properties for %s in %s\n", + m_header.file_name.c_str(), m_filename.c_str()); + return archive_file::error::FILE_TRUNCATED; + } + + // initialize the decompressor + lzerr = LzmaDec_Allocate(&stream, &m_buffer[0], props_size, &alloc_imp); + if (SZ_ERROR_MEM == lzerr) + { + osd_printf_error( + "unzip: memory error allocating LZMA decoder to decompress %s from %s\n", + m_header.file_name.c_str(), m_filename.c_str()); + return archive_file::error::OUT_OF_MEMORY; + } + else if (SZ_OK != lzerr) + { + osd_printf_error( + "unzip: error allocating LZMA decoder to decompress %s from %s (%d)\n", + m_header.file_name.c_str(), m_filename.c_str(), int(lzerr)); + return archive_file::error::DECOMPRESS_ERROR; + } + LzmaDec_Init(&stream); + + // loop until we're done + while (0 < input_remaining) + { + // read in the next chunk of data + filerr = m_file->read( + &m_buffer[0], + offset, + std::uint32_t((std::min<std::uint64_t>)(input_remaining, m_buffer.size())), + read_length); + if (filerr != osd_file::error::NONE) + { + osd_printf_error( + "unzip: error reading compressed data for %s in %s (%d)\n", + m_header.file_name.c_str(), m_filename.c_str(), int(filerr)); + LzmaDec_Free(&stream, &alloc_imp); + return archive_file::error::FILE_ERROR; + } + offset += read_length; + input_remaining -= read_length; + + // if we read nothing, but still have data left, the file is truncated + if ((0 == read_length) && (0 < input_remaining)) + { + osd_printf_error( + "unzip: unexpectedly reached end-of-file while reading compressed data for %s in %s\n", + m_header.file_name.c_str(), m_filename.c_str()); + LzmaDec_Free(&stream, &alloc_imp); + return archive_file::error::FILE_TRUNCATED; + } + + // now decompress + SizeT len(read_length); + // FIXME: is the input length really an in/out parameter or not? + // At least on reaching the end of the input, the input length doesn't seem to get updated + lzerr = LzmaDec_DecodeToBuf( + &stream, + output + length - output_remaining, + &output_remaining, + reinterpret_cast<Byte const *>(&m_buffer[0]), + &len, + ((0 == input_remaining) && eos_mark) ? LZMA_FINISH_END : LZMA_FINISH_ANY, + &lzstatus); + if (SZ_OK != lzerr) + { + osd_printf_error("unzip: error decoding LZMA data for %s in %s (%d)\n", m_header.file_name.c_str(), m_filename.c_str(), int(lzerr)); + LzmaDec_Free(&stream, &alloc_imp); + return archive_file::error::DECOMPRESS_ERROR; + } + } + + // finish decompression + LzmaDec_Free(&stream, &alloc_imp); + + // if anything looks funny, report an error + if (LZMA_STATUS_FINISHED_WITH_MARK == lzstatus) + { + return archive_file::error::NONE; + } + else if (eos_mark) + { + osd_printf_error( + "unzip: LZMA end mark not found for %s in %s (%d)\n", + m_header.file_name.c_str(), m_filename.c_str(), int(lzstatus)); + return archive_file::error::DECOMPRESS_ERROR; + } + else if (LZMA_STATUS_MAYBE_FINISHED_WITHOUT_MARK != lzstatus) + { + osd_printf_error( + "unzip: LZMA decompression of %s from %s doesn't appear to have completed correctly (%d)\n", + m_header.file_name.c_str(), m_filename.c_str(), int(lzstatus)); + return archive_file::error::DECOMPRESS_ERROR; + } + else + { + return archive_file::error::NONE; + } +} + } // anonymous namespace @@ -973,17 +1328,6 @@ void m7z_file_cache_clear(); zip_file_open - opens a ZIP file for reading -------------------------------------------------*/ -/** - * @fn zip_error zip_file_open(const char *filename, zip_file **zip) - * - * @brief Queries if a given zip file open. - * - * @param filename Filename of the file. - * @param [in,out] zip If non-null, the zip. - * - * @return A zip_error. - */ - archive_file::error archive_file::open_zip(const std::string &filename, ptr &result) { // ensure we start with a NULL result |