├── .github ├── FUNDING.yml └── workflows │ └── cmake.yml ├── .gitignore ├── CHANGELOG.md ├── CMakeLists.txt ├── ISArchiveV3.cpp ├── ISArchiveV3.h ├── LICENSE ├── README.md ├── blast.c ├── blast.h ├── build └── .gitkeep ├── config.h.in ├── main.cpp └── test-data ├── TestArchive1-FastCompression.Z ├── TestArchive1-HighCompression.Z ├── TestArchive1-MediumCompression.Z └── TestArchive1-NoCompression.Z /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [wfr] 2 | custom: ['https://algoexplorer.io/address/D3CI5EKC7IRLJMGV74NSKJD6CDXDGS6DT2OQZ4T6NUAB5RIZMTEM37DODI'] 3 | -------------------------------------------------------------------------------- /.github/workflows/cmake.yml: -------------------------------------------------------------------------------- 1 | name: CMake 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) 11 | BUILD_TYPE: Release 12 | 13 | jobs: 14 | build: 15 | # The CMake configure and build commands are platform agnostic and should work equally well on Windows or Mac. 16 | # You can convert this to a matrix build if you need cross-platform coverage. 17 | # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix 18 | runs-on: ubuntu-22.04 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: Print env 24 | run: | 25 | echo github.event.action: ${{ github.event.action }} 26 | echo github.event_name: ${{ github.event_name }} 27 | 28 | - name: Configure CMake 29 | # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. 30 | # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type 31 | run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} 32 | 33 | - name: Build 34 | # Build your program with the given configuration 35 | run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} 36 | 37 | - name: Test 38 | working-directory: ${{github.workspace}}/build 39 | # Execute tests defined by the CMake configuration. 40 | # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail 41 | run: ctest -C ${{env.BUILD_TYPE}} 42 | 43 | - uses: actions/upload-artifact@v4 44 | with: 45 | name: unshieldv3.x86_64 46 | path: ${{github.workspace}}/build/unshieldv3 47 | retention-days: 7 48 | 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | 34 | 35 | # This file is used to ignore files which are generated 36 | # ---------------------------------------------------------------------------- 37 | 38 | *~ 39 | *.autosave 40 | *.a 41 | *.core 42 | *.moc 43 | *.o 44 | *.obj 45 | *.orig 46 | *.rej 47 | *.so 48 | *.so.* 49 | *_pch.h.cpp 50 | *_resource.rc 51 | *.qm 52 | .#* 53 | *.*# 54 | core 55 | !core/ 56 | tags 57 | .DS_Store 58 | .directory 59 | *.debug 60 | Makefile* 61 | *.prl 62 | *.app 63 | moc_*.cpp 64 | ui_*.h 65 | qrc_*.cpp 66 | Thumbs.db 67 | *.res 68 | *.rc 69 | /.qmake.cache 70 | /.qmake.stash 71 | 72 | # qtcreator generated files 73 | *.pro.user* 74 | 75 | # xemacs temporary files 76 | *.flc 77 | 78 | # Vim temporary files 79 | .*.swp 80 | 81 | # Visual Studio generated files 82 | *.ib_pdb_index 83 | *.idb 84 | *.ilk 85 | *.pdb 86 | *.sln 87 | *.suo 88 | *.vcproj 89 | *vcproj.*.*.user 90 | *.ncb 91 | *.sdf 92 | *.opensdf 93 | *.vcxproj 94 | *vcxproj.* 95 | 96 | # MinGW generated files 97 | *.Debug 98 | *.Release 99 | 100 | # Python byte code 101 | *.pyc 102 | 103 | # Binaries 104 | # -------- 105 | *.dll 106 | *.exe 107 | 108 | # Build directory 109 | # --------------- 110 | build/* 111 | 112 | # generated config 113 | config.h 114 | 115 | # clangd cache 116 | .cache/ 117 | 118 | # VScode IDE files 119 | .vscode/ 120 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [0.2.2] 2025-04-24 6 | 7 | ### Fixed 8 | - compilation with modern clang 9 | (thanks to https://github.com/yincrash and https://github.com/jchv) 10 | - stop linking against `libstdc++fs` 11 | - include ` 12 | - Windows path handling 13 | (thanks to https://github.com/yincrash) 14 | 15 | ## [0.2.1] 2022-05-08 16 | 17 | ### Added 18 | 19 | - `info` command, showing archive metadata 20 | - `list -v` command, showing size and date/time 21 | - support for uncompressed archives 22 | 23 | ## [0.1.0] 2022-04-17 24 | 25 | Minimal viable implementation that's able to list and extract regular archives. 26 | 27 | Contributors: 28 | - https://github.com/OmniBlade 29 | - https://github.com/ligfx 30 | - https://github.com/adrium 31 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required (VERSION 3.5) 2 | 3 | project (unshieldv3 LANGUAGES CXX C VERSION "0.2.2") 4 | 5 | if (NOT ${CMAKE_C_BYTE_ORDER} STREQUAL "LITTLE_ENDIAN") 6 | message (FATAL_ERROR "Incompatible target. Only LITTLE_ENDIAN byte order is supported.") 7 | endif() 8 | 9 | set(CMAKE_CXX_STANDARD 17) 10 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 11 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O2 -Wall") 12 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O2 -Wall") 13 | 14 | configure_file( 15 | "${CMAKE_CURRENT_SOURCE_DIR}/config.h.in" 16 | "${CMAKE_CURRENT_SOURCE_DIR}/config.h" 17 | ) 18 | 19 | add_executable (unshieldv3 20 | main.cpp 21 | ISArchiveV3.cpp 22 | blast.c 23 | ) 24 | 25 | target_link_libraries(unshieldv3) 26 | 27 | install(TARGETS unshieldv3) 28 | -------------------------------------------------------------------------------- /ISArchiveV3.cpp: -------------------------------------------------------------------------------- 1 | /* unshieldv3 -- extract InstallShield V3 archives. 2 | Copyright (c) 2019 Wolfgang Frisch 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #include "ISArchiveV3.h" 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | extern "C" { 26 | #include "blast.h" 27 | }; 28 | 29 | namespace fs = std::filesystem; 30 | 31 | 32 | class Directory { 33 | public: 34 | std::string name; 35 | uint16_t file_count; 36 | }; 37 | 38 | 39 | ISArchiveV3::ISArchiveV3(const std::filesystem::path& apath) 40 | : m_path(apath) 41 | { 42 | std::vector directories; 43 | 44 | fin.open(apath, std::ios::in | std::ios::binary); 45 | if (!fin.is_open()) { 46 | std::ostringstream os; 47 | os << "Cannot open archive: " << apath; 48 | throw std::runtime_error(os.str()); 49 | } 50 | uint64_t file_size = fs::file_size(apath); 51 | assert(file_size > sizeof(Header)); 52 | fin.read(reinterpret_cast(&hdr), sizeof(Header)); 53 | assert(hdr.signature1 == 0x8C655D13 && hdr.signature2 == 0x02013a); 54 | assert(hdr.toc_address < file_size); 55 | fin.seekg(hdr.toc_address, std::ios::beg); 56 | 57 | for (int i = 0; i < hdr.dir_count; i++) { 58 | uint16_t file_count = read(); 59 | uint16_t chunk_size = read(); 60 | std::string name = readString16(); 61 | fin.ignore(chunk_size - uint16_t(name.length()) - 6); 62 | directories.push_back({name, file_count}); 63 | } 64 | 65 | for (Directory& directory : directories) { 66 | for (int i = 0; i < directory.file_count; i++) { 67 | File f; 68 | 69 | f.volume_end = read(); 70 | f.index = read(); 71 | f.uncompressed_size = read(); 72 | f.compressed_size = read(); 73 | f.offset = read(); 74 | f.datetime = read(); 75 | uint32_t u2 = read(); 76 | (void)u2; 77 | uint16_t chunk_size = read(); 78 | f.attrib = read(); 79 | f.is_split = read(); 80 | uint8_t u3 = read(); 81 | (void)u3; 82 | f.volume_start = read(); 83 | f.name = readString8(); 84 | fin.ignore(chunk_size - uint16_t(f.name.length()) - 30); 85 | 86 | if (directory.name.length()) { 87 | f.full_path = directory.name + "\\" + f.name; 88 | } else { 89 | f.full_path = f.name; 90 | } 91 | if (!isValidName(f.full_path)) { 92 | throw std::runtime_error(std::string("Invalid file path: ") + f.full_path); 93 | } 94 | 95 | m_files.push_back(f); 96 | } 97 | } 98 | } 99 | 100 | std::tm ISArchiveV3::File::tm() const { 101 | // source: https://github.com/lephilousophe/idecomp 102 | uint16_t file_date = datetime & 0xffff; 103 | uint16_t file_time = (datetime >> 16) & 0xffff; 104 | std::tm tm = { /* .tm_sec = */ (file_time & 0x1f) * 2, 105 | /* .tm_min = */ (file_time >> 5) & 0x3f, 106 | /* .tm_hour = */ (file_time >> 11) & 0x1f, 107 | /* .tm_mday = */ (file_date) & 0x1f, 108 | /* .tm_mon = */ ((file_date >> 5) & 0xf) - 1, 109 | /* .tm_year = */ (((file_date >> 9) & 0x7f) + 1980) - 1900, 110 | }; 111 | tm.tm_isdst = -1; 112 | return tm; 113 | } 114 | 115 | std::filesystem::path ISArchiveV3::File::path() const { 116 | std::string fp = full_path; 117 | // windows paths are wchar_t, convert 118 | char pref_seperator = fs::path::preferred_separator; 119 | std::replace(fp.begin(), fp.end(), 120 | '\\', pref_seperator); 121 | return std::filesystem::path(fp); 122 | } 123 | 124 | std::string ISArchiveV3::File::attribString() const { 125 | std::ostringstream os; 126 | os << (attrib & File::Attributes::ARCHIVE ? 'A' : '_'); 127 | os << (attrib & File::Attributes::HIDDEN ? 'H' : '_'); 128 | os << (attrib & File::Attributes::READONLY ? 'R' : '_'); 129 | os << (attrib & File::Attributes::SYSTEM ? 'S' : '_'); 130 | return os.str(); 131 | } 132 | 133 | const std::vector& ISArchiveV3::files() const { 134 | return m_files; 135 | } 136 | 137 | const ISArchiveV3::File* ISArchiveV3::fileByPath(const std::string& full_path) const { 138 | // std::map would be more efficient but let's keep it simple for now. 139 | for (const auto& file : m_files) { 140 | if (file.full_path == full_path) { 141 | return &file; 142 | } 143 | } 144 | return nullptr; 145 | } 146 | 147 | bool ISArchiveV3::exists(const std::string& full_path) const { 148 | return fileByPath(full_path) != nullptr; 149 | } 150 | 151 | unsigned _blast_in(void *how, unsigned char **buf) { 152 | std::vector *inbuf = reinterpret_cast*>(how); 153 | *buf = inbuf->data(); 154 | return unsigned(inbuf->size()); 155 | } 156 | 157 | int _blast_out(void *how, unsigned char *buf, unsigned len) { 158 | std::vector *outbuf = reinterpret_cast*>(how); 159 | outbuf->insert(outbuf->end(), &buf[0], &buf[len]); 160 | return false; // would indicate write error 161 | } 162 | 163 | std::vector ISArchiveV3::decompress(const std::string& full_path) { 164 | if (!exists(full_path)) { 165 | std::ostringstream os; 166 | os << "decompress() called with invalid path: " << full_path; 167 | throw std::runtime_error(os.str()); 168 | } 169 | const File* file = fileByPath(full_path); 170 | assert(file != nullptr); 171 | fin.seekg(file->offset, std::ios::beg); 172 | std::vector buf(file->compressed_size); 173 | std::vector out; 174 | fin.read(reinterpret_cast(&buf[0]), file->compressed_size); 175 | if (fin.fail()) { 176 | throw std::runtime_error("Read failed"); 177 | } 178 | 179 | if (file->attrib & File::Attributes::UNCOMPRESSED) { 180 | return buf; 181 | } 182 | 183 | int ret; 184 | unsigned left = 0; 185 | ret = blast(_blast_in, static_cast(&buf), _blast_out, static_cast(&out), &left, nullptr); 186 | if (ret != 0) { 187 | std::ostringstream os; 188 | os << "Blast decompression error: " << ret; 189 | throw std::runtime_error(os.str()); 190 | return {}; 191 | } 192 | 193 | return out; 194 | } 195 | 196 | template T ISArchiveV3::read() { 197 | T re; 198 | fin.read(reinterpret_cast(&re), sizeof(re)); 199 | return re; 200 | } 201 | 202 | std::string ISArchiveV3::readString8() { 203 | uint8_t len = read(); 204 | std::vector buf(len); 205 | fin.read(&buf[0], len); 206 | return std::string(buf.begin(), buf.end()); 207 | } 208 | 209 | std::string ISArchiveV3::readString16() { 210 | uint16_t len = read(); 211 | std::vector buf(len); 212 | fin.read(&buf[0], len); 213 | return std::string(buf.begin(), buf.end()); 214 | } 215 | 216 | bool ISArchiveV3::isValidName(const std::string& name) const { 217 | if (name.find("..\\") != std::string::npos) { 218 | return false; 219 | } 220 | if (name.find("../") != std::string::npos) { 221 | return false; 222 | } 223 | return true; 224 | } 225 | -------------------------------------------------------------------------------- /ISArchiveV3.h: -------------------------------------------------------------------------------- 1 | /* unshieldv3 -- extract InstallShield V3 archives. 2 | Copyright (c) 2019 Wolfgang Frisch 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #pragma once 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | class ISArchiveV3 { 25 | public: 26 | ISArchiveV3(const std::filesystem::path& apath); 27 | 28 | class __attribute__ ((packed)) Header { 29 | public: 30 | uint32_t signature1; 31 | uint32_t signature2; 32 | uint16_t u1; 33 | uint16_t is_multivolume; 34 | uint16_t file_count; 35 | uint32_t datetime; 36 | uint32_t compressed_size; 37 | uint32_t uncompressed_size; 38 | uint32_t u2; 39 | uint8_t volume_total; // set in first vol only, zero in subsequent vols 40 | uint8_t volume_number; // [1...n] 41 | uint8_t u3; 42 | uint32_t split_begin_address; 43 | uint32_t split_end_address; 44 | uint32_t toc_address; 45 | uint32_t u4; 46 | uint16_t dir_count; 47 | uint32_t u5; 48 | uint32_t u6; 49 | }; 50 | 51 | class File { 52 | public: 53 | std::string name; 54 | std::string full_path; // Directory separator: \ (Windows) 55 | uint16_t index; // position in archive 56 | uint32_t compressed_size; 57 | uint32_t uncompressed_size; 58 | uint32_t datetime; 59 | uint8_t attrib; 60 | uint32_t offset; 61 | uint8_t is_split; 62 | uint8_t volume_start, volume_end; 63 | 64 | enum Attributes : uint8_t { 65 | READONLY = 0x01, 66 | HIDDEN = 0x02, 67 | SYSTEM = 0x04, 68 | UNCOMPRESSED = 0x10, 69 | ARCHIVE = 0x20 70 | }; 71 | 72 | std::tm tm() const; 73 | std::filesystem::path path() const; 74 | std::string attribString() const; 75 | }; 76 | 77 | const std::vector& files() const; 78 | bool exists(const std::string& full_path) const; 79 | std::vector decompress(const std::string& full_path); 80 | std::filesystem::path path() const { 81 | return m_path; 82 | } 83 | Header header() const { 84 | return hdr; 85 | } 86 | 87 | protected: 88 | template T read(); 89 | std::string readString8(); 90 | std::string readString16(); 91 | bool isValidName(const std::string& name) const; 92 | const File* fileByPath(const std::string& full_path) const; 93 | 94 | const std::filesystem::path m_path; 95 | std::ifstream fin; 96 | std::vector m_files; 97 | Header hdr; 98 | }; 99 | 100 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # unshieldv3 2 | Extract InstallShield V3 (Z) archives. 3 | 4 | ## About InstallShield v3 (Z) archives 5 | InstallShield Z format is a compressed archive format used by version 3 of the InstallShield installation software. 6 | 7 | Z files can be recognized by their magic marker: `13 5D 65 8C 3A 01 02 00`. 8 | 9 | Contained files are compressed with the "PKware implode" algorithm. 10 | 11 | 12 | 13 | ## Usage 14 | ``` 15 | usage: 16 | unshieldv3 help Produce this message 17 | unshieldv3 info ARCHIVE.Z Show archive metadata 18 | unshieldv3 list [-v] ARCHIVE.Z List ARCHIVE contents 19 | unshieldv3 extract ARCHIVE.Z DESTDIR Extract ARCHIVE to DESTDIR 20 | ``` 21 | 22 | e.g. 23 | ``` 24 | $ ./unshieldv3 list -v NETSCAPE.1 25 | Path Size Date 26 | ---- ---- ---- 27 | NHANDBK.ICO 766 1995-06-12 12:17:56 28 | HANDBK.ICO 766 1995-06-12 12:17:48 29 | NSCAPE16.TLB 3728 1995-07-17 17:02:20 30 | NAPLAYER.EXE 149360 1995-06-30 15:51:12 31 | NETSCAPE.EXE 1353344 1996-03-08 21:24:34 32 | ``` 33 | 34 | ## Building 35 | Requirements: GCC, cmake 36 | ``` 37 | cd build/ 38 | cmake .. 39 | make 40 | ``` 41 | 42 | ## References 43 | * Original proprietary (de)compressor: [ICOMP95.EXE](https://www.sac.sk/files.php?d=7&l=I). 44 | * Veit Kannegieser reverse-engineered the file format and wrote 45 | ["stix"](https://github.com/DeclanHoare/stix/) in a mixture of Pascal and x86 46 | Assembly. 47 | * [Clean C implementation of the "implode" 48 | algo](https://github.com/madler/zlib/tree/master/contrib/blast) by [Mark 49 | Adler](https://github.com/madler/). 50 | * [C# unpacker](https://github.com/OpenRA/OpenRA/pull/3342) in OpenRA. 51 | * [C++ unpacker "isextract"](https://github.com/OmniBlade/isextract) by OmniBlade. 52 | * [Python unpacker "idecomp"](https://github.com/lephilousophe/idecomp) by lephilousophe. 53 | 54 | ### See also 55 | Later InstallShield archives can be extracted with [Unshield](https://github.com/twogood/unshield). 56 | 57 | ## Authors 58 | Licensed under Apache 2.0. © Wolfgang Frisch. 59 | 60 | ### Contributors 61 | * @OmniBlade 62 | * @ligfx 63 | * @adrium 64 | -------------------------------------------------------------------------------- /blast.c: -------------------------------------------------------------------------------- 1 | /* blast.c 2 | * Copyright (C) 2003, 2012, 2013 Mark Adler 3 | * For conditions of distribution and use, see copyright notice in blast.h 4 | * version 1.3, 24 Aug 2013 5 | * 6 | * blast.c decompresses data compressed by the PKWare Compression Library. 7 | * This function provides functionality similar to the explode() function of 8 | * the PKWare library, hence the name "blast". 9 | * 10 | * This decompressor is based on the excellent format description provided by 11 | * Ben Rudiak-Gould in comp.compression on August 13, 2001. Interestingly, the 12 | * example Ben provided in the post is incorrect. The distance 110001 should 13 | * instead be 111000. When corrected, the example byte stream becomes: 14 | * 15 | * 00 04 82 24 25 8f 80 7f 16 | * 17 | * which decompresses to "AIAIAIAIAIAIA" (without the quotes). 18 | */ 19 | 20 | /* 21 | * Change history: 22 | * 23 | * 1.0 12 Feb 2003 - First version 24 | * 1.1 16 Feb 2003 - Fixed distance check for > 4 GB uncompressed data 25 | * 1.2 24 Oct 2012 - Add note about using binary mode in stdio 26 | * - Fix comparisons of differently signed integers 27 | * 1.3 24 Aug 2013 - Return unused input from blast() 28 | * - Fix test code to correctly report unused input 29 | * - Enable the provision of initial input to blast() 30 | */ 31 | 32 | #include /* for NULL */ 33 | #include /* for setjmp(), longjmp(), and jmp_buf */ 34 | #include "blast.h" /* prototype for blast() */ 35 | 36 | #define local static /* for local function definitions */ 37 | #define MAXBITS 13 /* maximum code length */ 38 | #define MAXWIN 4096 /* maximum window size */ 39 | 40 | /* input and output state */ 41 | struct state { 42 | /* input state */ 43 | blast_in infun; /* input function provided by user */ 44 | void *inhow; /* opaque information passed to infun() */ 45 | unsigned char *in; /* next input location */ 46 | unsigned left; /* available input at in */ 47 | int bitbuf; /* bit buffer */ 48 | int bitcnt; /* number of bits in bit buffer */ 49 | 50 | /* input limit error return state for bits() and decode() */ 51 | jmp_buf env; 52 | 53 | /* output state */ 54 | blast_out outfun; /* output function provided by user */ 55 | void *outhow; /* opaque information passed to outfun() */ 56 | unsigned next; /* index of next write location in out[] */ 57 | int first; /* true to check distances (for first 4K) */ 58 | unsigned char out[MAXWIN]; /* output buffer and sliding window */ 59 | }; 60 | 61 | /* 62 | * Return need bits from the input stream. This always leaves less than 63 | * eight bits in the buffer. bits() works properly for need == 0. 64 | * 65 | * Format notes: 66 | * 67 | * - Bits are stored in bytes from the least significant bit to the most 68 | * significant bit. Therefore bits are dropped from the bottom of the bit 69 | * buffer, using shift right, and new bytes are appended to the top of the 70 | * bit buffer, using shift left. 71 | */ 72 | local int bits(struct state *s, int need) 73 | { 74 | int val; /* bit accumulator */ 75 | 76 | /* load at least need bits into val */ 77 | val = s->bitbuf; 78 | while (s->bitcnt < need) { 79 | if (s->left == 0) { 80 | s->left = s->infun(s->inhow, &(s->in)); 81 | if (s->left == 0) longjmp(s->env, 1); /* out of input */ 82 | } 83 | val |= (int)(*(s->in)++) << s->bitcnt; /* load eight bits */ 84 | s->left--; 85 | s->bitcnt += 8; 86 | } 87 | 88 | /* drop need bits and update buffer, always zero to seven bits left */ 89 | s->bitbuf = val >> need; 90 | s->bitcnt -= need; 91 | 92 | /* return need bits, zeroing the bits above that */ 93 | return val & ((1 << need) - 1); 94 | } 95 | 96 | /* 97 | * Huffman code decoding tables. count[1..MAXBITS] is the number of symbols of 98 | * each length, which for a canonical code are stepped through in order. 99 | * symbol[] are the symbol values in canonical order, where the number of 100 | * entries is the sum of the counts in count[]. The decoding process can be 101 | * seen in the function decode() below. 102 | */ 103 | struct huffman { 104 | short *count; /* number of symbols of each length */ 105 | short *symbol; /* canonically ordered symbols */ 106 | }; 107 | 108 | /* 109 | * Decode a code from the stream s using huffman table h. Return the symbol or 110 | * a negative value if there is an error. If all of the lengths are zero, i.e. 111 | * an empty code, or if the code is incomplete and an invalid code is received, 112 | * then -9 is returned after reading MAXBITS bits. 113 | * 114 | * Format notes: 115 | * 116 | * - The codes as stored in the compressed data are bit-reversed relative to 117 | * a simple integer ordering of codes of the same lengths. Hence below the 118 | * bits are pulled from the compressed data one at a time and used to 119 | * build the code value reversed from what is in the stream in order to 120 | * permit simple integer comparisons for decoding. 121 | * 122 | * - The first code for the shortest length is all ones. Subsequent codes of 123 | * the same length are simply integer decrements of the previous code. When 124 | * moving up a length, a one bit is appended to the code. For a complete 125 | * code, the last code of the longest length will be all zeros. To support 126 | * this ordering, the bits pulled during decoding are inverted to apply the 127 | * more "natural" ordering starting with all zeros and incrementing. 128 | */ 129 | local int decode(struct state *s, struct huffman *h) 130 | { 131 | int len; /* current number of bits in code */ 132 | int code; /* len bits being decoded */ 133 | int first; /* first code of length len */ 134 | int count; /* number of codes of length len */ 135 | int index; /* index of first code of length len in symbol table */ 136 | int bitbuf; /* bits from stream */ 137 | int left; /* bits left in next or left to process */ 138 | short *next; /* next number of codes */ 139 | 140 | bitbuf = s->bitbuf; 141 | left = s->bitcnt; 142 | code = first = index = 0; 143 | len = 1; 144 | next = h->count + 1; 145 | while (1) { 146 | while (left--) { 147 | code |= (bitbuf & 1) ^ 1; /* invert code */ 148 | bitbuf >>= 1; 149 | count = *next++; 150 | if (code < first + count) { /* if length len, return symbol */ 151 | s->bitbuf = bitbuf; 152 | s->bitcnt = (s->bitcnt - len) & 7; 153 | return h->symbol[index + (code - first)]; 154 | } 155 | index += count; /* else update for next length */ 156 | first += count; 157 | first <<= 1; 158 | code <<= 1; 159 | len++; 160 | } 161 | left = (MAXBITS+1) - len; 162 | if (left == 0) break; 163 | if (s->left == 0) { 164 | s->left = s->infun(s->inhow, &(s->in)); 165 | if (s->left == 0) longjmp(s->env, 1); /* out of input */ 166 | } 167 | bitbuf = *(s->in)++; 168 | s->left--; 169 | if (left > 8) left = 8; 170 | } 171 | return -9; /* ran out of codes */ 172 | } 173 | 174 | /* 175 | * Given a list of repeated code lengths rep[0..n-1], where each byte is a 176 | * count (high four bits + 1) and a code length (low four bits), generate the 177 | * list of code lengths. This compaction reduces the size of the object code. 178 | * Then given the list of code lengths length[0..n-1] representing a canonical 179 | * Huffman code for n symbols, construct the tables required to decode those 180 | * codes. Those tables are the number of codes of each length, and the symbols 181 | * sorted by length, retaining their original order within each length. The 182 | * return value is zero for a complete code set, negative for an over- 183 | * subscribed code set, and positive for an incomplete code set. The tables 184 | * can be used if the return value is zero or positive, but they cannot be used 185 | * if the return value is negative. If the return value is zero, it is not 186 | * possible for decode() using that table to return an error--any stream of 187 | * enough bits will resolve to a symbol. If the return value is positive, then 188 | * it is possible for decode() using that table to return an error for received 189 | * codes past the end of the incomplete lengths. 190 | */ 191 | local int construct(struct huffman *h, const unsigned char *rep, int n) 192 | { 193 | int symbol; /* current symbol when stepping through length[] */ 194 | int len; /* current length when stepping through h->count[] */ 195 | int left; /* number of possible codes left of current length */ 196 | short offs[MAXBITS+1]; /* offsets in symbol table for each length */ 197 | short length[256]; /* code lengths */ 198 | 199 | /* convert compact repeat counts into symbol bit length list */ 200 | symbol = 0; 201 | do { 202 | len = *rep++; 203 | left = (len >> 4) + 1; 204 | len &= 15; 205 | do { 206 | length[symbol++] = len; 207 | } while (--left); 208 | } while (--n); 209 | n = symbol; 210 | 211 | /* count number of codes of each length */ 212 | for (len = 0; len <= MAXBITS; len++) 213 | h->count[len] = 0; 214 | for (symbol = 0; symbol < n; symbol++) 215 | (h->count[length[symbol]])++; /* assumes lengths are within bounds */ 216 | if (h->count[0] == n) /* no codes! */ 217 | return 0; /* complete, but decode() will fail */ 218 | 219 | /* check for an over-subscribed or incomplete set of lengths */ 220 | left = 1; /* one possible code of zero length */ 221 | for (len = 1; len <= MAXBITS; len++) { 222 | left <<= 1; /* one more bit, double codes left */ 223 | left -= h->count[len]; /* deduct count from possible codes */ 224 | if (left < 0) return left; /* over-subscribed--return negative */ 225 | } /* left > 0 means incomplete */ 226 | 227 | /* generate offsets into symbol table for each length for sorting */ 228 | offs[1] = 0; 229 | for (len = 1; len < MAXBITS; len++) 230 | offs[len + 1] = offs[len] + h->count[len]; 231 | 232 | /* 233 | * put symbols in table sorted by length, by symbol order within each 234 | * length 235 | */ 236 | for (symbol = 0; symbol < n; symbol++) 237 | if (length[symbol] != 0) 238 | h->symbol[offs[length[symbol]]++] = symbol; 239 | 240 | /* return zero for complete set, positive for incomplete set */ 241 | return left; 242 | } 243 | 244 | /* 245 | * Decode PKWare Compression Library stream. 246 | * 247 | * Format notes: 248 | * 249 | * - First byte is 0 if literals are uncoded or 1 if they are coded. Second 250 | * byte is 4, 5, or 6 for the number of extra bits in the distance code. 251 | * This is the base-2 logarithm of the dictionary size minus six. 252 | * 253 | * - Compressed data is a combination of literals and length/distance pairs 254 | * terminated by an end code. Literals are either Huffman coded or 255 | * uncoded bytes. A length/distance pair is a coded length followed by a 256 | * coded distance to represent a string that occurs earlier in the 257 | * uncompressed data that occurs again at the current location. 258 | * 259 | * - A bit preceding a literal or length/distance pair indicates which comes 260 | * next, 0 for literals, 1 for length/distance. 261 | * 262 | * - If literals are uncoded, then the next eight bits are the literal, in the 263 | * normal bit order in the stream, i.e. no bit-reversal is needed. Similarly, 264 | * no bit reversal is needed for either the length extra bits or the distance 265 | * extra bits. 266 | * 267 | * - Literal bytes are simply written to the output. A length/distance pair is 268 | * an instruction to copy previously uncompressed bytes to the output. The 269 | * copy is from distance bytes back in the output stream, copying for length 270 | * bytes. 271 | * 272 | * - Distances pointing before the beginning of the output data are not 273 | * permitted. 274 | * 275 | * - Overlapped copies, where the length is greater than the distance, are 276 | * allowed and common. For example, a distance of one and a length of 518 277 | * simply copies the last byte 518 times. A distance of four and a length of 278 | * twelve copies the last four bytes three times. A simple forward copy 279 | * ignoring whether the length is greater than the distance or not implements 280 | * this correctly. 281 | */ 282 | local int decomp(struct state *s) 283 | { 284 | int lit; /* true if literals are coded */ 285 | int dict; /* log2(dictionary size) - 6 */ 286 | int symbol; /* decoded symbol, extra bits for distance */ 287 | int len; /* length for copy */ 288 | unsigned dist; /* distance for copy */ 289 | int copy; /* copy counter */ 290 | unsigned char *from, *to; /* copy pointers */ 291 | static int virgin = 1; /* build tables once */ 292 | static short litcnt[MAXBITS+1], litsym[256]; /* litcode memory */ 293 | static short lencnt[MAXBITS+1], lensym[16]; /* lencode memory */ 294 | static short distcnt[MAXBITS+1], distsym[64]; /* distcode memory */ 295 | static struct huffman litcode = {litcnt, litsym}; /* length code */ 296 | static struct huffman lencode = {lencnt, lensym}; /* length code */ 297 | static struct huffman distcode = {distcnt, distsym};/* distance code */ 298 | /* bit lengths of literal codes */ 299 | static const unsigned char litlen[] = { 300 | 11, 124, 8, 7, 28, 7, 188, 13, 76, 4, 10, 8, 12, 10, 12, 10, 8, 23, 8, 301 | 9, 7, 6, 7, 8, 7, 6, 55, 8, 23, 24, 12, 11, 7, 9, 11, 12, 6, 7, 22, 5, 302 | 7, 24, 6, 11, 9, 6, 7, 22, 7, 11, 38, 7, 9, 8, 25, 11, 8, 11, 9, 12, 303 | 8, 12, 5, 38, 5, 38, 5, 11, 7, 5, 6, 21, 6, 10, 53, 8, 7, 24, 10, 27, 304 | 44, 253, 253, 253, 252, 252, 252, 13, 12, 45, 12, 45, 12, 61, 12, 45, 305 | 44, 173}; 306 | /* bit lengths of length codes 0..15 */ 307 | static const unsigned char lenlen[] = {2, 35, 36, 53, 38, 23}; 308 | /* bit lengths of distance codes 0..63 */ 309 | static const unsigned char distlen[] = {2, 20, 53, 230, 247, 151, 248}; 310 | static const short base[16] = { /* base for length codes */ 311 | 3, 2, 4, 5, 6, 7, 8, 9, 10, 12, 16, 24, 40, 72, 136, 264}; 312 | static const char extra[16] = { /* extra bits for length codes */ 313 | 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8}; 314 | 315 | /* set up decoding tables (once--might not be thread-safe) */ 316 | if (virgin) { 317 | construct(&litcode, litlen, sizeof(litlen)); 318 | construct(&lencode, lenlen, sizeof(lenlen)); 319 | construct(&distcode, distlen, sizeof(distlen)); 320 | virgin = 0; 321 | } 322 | 323 | /* read header */ 324 | lit = bits(s, 8); 325 | if (lit > 1) return -1; 326 | dict = bits(s, 8); 327 | if (dict < 4 || dict > 6) return -2; 328 | 329 | /* decode literals and length/distance pairs */ 330 | do { 331 | if (bits(s, 1)) { 332 | /* get length */ 333 | symbol = decode(s, &lencode); 334 | len = base[symbol] + bits(s, extra[symbol]); 335 | if (len == 519) break; /* end code */ 336 | 337 | /* get distance */ 338 | symbol = len == 2 ? 2 : dict; 339 | dist = decode(s, &distcode) << symbol; 340 | dist += bits(s, symbol); 341 | dist++; 342 | if (s->first && dist > s->next) 343 | return -3; /* distance too far back */ 344 | 345 | /* copy length bytes from distance bytes back */ 346 | do { 347 | to = s->out + s->next; 348 | from = to - dist; 349 | copy = MAXWIN; 350 | if (s->next < dist) { 351 | from += copy; 352 | copy = dist; 353 | } 354 | copy -= s->next; 355 | if (copy > len) copy = len; 356 | len -= copy; 357 | s->next += copy; 358 | do { 359 | *to++ = *from++; 360 | } while (--copy); 361 | if (s->next == MAXWIN) { 362 | if (s->outfun(s->outhow, s->out, s->next)) return 1; 363 | s->next = 0; 364 | s->first = 0; 365 | } 366 | } while (len != 0); 367 | } 368 | else { 369 | /* get literal and write it */ 370 | symbol = lit ? decode(s, &litcode) : bits(s, 8); 371 | s->out[s->next++] = symbol; 372 | if (s->next == MAXWIN) { 373 | if (s->outfun(s->outhow, s->out, s->next)) return 1; 374 | s->next = 0; 375 | s->first = 0; 376 | } 377 | } 378 | } while (1); 379 | return 0; 380 | } 381 | 382 | /* See comments in blast.h */ 383 | int blast(blast_in infun, void *inhow, blast_out outfun, void *outhow, 384 | unsigned *left, unsigned char **in) 385 | { 386 | struct state s; /* input/output state */ 387 | int err; /* return value */ 388 | 389 | /* initialize input state */ 390 | s.infun = infun; 391 | s.inhow = inhow; 392 | if (left != NULL && *left) { 393 | s.left = *left; 394 | s.in = *in; 395 | } 396 | else 397 | s.left = 0; 398 | s.bitbuf = 0; 399 | s.bitcnt = 0; 400 | 401 | /* initialize output state */ 402 | s.outfun = outfun; 403 | s.outhow = outhow; 404 | s.next = 0; 405 | s.first = 1; 406 | 407 | /* return if bits() or decode() tries to read past available input */ 408 | if (setjmp(s.env) != 0) /* if came back here via longjmp(), */ 409 | err = 2; /* then skip decomp(), return error */ 410 | else 411 | err = decomp(&s); /* decompress */ 412 | 413 | /* return unused input */ 414 | if (left != NULL) 415 | *left = s.left; 416 | if (in != NULL) 417 | *in = s.left ? s.in : NULL; 418 | 419 | /* write any leftover output and update the error code if needed */ 420 | if (err != 1 && s.next && s.outfun(s.outhow, s.out, s.next) && err == 0) 421 | err = 1; 422 | return err; 423 | } 424 | 425 | #ifdef TEST 426 | /* Example of how to use blast() */ 427 | #include 428 | #include 429 | 430 | #define CHUNK 16384 431 | 432 | local unsigned inf(void *how, unsigned char **buf) 433 | { 434 | static unsigned char hold[CHUNK]; 435 | 436 | *buf = hold; 437 | return fread(hold, 1, CHUNK, (FILE *)how); 438 | } 439 | 440 | local int outf(void *how, unsigned char *buf, unsigned len) 441 | { 442 | return fwrite(buf, 1, len, (FILE *)how) != len; 443 | } 444 | 445 | /* Decompress a PKWare Compression Library stream from stdin to stdout */ 446 | int main(void) 447 | { 448 | int ret; 449 | unsigned left; 450 | 451 | /* decompress to stdout */ 452 | left = 0; 453 | ret = blast(inf, stdin, outf, stdout, &left, NULL); 454 | if (ret != 0) 455 | fprintf(stderr, "blast error: %d\n", ret); 456 | 457 | /* count any leftover bytes */ 458 | while (getchar() != EOF) 459 | left++; 460 | if (left) 461 | fprintf(stderr, "blast warning: %u unused bytes of input\n", left); 462 | 463 | /* return blast() error code */ 464 | return ret; 465 | } 466 | #endif 467 | -------------------------------------------------------------------------------- /blast.h: -------------------------------------------------------------------------------- 1 | /* blast.h -- interface for blast.c 2 | Copyright (C) 2003, 2012, 2013 Mark Adler 3 | version 1.3, 24 Aug 2013 4 | 5 | This software is provided 'as-is', without any express or implied 6 | warranty. In no event will the author be held liable for any damages 7 | arising from the use of this software. 8 | 9 | Permission is granted to anyone to use this software for any purpose, 10 | including commercial applications, and to alter it and redistribute it 11 | freely, subject to the following restrictions: 12 | 13 | 1. The origin of this software must not be misrepresented; you must not 14 | claim that you wrote the original software. If you use this software 15 | in a product, an acknowledgment in the product documentation would be 16 | appreciated but is not required. 17 | 2. Altered source versions must be plainly marked as such, and must not be 18 | misrepresented as being the original software. 19 | 3. This notice may not be removed or altered from any source distribution. 20 | 21 | Mark Adler madler@alumni.caltech.edu 22 | */ 23 | 24 | 25 | /* 26 | * blast() decompresses the PKWare Data Compression Library (DCL) compressed 27 | * format. It provides the same functionality as the explode() function in 28 | * that library. (Note: PKWare overused the "implode" verb, and the format 29 | * used by their library implode() function is completely different and 30 | * incompatible with the implode compression method supported by PKZIP.) 31 | * 32 | * The binary mode for stdio functions should be used to assure that the 33 | * compressed data is not corrupted when read or written. For example: 34 | * fopen(..., "rb") and fopen(..., "wb"). 35 | */ 36 | 37 | 38 | typedef unsigned (*blast_in)(void *how, unsigned char **buf); 39 | typedef int (*blast_out)(void *how, unsigned char *buf, unsigned len); 40 | /* Definitions for input/output functions passed to blast(). See below for 41 | * what the provided functions need to do. 42 | */ 43 | 44 | 45 | int blast(blast_in infun, void *inhow, blast_out outfun, void *outhow, 46 | unsigned *left, unsigned char **in); 47 | /* Decompress input to output using the provided infun() and outfun() calls. 48 | * On success, the return value of blast() is zero. If there is an error in 49 | * the source data, i.e. it is not in the proper format, then a negative value 50 | * is returned. If there is not enough input available or there is not enough 51 | * output space, then a positive error is returned. 52 | * 53 | * The input function is invoked: len = infun(how, &buf), where buf is set by 54 | * infun() to point to the input buffer, and infun() returns the number of 55 | * available bytes there. If infun() returns zero, then blast() returns with 56 | * an input error. (blast() only asks for input if it needs it.) inhow is for 57 | * use by the application to pass an input descriptor to infun(), if desired. 58 | * 59 | * If left and in are not NULL and *left is not zero when blast() is called, 60 | * then the *left bytes are *in are consumed for input before infun() is used. 61 | * 62 | * The output function is invoked: err = outfun(how, buf, len), where the bytes 63 | * to be written are buf[0..len-1]. If err is not zero, then blast() returns 64 | * with an output error. outfun() is always called with len <= 4096. outhow 65 | * is for use by the application to pass an output descriptor to outfun(), if 66 | * desired. 67 | * 68 | * If there is any unused input, *left is set to the number of bytes that were 69 | * read and *in points to them. Otherwise *left is set to zero and *in is set 70 | * to NULL. If left or in are NULL, then they are not set. 71 | * 72 | * The return codes are: 73 | * 74 | * 2: ran out of input before completing decompression 75 | * 1: output error before completing decompression 76 | * 0: successful decompression 77 | * -1: literal flag not zero or one 78 | * -2: dictionary size not in 4..6 79 | * -3: distance is too far back 80 | * 81 | * At the bottom of blast.c is an example program that uses blast() that can be 82 | * compiled to produce a command-line decompression filter by defining TEST. 83 | */ 84 | -------------------------------------------------------------------------------- /build/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wfr/unshieldv3/4e5700fb97ba9f0338b49c1031834ce42177f265/build/.gitkeep -------------------------------------------------------------------------------- /config.h.in: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define CMAKE_PROJECT_NAME "@PROJECT_NAME@" 4 | #define CMAKE_PROJECT_VER "@PROJECT_VERSION@" 5 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | /* unshieldv3 -- extract InstallShield V3 archives. 2 | Copyright (c) 2019 Wolfgang Frisch 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #include "config.h" 18 | #include "ISArchiveV3.h" 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | using namespace std; 28 | namespace fs = std::filesystem; 29 | 30 | /********************************************************************** 31 | * Operations 32 | **********************************************************************/ 33 | void info(const ISArchiveV3& archive) { 34 | auto hdr = archive.header(); 35 | cout << "Archive: " << archive.path().string() << endl; 36 | cout << "File count: " << hdr.file_count << endl; 37 | cout << "Compressed size: " << hdr.compressed_size << endl; 38 | cout << "Uncompressed size: " << hdr.uncompressed_size << endl; 39 | 40 | if (hdr.is_multivolume) { 41 | cout << "This is a multi-volume archive: " << endl; 42 | cout << " - volume_number: " << int(hdr.volume_number) << endl; 43 | cout << " - volume_total: " << int(hdr.volume_total) << endl; 44 | cout << " - split_begin_address: " << hdr.split_begin_address << endl; 45 | cout << " - split_end_address: " << hdr.split_end_address << endl; 46 | } 47 | } 48 | 49 | void list_archive(const ISArchiveV3& archive, bool verbose = false) { 50 | size_t max_path = 0; 51 | if (verbose) { 52 | for (auto& f : archive.files()) { 53 | max_path = max(f.full_path.size(), max_path); 54 | } 55 | cout << left << setw(max_path) << "Path" << " " 56 | << right << setw(8) << "Size" << " " 57 | << "Date " 58 | << endl; 59 | cout << left << setw(max_path) << "----" << " " 60 | << right << setw(8) << "----" << " " 61 | << "----" 62 | << endl; 63 | } 64 | 65 | for (auto& f : archive.files()) { 66 | if (verbose) { 67 | std::tm tm = f.tm(); 68 | cout << left << setw(max_path) << f.full_path << " " 69 | << right << setw(8) << f.uncompressed_size << " " 70 | << std::put_time(&tm, "%Y-%m-%d %H:%M:%S") << " " 71 | << endl; 72 | } else { 73 | cout << f.full_path << endl; 74 | } 75 | } 76 | } 77 | 78 | bool extract(ISArchiveV3& archive, const fs::path& destination) { 79 | if (destination.empty()) { 80 | cerr << "Please specify a destination directory." << endl; 81 | return false; 82 | } 83 | if (!fs::exists(destination)) { 84 | cerr << "Destination directory not found: " << destination << endl; 85 | return false; 86 | } 87 | for (auto& file : archive.files()) { 88 | cout << file.full_path << endl; 89 | cout << " Compressed size: " << setw(10) << file.compressed_size << endl; 90 | auto contents = archive.decompress(file.full_path); 91 | cout << " Uncompressed size: " << setw(10) << contents.size() << endl; 92 | 93 | fs::path dest = destination / file.path(); 94 | fs::path dest_dir = dest.parent_path(); 95 | if (!fs::create_directories(dest_dir)) { 96 | if (!fs::exists(dest_dir)) { 97 | cerr << "Could not create directory: " << dest_dir << endl; 98 | return false; 99 | } 100 | } 101 | ofstream fout(dest, ios::binary | ios::out); 102 | if (fout.fail()) { 103 | cerr << dest << endl; 104 | cerr << "Could not create file: " << dest << endl; 105 | return false; 106 | } 107 | fout.write(reinterpret_cast(contents.data()), long(contents.size())); 108 | if (fout.fail()) { 109 | cerr << "Could not write to: " << dest << endl; 110 | return false; 111 | } 112 | fout.close(); 113 | } 114 | return true; 115 | } 116 | 117 | /********************************************************************** 118 | * Command-line 119 | **********************************************************************/ 120 | int cmd_help(deque subargs = {}) { 121 | cerr << "unshieldv3 version " << CMAKE_PROJECT_VER << endl; 122 | cerr << "usage: " << endl; 123 | cerr << " unshieldv3 help Produce this message" << endl; 124 | cerr << " unshieldv3 info ARCHIVE.Z Show archive metadata" << endl; 125 | cerr << " unshieldv3 list [-v] ARCHIVE.Z List ARCHIVE contents" << endl; 126 | cerr << " unshieldv3 extract ARCHIVE.Z DESTDIR Extract ARCHIVE to DESTDIR" << endl; 127 | return 1; 128 | } 129 | 130 | int cmd_info(deque subargs) { 131 | string apath; 132 | 133 | if (subargs.size() != 1) { 134 | return cmd_help(); 135 | } 136 | 137 | apath = subargs[0]; 138 | if (!fs::exists(apath)) { 139 | cerr << "Archive not found: " << apath << endl; 140 | return 1; 141 | } 142 | 143 | ISArchiveV3 archive(apath); 144 | info(archive); 145 | return 0; 146 | } 147 | 148 | int cmd_list(deque subargs) { 149 | bool verbose = false; 150 | string apath; 151 | 152 | if (subargs.size() == 0) { 153 | return cmd_help(); 154 | } 155 | if (subargs[0] == "-v") { 156 | verbose = true; 157 | subargs.pop_front(); 158 | } 159 | if (subargs.size() == 1) { 160 | apath = subargs[0]; 161 | } else { 162 | return cmd_help(); 163 | } 164 | if (!fs::exists(apath)) { 165 | cerr << "Archive not found: " << apath << endl; 166 | return 1; 167 | } 168 | ISArchiveV3 archive(apath); 169 | list_archive(archive, verbose); 170 | return 0; 171 | } 172 | 173 | int cmd_extract(deque subargs) { 174 | fs::path apath; 175 | fs::path destdir; 176 | 177 | if (subargs.size() != 2) { 178 | return cmd_help(); 179 | } 180 | 181 | apath = subargs[0]; 182 | destdir = subargs[1]; 183 | if (!fs::exists(apath)) { 184 | cerr << "Archive not found: " << apath << endl; 185 | return 1; 186 | } 187 | ISArchiveV3 archive(apath); 188 | return extract(archive, destdir) ? 0 : 1; 189 | } 190 | 191 | int main(int argc, char** argv) { 192 | vector args; 193 | for (int i = 0; i < argc; i++) { 194 | args.push_back(argv[i]); 195 | } 196 | if (args.size() <= 1) { 197 | return cmd_help(); 198 | } 199 | 200 | deque subargs = deque {args.begin() + 2, args.end()}; 201 | 202 | if (args[1] == "help") { 203 | return cmd_help(subargs); 204 | } 205 | 206 | if (args[1] == "info") { 207 | return cmd_info(subargs); 208 | } 209 | 210 | if (args[1] == "list") { 211 | return cmd_list(subargs); 212 | } 213 | 214 | if (args[1] == "extract") { 215 | return cmd_extract(subargs); 216 | } 217 | 218 | cmd_help(); 219 | return 1; 220 | } 221 | -------------------------------------------------------------------------------- /test-data/TestArchive1-FastCompression.Z: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wfr/unshieldv3/4e5700fb97ba9f0338b49c1031834ce42177f265/test-data/TestArchive1-FastCompression.Z -------------------------------------------------------------------------------- /test-data/TestArchive1-HighCompression.Z: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wfr/unshieldv3/4e5700fb97ba9f0338b49c1031834ce42177f265/test-data/TestArchive1-HighCompression.Z -------------------------------------------------------------------------------- /test-data/TestArchive1-MediumCompression.Z: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wfr/unshieldv3/4e5700fb97ba9f0338b49c1031834ce42177f265/test-data/TestArchive1-MediumCompression.Z -------------------------------------------------------------------------------- /test-data/TestArchive1-NoCompression.Z: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wfr/unshieldv3/4e5700fb97ba9f0338b49c1031834ce42177f265/test-data/TestArchive1-NoCompression.Z --------------------------------------------------------------------------------