├── release_version.txt ├── src ├── CMakeLists.txt ├── global.h ├── gui │ └── CMakeLists.txt └── cli │ ├── CMakeLists.txt │ └── main_cli.cpp ├── changelog.txt ├── CMakeLists.txt ├── LICENSE.md ├── README.md └── .gitmodules /release_version.txt: -------------------------------------------------------------------------------- 1 | 0.1.0 2 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Source directory CMakeLists.txt 2 | # add_subdirectory(gui) # GUI not implemented yet 3 | add_subdirectory(cli) 4 | 5 | -------------------------------------------------------------------------------- /src/global.h: -------------------------------------------------------------------------------- 1 | #ifndef GLOBAL_H 2 | #define GLOBAL_H 3 | 4 | #include 5 | 6 | // Application metadata 7 | #define APP_NAME "XFileUnpacker" 8 | #define APP_VERSION "0.1.0" 9 | #define APP_ORGANIZATION "XFileUnpacker" 10 | #define APP_DESCRIPTION "File unpacking utility" 11 | 12 | namespace Global { 13 | constexpr const char* VERSION = APP_VERSION; 14 | constexpr const char* NAME = APP_NAME; 15 | constexpr const char* DESCRIPTION = APP_DESCRIPTION; 16 | } 17 | 18 | #endif // GLOBAL_H 19 | -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | Version 0.1.0 (2025-10-20) 5 | -------------------------- 6 | - Initial project setup 7 | - Generic CMake build system for Qt5/Qt6 8 | - Dual-build support (GUI and CLI applications) 9 | - Basic project structure with documentation framework 10 | - MIT License 11 | - Added global headers and configuration 12 | 13 | Known Issues: 14 | - None yet 15 | 16 | Future Improvements: 17 | - Implementation of core unpacking functionality 18 | - Enhanced error handling 19 | - More comprehensive logging 20 | - Unit tests 21 | -------------------------------------------------------------------------------- /src/gui/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # GUI Application CMakeLists.txt 2 | set(TARGET_NAME XFileUnpacker-GUI) 3 | 4 | set(SOURCES 5 | main_gui.cpp 6 | mainwindow.cpp 7 | ) 8 | 9 | set(HEADERS 10 | mainwindow.h 11 | ) 12 | 13 | set(FORMS 14 | mainwindow.ui 15 | ) 16 | 17 | set(RESOURCES 18 | resources.qrc 19 | ) 20 | 21 | add_executable(${TARGET_NAME} 22 | ${SOURCES} 23 | ${HEADERS} 24 | ${FORMS} 25 | ${RESOURCES} 26 | ) 27 | 28 | if(Qt6_FOUND) 29 | target_link_libraries(${TARGET_NAME} 30 | Qt6::Core 31 | Qt6::Gui 32 | Qt6::Widgets 33 | ) 34 | else() 35 | target_link_libraries(${TARGET_NAME} 36 | Qt5::Core 37 | Qt5::Gui 38 | Qt5::Widgets 39 | ) 40 | endif() 41 | 42 | target_include_directories(${TARGET_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/..) 43 | 44 | # Installation 45 | install(TARGETS ${TARGET_NAME} DESTINATION bin) 46 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | 3 | project(XFileUnpacker 4 | VERSION 0.1.0 5 | DESCRIPTION "Generic Qt5/Qt6 project with Console/GUI versions" 6 | LANGUAGES CXX 7 | ) 8 | 9 | # Set CMake policy 10 | set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) 11 | 12 | # Find Qt5 or Qt6 13 | find_package(Qt6 COMPONENTS Core Gui Widgets QUIET) 14 | if(NOT Qt6_FOUND) 15 | find_package(Qt5 COMPONENTS Core Gui Widgets REQUIRED) 16 | endif() 17 | 18 | # Set C++ standard 19 | set(CMAKE_CXX_STANDARD 17) 20 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 21 | 22 | # Enable automoc, autorcc, autouic 23 | set(CMAKE_AUTOMOC ON) 24 | set(CMAKE_AUTORCC ON) 25 | set(CMAKE_AUTOUIC ON) 26 | 27 | # Add subdirectories 28 | add_subdirectory(src) 29 | 30 | # Enable testing 31 | enable_testing() 32 | add_subdirectory(tests) 33 | 34 | # Installation targets 35 | install(FILES README.md DESTINATION .) 36 | install(FILES LICENSE.txt DESTINATION .) 37 | install(FILES changelog.txt DESTINATION .) 38 | install(DIRECTORY doc/ DESTINATION doc) 39 | install(DIRECTORY res/ DESTINATION res) 40 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 (C) hors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XFileUnpacker 2 | 3 | A generic file unpacking utility with both Console (CLI) and Graphical User Interface (GUI) versions built with Qt5/Qt6. 4 | 5 | ## Features 6 | 7 | - **Console Application**: Command-line interface for batch processing 8 | - **GUI Application**: User-friendly graphical interface 9 | - **Qt5/Qt6 Support**: Automatically detects and uses available Qt version 10 | - **Cross-platform**: Works on Windows, macOS, and Linux 11 | 12 | ## Version 13 | 14 | **0.1.0** - Initial release 15 | 16 | ## Requirements 17 | 18 | - CMake 3.16 or higher 19 | - Qt5 (Core, Gui, Widgets) or Qt6 (Core, Gui, Widgets) 20 | - C++17 compatible compiler 21 | 22 | ## Building 23 | 24 | ### Clone and Build 25 | 26 | ```bash 27 | git clone https://github.com/horsicq/XFileUnpacker.git --recursive 28 | cd XFileUnpacker 29 | mkdir build 30 | cd build 31 | cmake .. 32 | cmake --build . 33 | ``` 34 | 35 | ### Installation 36 | 37 | ```bash 38 | cmake --install . 39 | ``` 40 | 41 | ## Usage 42 | 43 | ### GUI Application 44 | 45 | ```bash 46 | XFileUnpacker-GUI 47 | ``` 48 | 49 | ### CLI Application 50 | 51 | ```bash 52 | XFileUnpacker-CLI [options] 53 | ``` 54 | 55 | ## Project Structure 56 | 57 | ``` 58 | XFileUnpacker/ 59 | ├── src/ 60 | │ ├── gui/ # GUI application 61 | │ ├── cli/ # CLI application 62 | │ ├── global.h # Global definitions 63 | │ └── CMakeLists.txt 64 | ├── test/ # Test files 65 | ├── doc/ # Documentation 66 | ├── res/ # Resources (icons, images, etc.) 67 | ├── dep/ # Dependencies 68 | ├── tools/ # Build tools and scripts 69 | ├── CMakeLists.txt 70 | ├── README.md 71 | ├── LICENSE.txt 72 | ├── changelog.txt 73 | └── release_version.txt 74 | ``` 75 | 76 | ## License 77 | 78 | MIT License - See LICENSE.txt for details 79 | 80 | ## Contributing 81 | 82 | Contributions are welcome! Please feel free to submit pull requests. 83 | 84 | ## Support 85 | 86 | For issues and feature requests, please use the issue tracker. 87 | -------------------------------------------------------------------------------- /src/cli/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # CLI Application CMakeLists.txt 2 | set(TARGET_NAME xfuc) 3 | 4 | add_definitions(-DUSE_DEX) 5 | add_definitions(-DUSE_PDF) 6 | add_definitions(-DUSE_ARCHIVE) 7 | add_definitions(-DXSIMD_ENABLE) 8 | 9 | if(WIN32) 10 | add_definitions(-DNOMINMAX) 11 | endif() 12 | 13 | # Add 3rd party libraries 14 | add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../../dep/XArchive/3rdparty/bzip2 ${CMAKE_BINARY_DIR}/3rdparty/bzip2) 15 | add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../../dep/XArchive/3rdparty/lzma ${CMAKE_BINARY_DIR}/3rdparty/lzma) 16 | add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../../dep/XArchive/3rdparty/zlib ${CMAKE_BINARY_DIR}/3rdparty/zlib) 17 | add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../../dep/XArchive/3rdparty/ppmd ${CMAKE_BINARY_DIR}/3rdparty/ppmd) 18 | add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../../dep/Formats/xsimd ${CMAKE_BINARY_DIR}/xsimd) 19 | 20 | # Include XFormats and XArchive 21 | include(${CMAKE_CURRENT_LIST_DIR}/../../dep/Formats/xformats.cmake) 22 | include(${CMAKE_CURRENT_LIST_DIR}/../../dep/XArchive/xarchives.cmake) 23 | include(${CMAKE_CURRENT_LIST_DIR}/../../dep/Controls/xmodel_archiverecords.cmake) 24 | 25 | set(SOURCES 26 | main_cli.cpp 27 | ${XFORMATS_SOURCES} 28 | ${XARCHIVES_SOURCES} 29 | ${XMODEL_ARCHIVERECORDS_SOURCES} 30 | ) 31 | 32 | set(HEADERS 33 | ) 34 | 35 | add_executable(${TARGET_NAME} 36 | ${SOURCES} 37 | ${HEADERS} 38 | ) 39 | 40 | # Link Qt5 41 | if(Qt6_FOUND) 42 | target_link_libraries(${TARGET_NAME} PRIVATE Qt6::Core) 43 | else() 44 | target_link_libraries(${TARGET_NAME} PRIVATE Qt5::Core) 45 | endif() 46 | 47 | target_link_libraries(${TARGET_NAME} PRIVATE bzip2) 48 | target_link_libraries(${TARGET_NAME} PRIVATE lzma) 49 | target_link_libraries(${TARGET_NAME} PRIVATE zlib) 50 | target_link_libraries(${TARGET_NAME} PRIVATE ppmd) 51 | target_link_libraries(${TARGET_NAME} PRIVATE xsimd) 52 | 53 | if(WIN32) 54 | target_link_libraries(${TARGET_NAME} PRIVATE Wintrust) 55 | target_link_libraries(${TARGET_NAME} PRIVATE Crypt32) 56 | endif() 57 | 58 | target_include_directories(${TARGET_NAME} PRIVATE 59 | ${CMAKE_CURRENT_SOURCE_DIR}/.. 60 | ${CMAKE_CURRENT_LIST_DIR}/../../dep/Controls 61 | ) 62 | 63 | # Installation 64 | install(TARGETS ${TARGET_NAME} DESTINATION bin) 65 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "dep/Controls"] 2 | path = dep/Controls 3 | url = https://github.com/horsicq/Controls 4 | [submodule "dep/FormatDialogs"] 5 | path = dep/FormatDialogs 6 | url = https://github.com/horsicq/FormatDialogs 7 | [submodule "dep/FormatWidgets"] 8 | path = dep/FormatWidgets 9 | url = https://github.com/horsicq/FormatWidgets 10 | [submodule "dep/Formats"] 11 | path = dep/Formats 12 | url = https://github.com/horsicq/Formats 13 | [submodule "dep/XAboutWidget"] 14 | path = dep/XAboutWidget 15 | url = https://github.com/horsicq/XAboutWidget 16 | [submodule "dep/XArchive"] 17 | path = dep/XArchive 18 | url = https://github.com/horsicq/XArchive 19 | [submodule "dep/XCapstone"] 20 | path = dep/XCapstone 21 | url = https://github.com/horsicq/XCapstone 22 | [submodule "dep/XDataConvertorWidget"] 23 | path = dep/XDataConvertorWidget 24 | url = https://github.com/horsicq/XDataConvertorWidget 25 | [submodule "dep/XDEX"] 26 | path = dep/XDEX 27 | url = https://github.com/horsicq/XDEX 28 | [submodule "dep/XDecompiler"] 29 | path = dep/XDecompiler 30 | url = https://github.com/horsicq/XDecompiler 31 | [submodule "dep/XDemangle"] 32 | path = dep/XDemangle 33 | url = https://github.com/horsicq/XDemangle 34 | [submodule "dep/XDemangleWidget"] 35 | path = dep/XDemangleWidget 36 | url = https://github.com/horsicq/XDemangleWidget 37 | [submodule "dep/XDisasmCore"] 38 | path = dep/XDisasmCore 39 | url = https://github.com/horsicq/XDisasmCore 40 | [submodule "dep/XDisasmView"] 41 | path = dep/XDisasmView 42 | url = https://github.com/horsicq/XDisasmView 43 | [submodule "dep/XEntropyWidget"] 44 | path = dep/XEntropyWidget 45 | url = https://github.com/horsicq/XEntropyWidget 46 | [submodule "dep/XExtractor"] 47 | path = dep/XExtractor 48 | url = https://github.com/horsicq/XExtractor 49 | [submodule "dep/XExtractorWidget"] 50 | path = dep/XExtractorWidget 51 | url = https://github.com/horsicq/XExtractorWidget 52 | [submodule "dep/XFileInfo"] 53 | path = dep/XFileInfo 54 | url = https://github.com/horsicq/XFileInfo 55 | [submodule "dep/XGithub"] 56 | path = dep/XGithub 57 | url = https://github.com/horsicq/XGithub 58 | [submodule "dep/XHashWidget"] 59 | path = dep/XHashWidget 60 | url = https://github.com/horsicq/XHashWidget 61 | [submodule "dep/XHexEdit"] 62 | path = dep/XHexEdit 63 | url = https://github.com/horsicq/XHexEdit 64 | [submodule "dep/XHexView"] 65 | path = dep/XHexView 66 | url = https://github.com/horsicq/XHexView 67 | [submodule "dep/XInfoDB"] 68 | path = dep/XInfoDB 69 | url = https://github.com/horsicq/XInfoDB 70 | [submodule "dep/XMemoryMapWidget"] 71 | path = dep/XMemoryMapWidget 72 | url = https://github.com/horsicq/XMemoryMapWidget 73 | [submodule "dep/XOptions"] 74 | path = dep/XOptions 75 | url = https://github.com/horsicq/XOptions 76 | [submodule "dep/XPDF"] 77 | path = dep/XPDF 78 | url = https://github.com/horsicq/XPDF 79 | [submodule "dep/XQwt"] 80 | path = dep/XQwt 81 | url = https://github.com/horsicq/XQwt 82 | [submodule "dep/XRegionsWidget"] 83 | path = dep/XRegionsWidget 84 | url = https://github.com/horsicq/XRegionsWidget 85 | [submodule "dep/XScanEngine"] 86 | path = dep/XScanEngine 87 | url = https://github.com/horsicq/XScanEngine 88 | [submodule "dep/XShortcuts"] 89 | path = dep/XShortcuts 90 | url = https://github.com/horsicq/XShortcuts 91 | [submodule "dep/XStaticUnpacker"] 92 | path = dep/XStaticUnpacker 93 | url = https://github.com/horsicq/XStaticUnpacker 94 | [submodule "dep/XStyles"] 95 | path = dep/XStyles 96 | url = https://github.com/horsicq/XStyles 97 | [submodule "dep/XSymbolsWidget"] 98 | path = dep/XSymbolsWidget 99 | url = https://github.com/horsicq/XSymbolsWidget 100 | [submodule "dep/XTranslation"] 101 | path = dep/XTranslation 102 | url = https://github.com/horsicq/XTranslation 103 | [submodule "dep/XUpdate"] 104 | path = dep/XUpdate 105 | url = https://github.com/horsicq/XUpdate 106 | [submodule "dep/XVisualizationWidget"] 107 | path = dep/XVisualizationWidget 108 | url = https://github.com/horsicq/XVisualizationWidget 109 | [submodule "dep/XYara"] 110 | path = dep/XYara 111 | url = https://github.com/horsicq/XYara 112 | [submodule "dep/build_tools"] 113 | path = dep/build_tools 114 | url = https://github.com/horsicq/build_tools 115 | [submodule "dep/hex_templates"] 116 | path = dep/hex_templates 117 | url = https://github.com/horsicq/hex_templates 118 | [submodule "dep/nfd_widget"] 119 | path = dep/nfd_widget 120 | url = https://github.com/horsicq/nfd_widget 121 | [submodule "dep/signatures"] 122 | path = dep/signatures 123 | url = https://github.com/horsicq/signatures 124 | [submodule "dep/SpecAbstract"] 125 | path = dep/SpecAbstract 126 | url = https://github.com/horsicq/SpecAbstract 127 | -------------------------------------------------------------------------------- /src/cli/main_cli.cpp: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2025 hors 2 | * 3 | * Permission is hereby granted, free of charge, to any person obtaining a copy 4 | * of this software and associated documentation files (the "Software"), to deal 5 | * in the Software without restriction, including without limitation the rights 6 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | * copies of the Software, and to permit persons to whom the Software is 8 | * furnished to do so, subject to the following conditions: 9 | * 10 | * The above copyright notice and this permission notice shall be included in 11 | * all copies or substantial portions of the Software. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | * SOFTWARE. 20 | */ 21 | 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include "../global.h" 32 | #include "xformats.h" 33 | #include "xarchives.h" 34 | #include "xoptions.h" 35 | #include "xmodel_archiverecords.h" 36 | 37 | void printError(const QString &sMessage) 38 | { 39 | QTextStream stream(stderr); 40 | stream << "Error: " << sMessage << Qt::endl; 41 | } 42 | 43 | void printInfo(const QString &sMessage) 44 | { 45 | QTextStream stream(stdout); 46 | stream << sMessage << Qt::endl; 47 | } 48 | 49 | void progressCallback(void *pUserData, XBinary::PDSTRUCT *pPdStruct) 50 | { 51 | Q_UNUSED(pUserData) 52 | 53 | if (pPdStruct) { 54 | qint64 nTotalCurrent = 0; 55 | qint64 nTotalAll = 0; 56 | QStringList listStatuses; 57 | 58 | // Sum all valid records 59 | for (qint32 i = 0; i < XBinary::N_NUMBER_PDRECORDS; i++) { 60 | if (pPdStruct->_pdRecord[i].bIsValid) { 61 | nTotalCurrent += pPdStruct->_pdRecord[i].nCurrent; 62 | nTotalAll += pPdStruct->_pdRecord[i].nTotal; 63 | 64 | QString sStatus = pPdStruct->_pdRecord[i].sStatus; 65 | if (!sStatus.isEmpty()) { 66 | listStatuses.append(sStatus); 67 | } 68 | } 69 | } 70 | 71 | if (nTotalAll > 0) { 72 | qint32 nPercent = (nTotalCurrent * 100) / nTotalAll; 73 | QString sStatus = listStatuses.join("/"); 74 | 75 | QTextStream stream(stdout); 76 | stream << QString("\rProgress: %1% (%2/%3)") 77 | .arg(nPercent) 78 | .arg(nTotalCurrent) 79 | .arg(nTotalAll); 80 | 81 | if (!sStatus.isEmpty()) { 82 | stream << " - " << sStatus; 83 | } 84 | 85 | stream.flush(); 86 | } 87 | } 88 | } 89 | 90 | void testProgressCallback() 91 | { 92 | printInfo("Testing progress callback..."); 93 | 94 | XBinary::PDSTRUCT pdStruct = XBinary::createPdStruct(); 95 | pdStruct.pCallback = progressCallback; 96 | pdStruct.pCallbackUserData = nullptr; 97 | pdStruct.nLastCallbackTime = 0; 98 | 99 | // Initialize progress for record 0 100 | XBinary::setPdStructInit(&pdStruct, 0, 100); 101 | pdStruct._pdRecord[0].sStatus = "Processing files"; 102 | 103 | // Simulate progress updates 104 | for (qint32 i = 0; i <= 100; i += 5) { 105 | pdStruct._pdRecord[0].nCurrent = i; 106 | progressCallback(nullptr, &pdStruct); 107 | 108 | // Sleep for a short time to make progress visible 109 | QThread::msleep(100); 110 | } 111 | 112 | QTextStream(stdout) << "\n"; 113 | printInfo("Progress callback test completed"); 114 | } 115 | 116 | int main(int argc, char *argv[]) 117 | { 118 | QCoreApplication app(argc, argv); 119 | QCoreApplication::setApplicationName(Global::NAME); 120 | QCoreApplication::setApplicationVersion(Global::VERSION); 121 | QCoreApplication::setOrganizationName(APP_ORGANIZATION); 122 | 123 | QCommandLineParser parser; 124 | parser.setApplicationDescription(Global::DESCRIPTION); 125 | parser.addHelpOption(); 126 | parser.addVersionOption(); 127 | 128 | // Add command line options 129 | QCommandLineOption outputOption(QStringList() << "o" << "output", 130 | "Output directory for extracted files", 131 | "directory"); 132 | parser.addOption(outputOption); 133 | 134 | QCommandLineOption extractOption(QStringList() << "x" << "extract", 135 | "Extract/unpack archive (default action)"); 136 | parser.addOption(extractOption); 137 | 138 | QCommandLineOption listOption(QStringList() << "l" << "list", 139 | "List archive contents without extracting"); 140 | parser.addOption(listOption); 141 | 142 | QCommandLineOption testOption(QStringList() << "t" << "test", 143 | "Test archive integrity by extracting to temporary location"); 144 | parser.addOption(testOption); 145 | 146 | QCommandLineOption testProgressOption(QStringList() << "test-progress", 147 | "Test progress callback display"); 148 | parser.addOption(testProgressOption); 149 | 150 | // Add positional argument for input file 151 | parser.addPositionalArgument("file", "File to unpack"); 152 | 153 | // Process command line arguments 154 | parser.process(app); 155 | 156 | // Check for test progress option first (doesn't need a file) 157 | if (parser.isSet(testProgressOption)) { 158 | testProgressCallback(); 159 | return 0; 160 | } 161 | 162 | const QStringList listArgs = parser.positionalArguments(); 163 | if (listArgs.isEmpty()) { 164 | printError("No input file specified"); 165 | parser.showHelp(1); 166 | return 1; 167 | } 168 | 169 | QString sInputFile = listArgs.first(); 170 | QFileInfo fileInfo(sInputFile); 171 | 172 | if (!fileInfo.exists()) { 173 | printError(QString("File not found: %1").arg(sInputFile)); 174 | return 1; 175 | } 176 | 177 | if (!fileInfo.isFile()) { 178 | printError(QString("Not a file: %1").arg(sInputFile)); 179 | return 1; 180 | } 181 | 182 | bool bListOnly = parser.isSet(listOption); 183 | bool bTest = parser.isSet(testOption); 184 | bool bExtract = parser.isSet(extractOption); 185 | QString sOutputDir = parser.value(outputOption); 186 | 187 | // Default action is extract if no action specified 188 | if (!bListOnly && !bTest && !bExtract) { 189 | bExtract = true; 190 | } 191 | 192 | printInfo(QString("Processing file: %1").arg(fileInfo.absoluteFilePath())); 193 | printInfo(QString("File size: %1 bytes").arg(fileInfo.size())); 194 | 195 | QFile file(sInputFile); 196 | if (!file.open(QIODevice::ReadOnly)) { 197 | printError(QString("Cannot open file: %1").arg(sInputFile)); 198 | return 1; 199 | } 200 | 201 | // Detect file type 202 | QSet stFileTypes = XFormats::getFileTypes(&file, true); 203 | XBinary::FT fileType = XBinary::_getPrefFileType(&stFileTypes); 204 | 205 | QString sFileTypeString = XBinary::fileTypeIdToString(fileType); 206 | 207 | printInfo(QString("File type: %1").arg(sFileTypeString)); 208 | 209 | // Get binary object for validation 210 | XBinary *pBinary = XFormats::getClass(fileType, &file); 211 | 212 | if (!pBinary) { 213 | printError(QString("Not an archive or unsupported format: %1").arg(sFileTypeString)); 214 | file.close(); 215 | return 1; 216 | } 217 | 218 | // Get archive records 219 | XBinary::PDSTRUCT pdStruct = XBinary::createPdStruct(); 220 | pdStruct.pCallback = progressCallback; 221 | pdStruct.pCallbackUserData = nullptr; 222 | pdStruct.nLastCallbackTime = 0; 223 | 224 | QList listRecords = XFormats::getArchiveRecords(fileType, &file, -1, false, -1, &pdStruct); 225 | 226 | QTextStream(stdout) << "\n"; // Clear progress line 227 | printInfo(QString("Number of records: %1").arg(listRecords.count())); 228 | 229 | if (listRecords.isEmpty()) { 230 | printError("Archive contains no records"); 231 | delete pBinary; 232 | file.close(); 233 | return 1; 234 | } 235 | 236 | if (bListOnly) { 237 | XModel_ArchiveRecords model(pBinary->getAvailableFPARTProperties(), &listRecords); 238 | XOptions::printModel(&model); 239 | } else if (bTest) { 240 | printInfo("Testing archive integrity..."); 241 | 242 | QString sTempDir = QDir::tempPath() + QDir::separator() + "xfileunpacker_test_" + QString::number(QDateTime::currentMSecsSinceEpoch()); 243 | QDir().mkpath(sTempDir); 244 | 245 | printInfo(QString("Extracting to temporary location: %1").arg(sTempDir)); 246 | 247 | XFormats xformats; 248 | bool bTestResult = xformats.unpackDeviceToFolder(fileType, &file, sTempDir, &pdStruct); 249 | 250 | qint32 nExtractedFiles = 0; 251 | if (bTestResult) { 252 | QDir testDir(sTempDir); 253 | QFileInfoList listFiles = testDir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot, QDir::Name); 254 | nExtractedFiles = listFiles.count(); 255 | } 256 | 257 | QDir(sTempDir).removeRecursively(); 258 | 259 | if (bTestResult && nExtractedFiles > 0) { 260 | printInfo(QString("Test PASSED: Successfully extracted %1 file(s)").arg(nExtractedFiles)); 261 | } else { 262 | printError("Test FAILED: Could not extract archive"); 263 | delete pBinary; 264 | file.close(); 265 | return 1; 266 | } 267 | } else if (bExtract) { 268 | if (sOutputDir.isEmpty()) { 269 | sOutputDir = fileInfo.absolutePath(); 270 | } 271 | 272 | printInfo(QString("Extracting to: %1").arg(sOutputDir)); 273 | 274 | QString sExtractPath = sOutputDir + QDir::separator() + fileInfo.completeBaseName(); 275 | QDir().mkpath(sExtractPath); 276 | 277 | printInfo(QString("Unpacking archive...")); 278 | 279 | XFormats xformats; 280 | bool bUnpackResult = xformats.unpackDeviceToFolder(fileType, &file, sExtractPath, &pdStruct); 281 | 282 | if (bUnpackResult) { 283 | printInfo(QString("\nExtracted %1 file(s) successfully").arg(listRecords.count())); 284 | } else { 285 | printError("Failed to extract archive"); 286 | delete pBinary; 287 | file.close(); 288 | return 1; 289 | } 290 | } 291 | 292 | delete pBinary; 293 | file.close(); 294 | 295 | printInfo("Operation completed successfully"); 296 | 297 | return 0; 298 | } 299 | --------------------------------------------------------------------------------