├── .gitignore ├── CMakeLists.txt ├── src ├── target_conflict.cpp ├── target_conflict.h └── main.cpp ├── README_zh.md ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | CLAUDE.md 2 | 3 | .claude/ 4 | 5 | .serena/ 6 | 7 | build/ -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | project(hitpag) 3 | 4 | # 设置C++标准 5 | set(CMAKE_CXX_STANDARD 17) 6 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 7 | 8 | # 添加可执行文件 9 | add_executable(hitpag 10 | src/main.cpp 11 | src/target_conflict.cpp 12 | ) 13 | 14 | # 链接文件系统库 15 | find_package(Threads REQUIRED) 16 | target_link_libraries(hitpag PRIVATE stdc++fs Threads::Threads) 17 | 18 | # 安装目标 19 | install(TARGETS hitpag DESTINATION bin) 20 | -------------------------------------------------------------------------------- /src/target_conflict.cpp: -------------------------------------------------------------------------------- 1 | #include "target_conflict.h" 2 | 3 | #include 4 | #include 5 | 6 | namespace target_conflict { 7 | 8 | namespace { 9 | char to_lower(char ch) { 10 | return static_cast(std::tolower(static_cast(ch))); 11 | } 12 | } // namespace 13 | 14 | Action prompt_action( 15 | const std::function& output_fn, 16 | const std::function& input_fn, 17 | const std::string& header, 18 | const std::string& options_line, 19 | const std::string& prompt_line, 20 | const std::string& invalid_choice_line) { 21 | 22 | output_fn(header + "\n"); 23 | output_fn(options_line + "\n"); 24 | 25 | while (true) { 26 | output_fn(prompt_line); 27 | std::string choice = input_fn(); 28 | if (!choice.empty()) { 29 | char ch = to_lower(choice.front()); 30 | if (ch == 'o') return Action::Overwrite; 31 | if (ch == 'c') return Action::Cancel; 32 | if (ch == 'r') return Action::Rename; 33 | } 34 | output_fn(invalid_choice_line + "\n"); 35 | } 36 | } 37 | 38 | std::string prompt_new_path( 39 | const std::function& output_fn, 40 | const std::function& input_fn, 41 | const std::string& prompt_line, 42 | const std::string& default_value) { 43 | output_fn(prompt_line); 44 | std::string value = input_fn(); 45 | if (value.empty()) { 46 | return default_value; 47 | } 48 | return value; 49 | } 50 | 51 | } // namespace target_conflict 52 | -------------------------------------------------------------------------------- /src/target_conflict.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace target_conflict { 7 | 8 | /** 9 | * Defines the available actions when a target path already exists. 10 | */ 11 | enum class Action { 12 | Overwrite, 13 | Cancel, 14 | Rename 15 | }; 16 | 17 | /** 18 | * Presents a choice to the user and parses the response. 19 | * 20 | * The function delegates input/output handling to the provided callbacks so it can be used 21 | * in both interactive mode and non-interactive command-line mode. 22 | * 23 | * @param output_fn Function used to display messages to the user. 24 | * @param input_fn Function used to collect user input (should return a trimmed string). 25 | * @param header Message shown before the options list (e.g., informing that the target exists). 26 | * @param options_line Describes the available options (e.g., "[O]verwrite / [C]ancel / [R]ename"). 27 | * @param prompt_line Prompt displayed when waiting for the user's choice. 28 | * @param invalid_choice_line Message displayed when the input cannot be parsed. 29 | * @return The parsed user action. 30 | */ 31 | Action prompt_action( 32 | const std::function& output_fn, 33 | const std::function& input_fn, 34 | const std::string& header, 35 | const std::string& options_line, 36 | const std::string& prompt_line, 37 | const std::string& invalid_choice_line); 38 | 39 | /** 40 | * Prompts the user for a new target path when they choose to rename. 41 | * 42 | * @param output_fn Function used to display the prompt. 43 | * @param input_fn Function used to collect user input (should return a trimmed string). 44 | * @param prompt_line Prompt displayed to request the new path. 45 | * @return The user supplied path. 46 | */ 47 | std::string prompt_new_path( 48 | const std::function& output_fn, 49 | const std::function& input_fn, 50 | const std::string& prompt_line, 51 | const std::string& default_value); 52 | 53 | } // namespace target_conflict 54 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | # hitpag - 智能压缩工具 2 | 3 | **智能、强大、易用的命令行压缩工具** 4 | 5 | [![GitHub](https://img.shields.io/badge/GitHub-Hitmux/hitpag-blue)](https://github.com/Hitmux/hitpag) 6 | [![Website](https://img.shields.io/badge/Website-hitmux.org-green)](https://hitmux.org) 7 | 8 | [English](README.md) | [简体中文](README_zh.md) 9 | 10 | --- 11 | 12 | ## 为什么选择 hitpag? 13 | 14 | - **🧠 智能识别** - 通过文件头自动检测格式,不依赖扩展名 15 | - **⚡ 一条命令** - 无需记忆不同格式对应的不同工具 16 | - **📦 全格式支持** - tar, gzip, bzip2, xz, zip, 7z, rar, lz4, zstd, xar 17 | - **🔐 密码保护** - 支持 zip 和 7z 格式加密 18 | - **🚀 多线程** - 并行压缩,性能更强 19 | 20 | --- 21 | 22 | ## 快速开始 23 | 24 | ### 安装 25 | ```bash 26 | # Ubuntu/Debian 27 | sudo apt install -y tar gzip bzip2 xz-utils zip unzip p7zip-full lz4 zstd 28 | 29 | # 构建 30 | git clone https://github.com/Hitmux/hitpag.git 31 | cd hitpag && mkdir build && cd build && cmake .. && make 32 | sudo make install # 可选 33 | ``` 34 | 35 | ### 基本用法 36 | ```bash 37 | # 解压 - 直接指向压缩包 38 | hitpag archive.tar.gz ./output/ 39 | hitpag backup.zip ./extracted/ 40 | hitpag data.7z ./data/ 41 | 42 | # 压缩 - 指定源和目标 43 | hitpag ./my_folder/ backup.zip 44 | hitpag ./documents/ archive.tar.gz 45 | 46 | # 带密码 47 | hitpag -pMySecret secure.7z ./sensitive/ 48 | hitpag -p encrypted.zip ./output/ # 交互式输入密码 49 | ``` 50 | 51 | 就这么简单!hitpag 会自动处理其余的事情。 52 | 53 | --- 54 | 55 | ## 高级用法 56 | 57 | ### 性能选项 58 | ```bash 59 | # 多线程压缩并显示性能统计 60 | hitpag -l9 -t8 --benchmark data.tar.xz ./large_files/ 61 | 62 | # 超快压缩 (LZ4) 63 | hitpag --format=lz4 temp.lz4 ./temp_data/ 64 | 65 | # 高效压缩 (Zstandard) 66 | hitpag --format=zstd archive.zstd ./documents/ 67 | ``` 68 | 69 | ### 文件过滤 70 | ```bash 71 | # 只包含特定文件 72 | hitpag --include='*.cpp' --include='*.h' code.7z ./project/ 73 | 74 | # 排除文件 75 | hitpag --exclude='*.tmp' --exclude='node_modules/*' clean.tar.gz ./project/ 76 | ``` 77 | 78 | ### 其他选项 79 | ```bash 80 | hitpag -i # 交互模式 81 | hitpag --verbose archive.7z # 详细输出 82 | hitpag --verify data.tar.gz # 压缩后验证 83 | hitpag --format=rar unknown # 强制指定格式 84 | ``` 85 | 86 | --- 87 | 88 | ## 支持的格式 89 | 90 | | 格式 | 压缩 | 解压 | 密码 | 说明 | 91 | |------|------|------|------|------| 92 | | tar, tar.gz, tar.bz2, tar.xz | ✅ | ✅ | ❌ | 经典 Unix 格式 | 93 | | zip | ✅ | ✅ | ✅ | 支持分卷压缩包 (.z01, .z02, ...) | 94 | | 7z | ✅ | ✅ | ✅ | 最高压缩率 | 95 | | rar | ❌ | ✅ | ✅ | 仅支持解压 | 96 | | lz4 | ✅ | ✅ | ❌ | 超快速度 | 97 | | zstd | ✅ | ✅ | ❌ | 速度/压缩率最佳平衡 | 98 | | xar | ✅ | ✅ | ❌ | macOS 原生格式 | 99 | 100 | --- 101 | 102 | ## 命令参考 103 | 104 | | 选项 | 说明 | 105 | |------|------| 106 | | `-i` | 交互模式 | 107 | | `-p[password]` | 密码(不提供则交互输入) | 108 | | `-l[1-9]` | 压缩级别 | 109 | | `-t[count]` | 线程数 | 110 | | `--format=TYPE` | 强制指定格式 | 111 | | `--verbose` | 详细输出 | 112 | | `--benchmark` | 性能统计 | 113 | | `--verify` | 完整性验证 | 114 | | `--include=PATTERN` | 包含文件 | 115 | | `--exclude=PATTERN` | 排除文件 | 116 | 117 | --- 118 | 119 | ## 问题排查 120 | 121 | | 问题 | 解决方案 | 122 | |------|----------| 123 | | 格式无法识别 | 使用 `--format=TYPE` 指定 | 124 | | 权限被拒绝 | 检查文件/目录权限 | 125 | | 找不到工具 | 安装对应工具 (p7zip-full, unrar 等) | 126 | | 分卷 ZIP 失败 | 安装 p7zip-full (`sudo apt install p7zip-full`) | 127 | 128 | --- 129 | 130 | ## 贡献 131 | 132 | - 📝 [提交问题](https://github.com/Hitmux/hitpag/issues) 133 | - 🔧 [提交 PR](https://github.com/Hitmux/hitpag/pulls) 134 | 135 | ## 许可证 136 | 137 | [GNU Affero General Public License v3.0](LICENSE) 138 | 139 | --- 140 | 141 | **开发者**: [Hitmux](https://hitmux.top) 142 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hitpag - The Intelligent Compression Tool 2 | 3 | **Smart, powerful, and easy-to-use command-line compression tool** 4 | 5 | [![GitHub](https://img.shields.io/badge/GitHub-Hitmux/hitpag-blue)](https://github.com/Hitmux/hitpag) 6 | [![Website](https://img.shields.io/badge/Website-hitmux.org-green)](https://hitmux.org) 7 | 8 | [English](README.md) | [简体中文](README_zh.md) 9 | 10 | --- 11 | 12 | ## Why hitpag? 13 | 14 | - **🧠 Smart Recognition** - Automatically detects file format by magic number, not just extension 15 | - **⚡ One Command** - No need to remember different tools for different formats 16 | - **📦 All Formats** - tar, gzip, bzip2, xz, zip, 7z, rar, lz4, zstd, xar 17 | - **🔐 Password Support** - Encryption for zip and 7z formats 18 | - **🚀 Multi-threaded** - Parallel compression for better performance 19 | 20 | --- 21 | 22 | ## Quick Start 23 | 24 | ### Install 25 | ```bash 26 | # Ubuntu/Debian 27 | sudo apt install -y tar gzip bzip2 xz-utils zip unzip p7zip-full lz4 zstd 28 | 29 | # Build 30 | git clone https://github.com/Hitmux/hitpag.git 31 | cd hitpag && mkdir build && cd build && cmake .. && make 32 | sudo make install # Optional 33 | ``` 34 | 35 | ### Basic Usage 36 | ```bash 37 | # Decompress - just point to the archive 38 | hitpag archive.tar.gz ./output/ 39 | hitpag backup.zip ./extracted/ 40 | hitpag data.7z ./data/ 41 | 42 | # Compress - specify source and target 43 | hitpag ./my_folder/ backup.zip 44 | hitpag ./documents/ archive.tar.gz 45 | 46 | # With password 47 | hitpag -pMySecret secure.7z ./sensitive/ 48 | hitpag -p encrypted.zip ./output/ # Prompts for password 49 | ``` 50 | 51 | That's it! hitpag figures out the rest. 52 | 53 | --- 54 | 55 | ## Advanced Usage 56 | 57 | ### Performance Options 58 | ```bash 59 | # Multi-threaded compression with benchmarking 60 | hitpag -l9 -t8 --benchmark data.tar.xz ./large_files/ 61 | 62 | # Ultra-fast compression (LZ4) 63 | hitpag --format=lz4 temp.lz4 ./temp_data/ 64 | 65 | # High-efficiency compression (Zstandard) 66 | hitpag --format=zstd archive.zstd ./documents/ 67 | ``` 68 | 69 | ### File Filtering 70 | ```bash 71 | # Include only specific files 72 | hitpag --include='*.cpp' --include='*.h' code.7z ./project/ 73 | 74 | # Exclude files 75 | hitpag --exclude='*.tmp' --exclude='node_modules/*' clean.tar.gz ./project/ 76 | ``` 77 | 78 | ### Other Options 79 | ```bash 80 | hitpag -i # Interactive mode 81 | hitpag --verbose archive.7z # Detailed output 82 | hitpag --verify data.tar.gz # Verify after compression 83 | hitpag --format=rar unknown # Force specific format 84 | ``` 85 | 86 | --- 87 | 88 | ## Supported Formats 89 | 90 | | Format | Compress | Decompress | Password | Notes | 91 | |--------|----------|------------|----------|-------| 92 | | tar, tar.gz, tar.bz2, tar.xz | ✅ | ✅ | ❌ | Classic Unix formats | 93 | | zip | ✅ | ✅ | ✅ | Including split archives (.z01, .z02, ...) | 94 | | 7z | ✅ | ✅ | ✅ | Best compression ratio | 95 | | rar | ❌ | ✅ | ✅ | Decompress only | 96 | | lz4 | ✅ | ✅ | ❌ | Ultra-fast speed | 97 | | zstd | ✅ | ✅ | ❌ | Best speed/ratio balance | 98 | | xar | ✅ | ✅ | ❌ | macOS native | 99 | 100 | --- 101 | 102 | ## Command Reference 103 | 104 | | Option | Description | 105 | |--------|-------------| 106 | | `-i` | Interactive mode | 107 | | `-p[password]` | Password (prompts if not provided) | 108 | | `-l[1-9]` | Compression level | 109 | | `-t[count]` | Thread count | 110 | | `--format=TYPE` | Force format | 111 | | `--verbose` | Detailed output | 112 | | `--benchmark` | Performance stats | 113 | | `--verify` | Integrity check | 114 | | `--include=PATTERN` | Include files | 115 | | `--exclude=PATTERN` | Exclude files | 116 | 117 | --- 118 | 119 | ## Troubleshooting 120 | 121 | | Problem | Solution | 122 | |---------|----------| 123 | | Format not recognized | Use `--format=TYPE` to specify | 124 | | Permission denied | Check file/directory permissions | 125 | | Tool not found | Install required tool (p7zip-full, unrar, etc.) | 126 | | Split ZIP fails | Install p7zip-full (`sudo apt install p7zip-full`) | 127 | 128 | --- 129 | 130 | ## Contributing 131 | 132 | - 📝 [Issues](https://github.com/Hitmux/hitpag/issues) 133 | - 🔧 [Pull Requests](https://github.com/Hitmux/hitpag/pulls) 134 | 135 | ## License 136 | 137 | [GNU Affero General Public License v3.0](LICENSE) 138 | 139 | --- 140 | 141 | **Developer**: [Hitmux](https://hitmux.top) 142 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Hitmux 2 | // This program is free software: you can redistribute it and/or modify 3 | // it under the terms of the GNU Affero General Public License as published by 4 | // the Free Software Foundation, either version 3 of the License, or 5 | // (at your option) any later version. 6 | 7 | // This program is distributed in the hope that it will be useful, 8 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | // GNU Affero General Public License for more details. 11 | 12 | // You should have received a copy of the GNU Affero General Public License 13 | // along with this program. If not, see . 14 | 15 | 16 | // hitpag - Smart Compression/Decompression Tool 17 | // Version: 2.0.0 18 | // website: https://hitmux.top 19 | // github: https://github.com/Hitmux/hitpag 20 | // A versatile command-line utility for compressing and decompressing files and directories. 21 | // It intelligently determines the operation type based on file extensions and provides 22 | // a user-friendly interactive mode. 23 | // 24 | // ==================================================================================== 25 | // IMPORTANT: External Dependencies 26 | // This program acts as a wrapper around standard command-line compression tools. 27 | // For it to function correctly, the following tools must be installed and accessible 28 | // in your system's PATH: 29 | // 30 | // - For .tar, .tar.gz, .tar.bz2, .tar.xz: `tar` 31 | // - For .zip: `zip` (for compression) and `unzip` (for decompression) 32 | // - For .7z: `7z` 33 | // - For .rar: `unrar` (or `rar` for decompression) 34 | // - For .lz4: `lz4` 35 | // - For .zst: `zstd` 36 | // - For .xar: `xar` 37 | // ==================================================================================== 38 | 39 | #include 40 | #include 41 | #include // For std::string_view 42 | #include 43 | #include 44 | #include 45 | #include 46 | #include 47 | #include 48 | #include // For std::transform 49 | #include // For std::tolower 50 | #include // For timing operations 51 | #include // For multi-threading support 52 | #include // For file pattern matching 53 | 54 | #include "target_conflict.h" 55 | 56 | // Platform-specific includes for process management and terminal control 57 | #ifdef _WIN32 58 | #include 59 | #include // For _pclose, _popen on Windows 60 | #include // For _getch 61 | #else 62 | #include // For popen, pclose 63 | #include // For tcgetattr, tcsetattr 64 | #include // For STDIN_FILENO, read, fork, execvp, chdir 65 | #include // For waitpid 66 | #endif 67 | 68 | namespace fs = std::filesystem; 69 | 70 | namespace util { 71 | std::string trim_copy(const std::string& value) { 72 | const auto first = value.find_first_not_of(" \t\n\r"); 73 | if (first == std::string::npos) { 74 | return ""; 75 | } 76 | const auto last = value.find_last_not_of(" \t\n\r"); 77 | return value.substr(first, last - first + 1); 78 | } 79 | } 80 | 81 | // [NEW] Application constants for easy maintenance and display. 82 | constexpr std::string_view APP_VERSION = "2.0.4"; 83 | constexpr std::string_view APP_WEBSITE = "https://hitmux.top"; 84 | constexpr std::string_view APP_GITHUB = "https://github.com/Hitmux/hitpag"; 85 | 86 | /** 87 | * @brief Internationalization (i18n) module. 88 | * 89 | * Manages all user-facing strings to facilitate easy translation and text changes 90 | * without altering the core application logic. 91 | */ 92 | namespace i18n { 93 | // A map holding all user-visible text messages, keyed by a unique identifier. 94 | const std::map messages = { 95 | // General messages 96 | {"welcome", "Welcome to hitpag smart compression/decompression tool"}, 97 | {"goodbye", "Thank you for using hitpag, goodbye!"}, 98 | {"processing", "Processing {COUNT} items..."}, 99 | {"compression_ratio", "Compression ratio: {RATIO}% (saved {SAVED} bytes)"}, 100 | {"operation_time", "Operation completed in {TIME} seconds"}, 101 | {"threads_info", "Using {COUNT} threads for parallel processing"}, 102 | 103 | // Help messages 104 | {"usage", "Usage: hitpag [options] [--] SOURCE_PATH TARGET_PATH"}, 105 | {"help_options", "Options:"}, 106 | {"help_i", " -i Interactive mode"}, 107 | {"help_p", " -p[password] Encrypt/Decrypt with a password. If password is not attached, prompts for it."}, 108 | {"help_l", " -l[level] Compression level (1-9, default depends on format)"}, 109 | {"help_t", " -t[threads] Number of threads to use (default: auto-detect)"}, 110 | {"help_verbose", " --verbose Show detailed progress information"}, 111 | {"help_exclude", " --exclude=PATTERN Exclude files/directories matching pattern"}, 112 | {"help_include", " --include=PATTERN Include only files/directories matching pattern"}, 113 | {"help_benchmark", " --benchmark Show compression performance statistics"}, 114 | {"help_verify", " --verify Verify archive integrity after compression"}, 115 | {"help_format", " --format=TYPE Force archive type (zip, 7z, tar.gz, tar.bz2, tar.xz, rar, lz4, zstd, xar)"}, 116 | {"help_h", " -h, --help Display help information"}, 117 | {"help_v", " -v, --version Display version information"}, 118 | {"help_examples", "Examples:"}, 119 | {"help_example1", " hitpag arch.tar.gz ./extracted_dir # Decompress arch.tar.gz to extracted_dir"}, 120 | {"help_example2", " hitpag ./my_folder my_archive.zip # Compress my_folder to my_archive.zip (creates my_folder inside zip)"}, 121 | {"help_example_new_path", " hitpag ./my_folder/ my_archive.zip # Compress contents of my_folder (no root folder in zip)"}, 122 | {"help_example3", " hitpag -i big_file.rar . # Interactive decompression of big_file.rar to current directory"}, 123 | {"help_example4", " hitpag -pmysecret my_docs.7z ./docs # Encrypt ./docs into my_docs.7z with password 'mysecret'"}, 124 | {"help_example5", " hitpag -p secret.zip . # Decompress secret.zip, will prompt for password"}, 125 | {"help_example6", " hitpag -l9 -t4 big_data.tar.gz ./data # Compress with max level using 4 threads"}, 126 | {"help_example7", " hitpag --verbose --benchmark ./files archive.7z # Verbose compression with benchmarking"}, 127 | {"help_example8", " hitpag --exclude='*.tmp' --include='*.cpp' src/ code.tar.gz # Filter files during compression"}, 128 | {"help_example9", " hitpag --format=zip data.7z ./extracted # Force treat data.7z as ZIP and decompress"}, 129 | 130 | // Error messages 131 | {"error_missing_args", "Error: Missing arguments. {ADDITIONAL_INFO}"}, 132 | {"error_invalid_source", "Error: Source path '{PATH}' does not exist or is invalid. {REASON}"}, 133 | {"error_invalid_target", "Error: Invalid target path '{PATH}'. {REASON}"}, 134 | {"error_same_path", "Error: Source and target paths cannot be the same"}, 135 | {"error_unknown_format", "Error: Unrecognized file format or ambiguous operation. {INFO}"}, 136 | {"error_tool_not_found", "Error: Required tool not found: {TOOL_NAME}. Please ensure it is installed and in your system's PATH."}, 137 | {"error_operation_failed", "Error: Operation failed (command: {COMMAND}, exit code: {EXIT_CODE}). Might be due to a wrong password."}, 138 | {"error_permission_denied", "Error: Permission denied. {PATH}"}, 139 | {"error_not_enough_space", "Error: Not enough disk space"}, 140 | // [NEW] Added message for closed input stream (EOF). 141 | {"error_input_stream_closed", "Input stream closed. Operation canceled."}, 142 | 143 | // Interactive mode messages 144 | {"interactive_mode", "Interactive mode started"}, 145 | {"ask_operation", "Please select operation type:"}, 146 | {"operation_compress", "1. Compress"}, 147 | {"operation_decompress", "2. Decompress"}, 148 | {"ask_format", "Please select compression format:"}, 149 | {"format_tar_gz", "tar.gz (gzip compression)"}, 150 | {"format_zip", "zip (supports password)"}, 151 | {"format_7z", "7z (supports password)"}, 152 | {"format_tar", "tar (no compression)"}, 153 | {"format_tar_bz2", "tar.bz2 (bzip2 compression)"}, 154 | {"format_tar_xz", "tar.xz (xz compression)"}, 155 | {"format_rar", "rar (decompression only recommended)"}, 156 | {"format_lz4", "lz4 (fast compression)"}, 157 | {"format_zstd", "zstd (modern compression)"}, 158 | {"format_xar", "xar (macOS archive format)"}, 159 | {"ask_overwrite", "Target '{TARGET_PATH}' already exists, overwrite? (y/n): "}, 160 | {"ask_delete_source", "Delete source '{SOURCE_PATH}' after operation? (y/n): "}, 161 | {"ask_set_password", "Set a password for the archive? (y/n): "}, 162 | {"ask_has_password", "Does the archive require a password? (y/n): "}, 163 | {"enter_password", "Enter password: "}, 164 | {"confirm_password", "Confirm password: "}, 165 | {"password_mismatch", "Passwords do not match. Please try again."}, 166 | {"invalid_choice", "Invalid choice, please try again"}, 167 | 168 | // Operation messages 169 | {"compressing", "Compressing..."}, 170 | {"decompressing", "Decompressing..."}, 171 | {"verifying", "Verifying archive integrity..."}, 172 | {"verification_success", "Archive verification successful"}, 173 | {"verification_failed", "Archive verification failed"}, 174 | {"operation_complete", "Operation complete"}, 175 | {"operation_canceled", "Operation canceled"}, 176 | {"warning_tar_password", "Warning: Password protection is not supported for tar formats. The password will be ignored."}, 177 | {"info_split_zip_detected", "Split ZIP archive detected, using 7z for extraction."}, 178 | {"error_split_zip_requires_7z", "Error: Split ZIP archives require '7z' (p7zip) for extraction. Please install p7zip-full."}, 179 | {"error_split_zip_main_not_found", "Main ZIP file not found for split archive. Expected: {PATH}"}, 180 | {"filtering_files", "Filtering files: included {INCLUDED}, excluded {EXCLUDED}"}, 181 | {"target_exists_header", "Target {OBJECT_TYPE} '{TARGET_PATH}' already exists."}, 182 | {"target_exists_options", "Choose action: [O]verwrite / [C]ancel / [R]ename"}, 183 | {"target_exists_choice_prompt", "Choice (o/c/r): "}, 184 | {"target_exists_invalid", "Invalid choice, please enter o, c, or r."}, 185 | {"target_exists_rename_prompt", "Enter a new target path (default: {DEFAULT}): "}, 186 | {"target_exists_empty", "Target path cannot be empty."}, 187 | {"target_exists_same", "New target path matches the current path. Please choose a different value."}, 188 | {"target_exists_remove_failed", "Failed to remove existing target '{TARGET_PATH}': {REASON}"}, 189 | {"target_exists_keep_directory", "Proceeding without deleting the existing directory. Existing files may be overwritten."}, 190 | {"target_exists_rename_conflict", "Path '{TARGET_PATH}' already exists. You may overwrite it or choose a different name."}, 191 | {"target_exists_object_file", "file"}, 192 | {"target_exists_object_directory", "directory"}, 193 | }; 194 | 195 | /** 196 | * @brief Retrieves and formats a message string. 197 | * @param key The unique identifier for the message template. Using string_view for performance. 198 | * @param placeholders A map of placeholder keys to their replacement values. 199 | * @return The formatted message string. 200 | */ 201 | std::string get(std::string_view key, const std::map& placeholders = {}) { 202 | auto it = messages.find(std::string(key)); 203 | std::string message_template; 204 | if (it != messages.end()) { 205 | message_template = it->second; 206 | } else { 207 | return "[" + std::string(key) + "]"; // Return key itself if not found 208 | } 209 | 210 | // Replace placeholders with provided values 211 | for(const auto& p : placeholders) { 212 | std::string placeholder_key = "{" + p.first + "}"; 213 | size_t pos = 0; 214 | while((pos = message_template.find(placeholder_key, pos)) != std::string::npos) { 215 | message_template.replace(pos, placeholder_key.length(), p.second); 216 | pos += p.second.length(); 217 | } 218 | } 219 | // Remove any unused placeholders to keep the output clean 220 | size_t start_ph = 0; 221 | while((start_ph = message_template.find("{", start_ph)) != std::string::npos) { 222 | size_t end_ph = message_template.find("}", start_ph); 223 | if (end_ph != std::string::npos) { 224 | message_template.erase(start_ph, end_ph - start_ph + 1); 225 | } else { 226 | break; // No closing brace 227 | } 228 | } 229 | return message_template; 230 | } 231 | } 232 | 233 | namespace cli_io { 234 | std::string get_input() { 235 | std::string input; 236 | if (!std::getline(std::cin, input)) { 237 | throw std::runtime_error(i18n::get("error_input_stream_closed")); 238 | } 239 | return util::trim_copy(input); 240 | } 241 | } 242 | 243 | namespace target_path { 244 | using InputFn = std::function; 245 | using OutputFn = std::function; 246 | 247 | namespace { 248 | std::string generate_sequential_candidate(const std::string& base_path, int suffix_index) { 249 | fs::path original(base_path); 250 | fs::path parent = original.parent_path(); 251 | std::string filename = original.filename().string(); 252 | 253 | static const std::vector multi_extensions = { 254 | ".tar.gz", ".tar.bz2", ".tar.xz", ".tar.zst", ".tar.lz4" 255 | }; 256 | 257 | std::string stem; 258 | std::string extension; 259 | 260 | for (const auto& ext : multi_extensions) { 261 | if (filename.size() > ext.size() && 262 | filename.compare(filename.size() - ext.size(), ext.size(), ext) == 0) { 263 | stem = filename.substr(0, filename.size() - ext.size()); 264 | extension = ext; 265 | break; 266 | } 267 | } 268 | 269 | if (stem.empty()) { 270 | auto pos = filename.find_last_of('.'); 271 | if (pos == std::string::npos || pos == 0) { 272 | stem = filename; 273 | } else { 274 | stem = filename.substr(0, pos); 275 | extension = filename.substr(pos); 276 | } 277 | } 278 | 279 | if (stem.empty() || stem == "." || stem == "..") { 280 | stem = "target"; 281 | } 282 | 283 | std::string suffixed_name = stem + "_" + std::to_string(suffix_index) + extension; 284 | 285 | fs::path combined = parent / suffixed_name; 286 | return combined.string(); 287 | } 288 | } // namespace 289 | 290 | bool resolve_existing_target(std::string& target_path, 291 | const InputFn& input_fn, 292 | const OutputFn& output_fn, 293 | const OutputFn& error_fn) { 294 | const std::string original_target = target_path; 295 | std::string rename_base = original_target; 296 | int suffix_counter = 1; 297 | 298 | while (fs::exists(target_path)) { 299 | const bool is_dir = fs::is_directory(target_path); 300 | const std::string object_label = i18n::get( 301 | is_dir ? "target_exists_object_directory" : "target_exists_object_file"); 302 | 303 | const std::string header = i18n::get("target_exists_header", { 304 | {"TARGET_PATH", target_path}, 305 | {"OBJECT_TYPE", object_label} 306 | }); 307 | 308 | const std::string options_line = i18n::get("target_exists_options"); 309 | const std::string choice_prompt = i18n::get("target_exists_choice_prompt"); 310 | const std::string invalid_choice_line = i18n::get("target_exists_invalid"); 311 | 312 | const auto action = target_conflict::prompt_action( 313 | output_fn, 314 | input_fn, 315 | header, 316 | options_line, 317 | choice_prompt, 318 | invalid_choice_line 319 | ); 320 | 321 | if (action == target_conflict::Action::Overwrite) { 322 | if (is_dir) { 323 | output_fn(i18n::get("target_exists_keep_directory") + "\n"); 324 | break; 325 | } 326 | 327 | std::error_code ec; 328 | fs::remove(target_path, ec); 329 | if (ec) { 330 | error_fn(i18n::get("target_exists_remove_failed", { 331 | {"TARGET_PATH", target_path}, 332 | {"REASON", ec.message()} 333 | }) + "\n"); 334 | continue; 335 | } 336 | break; 337 | } 338 | 339 | if (action == target_conflict::Action::Cancel) { 340 | return false; 341 | } 342 | 343 | // Rename flow 344 | while (true) { 345 | std::string default_candidate = generate_sequential_candidate(rename_base, suffix_counter); 346 | std::string candidate = target_conflict::prompt_new_path( 347 | output_fn, 348 | input_fn, 349 | i18n::get("target_exists_rename_prompt", {{"DEFAULT", default_candidate}}), 350 | default_candidate 351 | ); 352 | candidate = util::trim_copy(candidate); 353 | 354 | if (candidate.empty()) { 355 | candidate = default_candidate; 356 | } 357 | 358 | if (candidate == target_path) { 359 | error_fn(i18n::get("target_exists_same") + "\n"); 360 | if (candidate == default_candidate) { 361 | ++suffix_counter; 362 | } 363 | continue; 364 | } 365 | 366 | if (fs::exists(candidate)) { 367 | error_fn(i18n::get("target_exists_rename_conflict", { 368 | {"TARGET_PATH", candidate} 369 | }) + "\n"); 370 | if (candidate == default_candidate) { 371 | ++suffix_counter; 372 | } else { 373 | rename_base = candidate; 374 | suffix_counter = 1; 375 | } 376 | continue; 377 | } 378 | 379 | target_path = candidate; 380 | if (candidate == default_candidate) { 381 | ++suffix_counter; 382 | } else { 383 | rename_base = candidate; 384 | suffix_counter = 1; 385 | } 386 | break; 387 | } 388 | } 389 | 390 | return true; 391 | } 392 | } 393 | 394 | /** 395 | * @brief Centralized error handling module. 396 | * 397 | * Defines custom exception types and a unified way to report errors with 398 | * appropriate error codes and internationalized messages. 399 | */ 400 | namespace error { 401 | enum class ErrorCode { 402 | SUCCESS = 0, 403 | MISSING_ARGS = 1, 404 | INVALID_SOURCE = 2, 405 | INVALID_TARGET = 3, 406 | SAME_PATH = 4, 407 | UNKNOWN_FORMAT = 5, 408 | TOOL_NOT_FOUND = 6, 409 | OPERATION_FAILED = 7, 410 | PERMISSION_DENIED = 8, 411 | NOT_ENOUGH_SPACE = 9, 412 | UNKNOWN_ERROR = 99 413 | }; 414 | 415 | // Custom exception class for application-specific errors. 416 | class HitpagException : public std::runtime_error { 417 | private: 418 | ErrorCode code_; 419 | public: 420 | HitpagException(ErrorCode code, const std::string& message) 421 | : std::runtime_error(message), code_(code) {} 422 | ErrorCode code() const { return code_; } 423 | }; 424 | 425 | /** 426 | * @brief Throws a HitpagException with a formatted message. 427 | * @param code The error code corresponding to the error. 428 | * @param placeholders Values for placeholders in the error message. 429 | */ 430 | void throw_error(ErrorCode code, const std::map& placeholders = {}) { 431 | std::string message_key; 432 | switch (code) { 433 | case ErrorCode::MISSING_ARGS: message_key = "error_missing_args"; break; 434 | case ErrorCode::INVALID_SOURCE: message_key = "error_invalid_source"; break; 435 | case ErrorCode::INVALID_TARGET: message_key = "error_invalid_target"; break; 436 | case ErrorCode::SAME_PATH: message_key = "error_same_path"; break; 437 | case ErrorCode::UNKNOWN_FORMAT: message_key = "error_unknown_format"; break; 438 | case ErrorCode::TOOL_NOT_FOUND: message_key = "error_tool_not_found"; break; 439 | case ErrorCode::OPERATION_FAILED: message_key = "error_operation_failed"; break; 440 | case ErrorCode::PERMISSION_DENIED: message_key = "error_permission_denied"; break; 441 | case ErrorCode::NOT_ENOUGH_SPACE: message_key = "error_not_enough_space"; break; 442 | default: message_key = "Unknown error"; code = ErrorCode::UNKNOWN_ERROR; 443 | } 444 | throw HitpagException(code, i18n::get(message_key, placeholders)); 445 | } 446 | } 447 | 448 | /** 449 | * @brief Command-line argument parsing module. 450 | * 451 | * Responsible for parsing argc/argv, populating an options struct, 452 | * and handling requests for help or version information. 453 | */ 454 | namespace args { 455 | struct Options { 456 | bool interactive_mode = false; 457 | bool show_help = false; 458 | bool show_version = false; 459 | std::string source_path; 460 | std::vector source_paths; 461 | std::string target_path; 462 | std::string password; 463 | bool password_prompt = false; 464 | int compression_level = 0; // 0 means use default for format 465 | int thread_count = 0; // 0 means auto-detect 466 | bool verbose = false; 467 | bool benchmark = false; 468 | bool verify = false; 469 | std::vector exclude_patterns; 470 | std::vector include_patterns; 471 | std::string force_format; // Manual format specification 472 | }; 473 | 474 | Options parse(int argc, char* argv[]) { 475 | Options options; 476 | if (argc < 2) { 477 | options.show_help = true; 478 | return options; 479 | } 480 | 481 | std::vector args_vec(argv + 1, argv + argc); 482 | 483 | size_t i = 0; 484 | while (i < args_vec.size() && args_vec[i][0] == '-') { 485 | const std::string& opt = args_vec[i]; 486 | 487 | if (opt == "--") { 488 | i++; // Consume the "--" 489 | break; // Stop processing options 490 | } 491 | 492 | if (opt == "-i") { 493 | options.interactive_mode = true; 494 | i++; 495 | } else if (opt == "-h" || opt == "--help") { 496 | options.show_help = true; 497 | return options; 498 | } else if (opt == "-v" || opt == "--version") { 499 | options.show_version = true; 500 | return options; 501 | } else if (opt.rfind("-p", 0) == 0) { // Handles -p and -p 502 | if (opt.length() > 2) { 503 | // Password is attached, e.g., -psecret 504 | options.password = opt.substr(2); 505 | } else { 506 | // Just -p, prompt for password later 507 | options.password_prompt = true; 508 | } 509 | i++; 510 | } else if (opt.rfind("-l", 0) == 0) { // Compression level 511 | if (opt.length() > 2) { 512 | try { 513 | options.compression_level = std::stoi(opt.substr(2)); 514 | if (options.compression_level < 1 || options.compression_level > 9) { 515 | error::throw_error(error::ErrorCode::MISSING_ARGS, {{"ADDITIONAL_INFO", "Compression level must be between 1-9"}}); 516 | } 517 | } catch (const std::exception&) { 518 | error::throw_error(error::ErrorCode::MISSING_ARGS, {{"ADDITIONAL_INFO", "Invalid compression level"}}); 519 | } 520 | } else { 521 | options.compression_level = 6; // Default compression level 522 | } 523 | i++; 524 | } else if (opt.rfind("-t", 0) == 0) { // Thread count 525 | if (opt.length() > 2) { 526 | try { 527 | options.thread_count = std::stoi(opt.substr(2)); 528 | if (options.thread_count < 1) { 529 | error::throw_error(error::ErrorCode::MISSING_ARGS, {{"ADDITIONAL_INFO", "Thread count must be positive"}}); 530 | } 531 | } catch (const std::exception&) { 532 | error::throw_error(error::ErrorCode::MISSING_ARGS, {{"ADDITIONAL_INFO", "Invalid thread count"}}); 533 | } 534 | } else { 535 | unsigned int hw_threads = std::thread::hardware_concurrency(); 536 | options.thread_count = (hw_threads > 0) ? hw_threads : 1; // Fallback to 1 if detection fails 537 | } 538 | i++; 539 | } else if (opt == "--verbose") { 540 | options.verbose = true; 541 | i++; 542 | } else if (opt == "--benchmark") { 543 | options.benchmark = true; 544 | i++; 545 | } else if (opt == "--verify") { 546 | options.verify = true; 547 | i++; 548 | } else if (opt.rfind("--exclude=", 0) == 0) { 549 | options.exclude_patterns.push_back(opt.substr(10)); 550 | i++; 551 | } else if (opt.rfind("--include=", 0) == 0) { 552 | options.include_patterns.push_back(opt.substr(10)); 553 | i++; 554 | } else if (opt.rfind("--format=", 0) == 0) { 555 | std::string format_value = opt.substr(9); 556 | if (format_value.empty()) { 557 | error::throw_error(error::ErrorCode::MISSING_ARGS, {{"ADDITIONAL_INFO", "--format requires a value"}}); 558 | } 559 | options.force_format = format_value; 560 | i++; 561 | } else { 562 | error::throw_error(error::ErrorCode::MISSING_ARGS, {{"ADDITIONAL_INFO", "Unknown option: " + opt}}); 563 | } 564 | } 565 | 566 | std::vector positional_args; 567 | while (i < args_vec.size()) { 568 | positional_args.push_back(args_vec[i++]); 569 | } 570 | 571 | if (!positional_args.empty()) { 572 | options.target_path = positional_args.back(); 573 | positional_args.pop_back(); 574 | options.source_paths = positional_args; 575 | if (!options.source_paths.empty()) { 576 | options.source_path = options.source_paths.front(); 577 | } 578 | } 579 | 580 | if (!options.source_paths.empty() && options.target_path.empty()) { 581 | error::throw_error(error::ErrorCode::MISSING_ARGS, {{"ADDITIONAL_INFO", "Target path missing"}}); 582 | } 583 | if (options.source_paths.empty() && !options.target_path.empty()) { 584 | error::throw_error(error::ErrorCode::MISSING_ARGS, {{"ADDITIONAL_INFO", "Source path missing"}}); 585 | } 586 | 587 | if (!options.interactive_mode && !options.show_help && !options.show_version) { 588 | if (options.source_paths.empty()) error::throw_error(error::ErrorCode::MISSING_ARGS, {{"ADDITIONAL_INFO", "Source path missing"}}); 589 | if (options.target_path.empty()) error::throw_error(error::ErrorCode::MISSING_ARGS, {{"ADDITIONAL_INFO", "Target path missing"}}); 590 | } 591 | 592 | return options; 593 | } 594 | 595 | // Data-driven help display for easy maintenance. 596 | struct HelpOption { std::string flag; std::string key; }; 597 | void show_help() { 598 | // [MODIFIED] Display hardcoded app info for a richer help screen. 599 | std::cout << "hitpag - Smart Compression/Decompression Tool (Version " << APP_VERSION << ")" << std::endl; 600 | std::cout << "Website: " << APP_WEBSITE << std::endl; 601 | std::cout << "GitHub: " << APP_GITHUB << std::endl; 602 | std::cout << "================================================================================" << std::endl << std::endl; 603 | 604 | std::cout << i18n::get("usage") << std::endl << std::endl; 605 | std::cout << i18n::get("help_options") << std::endl; 606 | 607 | const std::vector help_options = { 608 | {"-i", "help_i"}, {"-p", "help_p"}, {"-l", "help_l"}, {"-t", "help_t"}, 609 | {"--verbose", "help_verbose"}, {"--exclude", "help_exclude"}, 610 | {"--include", "help_include"}, {"--benchmark", "help_benchmark"}, 611 | {"--verify", "help_verify"}, {"--format", "help_format"}, {"-h", "help_h"}, {"-v", "help_v"} 612 | }; 613 | for(const auto& opt : help_options) std::cout << i18n::get(opt.key) << std::endl; 614 | 615 | std::cout << std::endl << i18n::get("help_examples") << std::endl; 616 | const std::vector example_keys = { 617 | "help_example1", "help_example2", "help_example_new_path", "help_example3", 618 | "help_example4", "help_example5", "help_example6", "help_example7", "help_example8", "help_example9" 619 | }; 620 | for(const auto& key : example_keys) std::cout << i18n::get(key) << std::endl; 621 | } 622 | 623 | void show_version() { 624 | // [MODIFIED] Use the hardcoded constant for version display. 625 | std::cout << "hitpag Version " << APP_VERSION << std::endl; 626 | } 627 | } 628 | 629 | /** 630 | * @brief File type recognition and operation determination module. 631 | * 632 | * Uses a combination of file extension and magic number (file header) analysis 633 | * to identify file types and infer the user's intended operation (compress/decompress). 634 | */ 635 | namespace file_type { 636 | enum class FileType { 637 | REGULAR_FILE, DIRECTORY, ARCHIVE_TAR, ARCHIVE_TAR_GZ, ARCHIVE_TAR_BZ2, 638 | ARCHIVE_TAR_XZ, ARCHIVE_ZIP, ARCHIVE_RAR, ARCHIVE_7Z, 639 | ARCHIVE_LZ4, ARCHIVE_ZSTD, ARCHIVE_XAR, UNKNOWN 640 | }; 641 | enum class OperationType { COMPRESS, DECOMPRESS, UNKNOWN }; 642 | 643 | struct RecognitionResult { 644 | FileType source_type = FileType::UNKNOWN; 645 | FileType target_type_hint = FileType::UNKNOWN; 646 | OperationType operation = OperationType::UNKNOWN; 647 | }; 648 | 649 | 650 | // Check if extension matches split ZIP part pattern (.z01, .z02, ... .z99) 651 | // This is a helper function used by both file_type and operation namespaces 652 | bool is_split_zip_extension(const std::string& ext_lower) { 653 | return ext_lower.size() == 4 && ext_lower[0] == '.' && ext_lower[1] == 'z' && 654 | std::isdigit(static_cast(ext_lower[2])) && 655 | std::isdigit(static_cast(ext_lower[3])); 656 | } 657 | 658 | FileType recognize_by_extension(const std::string& path_str) { 659 | fs::path p(path_str); 660 | if (!p.has_extension()) return FileType::UNKNOWN; 661 | std::string ext = p.extension().string(); 662 | std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c){ return std::tolower(c); }); 663 | 664 | if (ext == ".tar") return FileType::ARCHIVE_TAR; 665 | if (ext == ".zip") return FileType::ARCHIVE_ZIP; 666 | if (is_split_zip_extension(ext)) return FileType::ARCHIVE_ZIP; 667 | if (ext == ".rar") return FileType::ARCHIVE_RAR; 668 | if (ext == ".7z") return FileType::ARCHIVE_7Z; 669 | if (ext == ".lz4") return FileType::ARCHIVE_LZ4; 670 | if (ext == ".zst" || ext == ".zstd") return FileType::ARCHIVE_ZSTD; 671 | if (ext == ".xar") return FileType::ARCHIVE_XAR; 672 | if (ext == ".tgz") return FileType::ARCHIVE_TAR_GZ; 673 | if (ext == ".tbz2" || ext == ".tbz") return FileType::ARCHIVE_TAR_BZ2; 674 | if (ext == ".txz") return FileType::ARCHIVE_TAR_XZ; 675 | 676 | // Handle double extensions like ".tar.gz" 677 | if (p.has_stem() && fs::path(p.stem()).has_extension()) { 678 | std::string stem_ext = fs::path(p.stem()).extension().string(); 679 | std::transform(stem_ext.begin(), stem_ext.end(), stem_ext.begin(), [](unsigned char c){ return std::tolower(c); }); 680 | if (stem_ext == ".tar") { 681 | if (ext == ".gz") return FileType::ARCHIVE_TAR_GZ; 682 | if (ext == ".bz2") return FileType::ARCHIVE_TAR_BZ2; 683 | if (ext == ".xz") return FileType::ARCHIVE_TAR_XZ; 684 | } 685 | } 686 | return FileType::UNKNOWN; 687 | } 688 | 689 | FileType recognize_by_header(const std::string& path) { 690 | std::ifstream file(path, std::ios::binary); 691 | if (!file) return FileType::UNKNOWN; 692 | std::array header{}; 693 | file.read(header.data(), header.size()); 694 | if(file.gcount() < 4) return FileType::UNKNOWN; 695 | 696 | // ZIP archives (PK signature, but check if it's really ZIP vs other PK-based formats) 697 | if (header[0] == 0x50 && header[1] == 0x4B) { 698 | // Check specific ZIP signatures 699 | if ((header[2] == 0x03 && header[3] == 0x04) || // Local file header 700 | (header[2] == 0x05 && header[3] == 0x06) || // End central directory 701 | (header[2] == 0x01 && header[3] == 0x02)) { // Central directory file header 702 | return FileType::ARCHIVE_ZIP; 703 | } 704 | } 705 | 706 | // RAR archives 707 | if (header[0] == 0x52 && header[1] == 0x61 && header[2] == 0x72 && header[3] == 0x21) { 708 | return FileType::ARCHIVE_RAR; // Rar! 709 | } 710 | 711 | // 7Z archives 712 | if (file.gcount() >= 6 && header[0] == 0x37 && header[1] == 0x7A && header[2] == (char)0xBC && header[3] == (char)0xAF && 713 | header[4] == 0x27 && header[5] == 0x1C) { 714 | return FileType::ARCHIVE_7Z; 715 | } 716 | 717 | // GZIP files (could be .tar.gz or standalone .gz) 718 | if (header[0] == (char)0x1F && header[1] == (char)0x8B) { 719 | return FileType::ARCHIVE_TAR_GZ; // Default to tar.gz, could be refined further 720 | } 721 | 722 | // BZIP2 files 723 | if (header[0] == 0x42 && header[1] == 0x5A && header[2] == 0x68) { 724 | return FileType::ARCHIVE_TAR_BZ2; // BZh 725 | } 726 | 727 | // XZ files 728 | if (file.gcount() >= 6 && header[0] == (char)0xFD && header[1] == 0x37 && header[2] == 0x7A && 729 | header[3] == 0x58 && header[4] == 0x5A && header[5] == 0x00) { 730 | return FileType::ARCHIVE_TAR_XZ; // .7zXZ.. 731 | } 732 | 733 | // LZ4 files 734 | if (header[0] == 0x04 && header[1] == 0x22 && header[2] == 0x4D && header[3] == 0x18) { 735 | return FileType::ARCHIVE_LZ4; 736 | } 737 | 738 | // ZSTD files 739 | if ((header[0] == 0x28 && header[1] == (char)0xB5 && header[2] == 0x2F && header[3] == (char)0xFD) || 740 | (header[0] == 0x22 && header[1] == (char)0xB5 && header[2] == 0x2F && header[3] == (char)0xFD)) { 741 | return FileType::ARCHIVE_ZSTD; 742 | } 743 | 744 | // TAR archives have "ustar" at byte offset 257 745 | file.clear(); 746 | file.seekg(257); 747 | std::array tar_header{}; 748 | file.read(tar_header.data(), tar_header.size()); 749 | if (file.gcount() >= 5 && std::string(tar_header.data(), 5) == "ustar") { 750 | return FileType::ARCHIVE_TAR; 751 | } 752 | 753 | // Check for old TAR format (may not have ustar signature) 754 | file.clear(); 755 | file.seekg(0); 756 | std::array tar_block{}; 757 | file.read(tar_block.data(), tar_block.size()); 758 | if (file.gcount() >= 512) { 759 | // Check if looks like TAR header (filename in first 100 bytes, checksum calculation) 760 | bool looks_like_tar = true; 761 | bool has_filename = false; 762 | 763 | // Check if there's at least one non-null character in filename field 764 | for (int i = 0; i < 100; ++i) { 765 | if (tar_block[i] != 0) { 766 | has_filename = true; 767 | // Check if character is printable 768 | if (tar_block[i] < 32 || tar_block[i] > 126) { 769 | looks_like_tar = false; 770 | break; 771 | } 772 | } 773 | } 774 | 775 | if (looks_like_tar && has_filename) { 776 | return FileType::ARCHIVE_TAR; 777 | } 778 | } 779 | 780 | return FileType::UNKNOWN; 781 | } 782 | 783 | // Helper function to robustly determine the type of a source path. 784 | FileType recognize_source_type(const std::string& source_path_str) { 785 | if (!fs::exists(source_path_str)) { 786 | error::throw_error(error::ErrorCode::INVALID_SOURCE, {{"PATH", source_path_str}}); 787 | } 788 | 789 | if (fs::is_directory(source_path_str)) return FileType::DIRECTORY; 790 | 791 | if (fs::is_regular_file(source_path_str)) { 792 | // Try header detection first (more reliable) 793 | FileType type = recognize_by_header(source_path_str); 794 | if (type == FileType::UNKNOWN) { 795 | // Fall back to extension if header detection fails 796 | type = recognize_by_extension(source_path_str); 797 | } 798 | // If still unknown, it's just a regular file, not a known archive type. 799 | return (type == FileType::UNKNOWN) ? FileType::REGULAR_FILE : type; 800 | } 801 | 802 | error::throw_error(error::ErrorCode::INVALID_SOURCE, {{"PATH", source_path_str}, {"REASON", "not a regular file or directory"}}); 803 | return FileType::UNKNOWN; // Should not be reached 804 | } 805 | 806 | RecognitionResult recognize(const std::string& source_path_str, const std::string& target_path_str) { 807 | RecognitionResult result; 808 | result.source_type = recognize_source_type(source_path_str); 809 | 810 | if (!target_path_str.empty()) { 811 | result.target_type_hint = recognize_by_extension(target_path_str); 812 | } 813 | 814 | bool target_is_archive = (result.target_type_hint != FileType::UNKNOWN && result.target_type_hint != FileType::REGULAR_FILE && result.target_type_hint != FileType::DIRECTORY); 815 | 816 | if (result.source_type == FileType::DIRECTORY || result.source_type == FileType::REGULAR_FILE) { 817 | if (target_is_archive) { 818 | result.operation = OperationType::COMPRESS; 819 | } else { 820 | // If no archive extension detected, we'll default to compression operation 821 | // The format will be determined by --format option if provided 822 | result.operation = OperationType::COMPRESS; 823 | result.target_type_hint = FileType::UNKNOWN; 824 | } 825 | } else { // Source is an archive 826 | result.operation = OperationType::DECOMPRESS; 827 | if (fs::exists(target_path_str) && !fs::is_directory(target_path_str)) { 828 | error::throw_error(error::ErrorCode::INVALID_TARGET, {{"PATH", target_path_str}, {"REASON", "Target for decompression must be a directory."}}); 829 | } 830 | } 831 | 832 | return result; 833 | } 834 | 835 | std::string get_file_type_string(FileType type) { 836 | static const std::map type_map = { 837 | {FileType::REGULAR_FILE, "Regular File"}, {FileType::DIRECTORY, "Directory"}, 838 | {FileType::ARCHIVE_TAR, "TAR Archive"}, {FileType::ARCHIVE_TAR_GZ, "TAR.GZ Archive"}, 839 | {FileType::ARCHIVE_TAR_BZ2, "TAR.BZ2 Archive"}, {FileType::ARCHIVE_TAR_XZ, "TAR.XZ Archive"}, 840 | {FileType::ARCHIVE_ZIP, "ZIP Archive"}, {FileType::ARCHIVE_RAR, "RAR Archive"}, 841 | {FileType::ARCHIVE_7Z, "7Z Archive"}, {FileType::ARCHIVE_LZ4, "LZ4 Archive"}, 842 | {FileType::ARCHIVE_ZSTD, "ZSTD Archive"}, {FileType::ARCHIVE_XAR, "XAR Archive"}, 843 | {FileType::UNKNOWN, "Unknown Type"} 844 | }; 845 | auto it = type_map.find(type); 846 | return it != type_map.end() ? it->second : "Unknown"; 847 | } 848 | 849 | FileType parse_format_string(const std::string& format_str) { 850 | std::string fmt = format_str; 851 | std::transform(fmt.begin(), fmt.end(), fmt.begin(), [](unsigned char c){ return std::tolower(c); }); 852 | 853 | if (fmt == "zip") return FileType::ARCHIVE_ZIP; 854 | if (fmt == "7z") return FileType::ARCHIVE_7Z; 855 | if (fmt == "tar") return FileType::ARCHIVE_TAR; 856 | if (fmt == "tar.gz" || fmt == "tgz") return FileType::ARCHIVE_TAR_GZ; 857 | if (fmt == "tar.bz2" || fmt == "tbz2") return FileType::ARCHIVE_TAR_BZ2; 858 | if (fmt == "tar.xz" || fmt == "txz") return FileType::ARCHIVE_TAR_XZ; 859 | if (fmt == "rar") return FileType::ARCHIVE_RAR; 860 | if (fmt == "lz4") return FileType::ARCHIVE_LZ4; 861 | if (fmt == "zstd" || fmt == "zst") return FileType::ARCHIVE_ZSTD; 862 | if (fmt == "xar") return FileType::ARCHIVE_XAR; 863 | 864 | return FileType::UNKNOWN; 865 | } 866 | } 867 | 868 | /** 869 | * @brief File filtering module for include/exclude pattern matching. 870 | */ 871 | namespace file_filter { 872 | bool matches_pattern(const std::string& filename, const std::string& pattern) { 873 | try { 874 | std::regex regex_pattern(pattern); 875 | return std::regex_match(filename, regex_pattern); 876 | } catch (const std::regex_error&) { 877 | // Fallback to simple wildcard matching 878 | return filename.find(pattern) != std::string::npos; 879 | } 880 | } 881 | 882 | bool should_include_file(const std::string& filepath, 883 | const std::vector& include_patterns, 884 | const std::vector& exclude_patterns) { 885 | fs::path p(filepath); 886 | std::string filename = p.filename().string(); 887 | 888 | // Check exclude patterns first 889 | for (const auto& pattern : exclude_patterns) { 890 | if (matches_pattern(filename, pattern) || matches_pattern(filepath, pattern)) { 891 | return false; 892 | } 893 | } 894 | 895 | // If include patterns are specified, file must match at least one 896 | if (!include_patterns.empty()) { 897 | for (const auto& pattern : include_patterns) { 898 | if (matches_pattern(filename, pattern) || matches_pattern(filepath, pattern)) { 899 | return true; 900 | } 901 | } 902 | return false; // No include pattern matched 903 | } 904 | 905 | return true; // No include patterns, and not excluded 906 | } 907 | 908 | std::vector filter_files(const std::vector& files, 909 | const std::vector& include_patterns, 910 | const std::vector& exclude_patterns, 911 | bool verbose = false) { 912 | std::vector filtered; 913 | size_t excluded_count = 0; 914 | 915 | for (const auto& file : files) { 916 | if (should_include_file(file, include_patterns, exclude_patterns)) { 917 | filtered.push_back(file); 918 | } else { 919 | excluded_count++; 920 | if (verbose) { 921 | std::cout << "Excluded: " << file << std::endl; 922 | } 923 | } 924 | } 925 | 926 | if (verbose) { 927 | std::cout << i18n::get("filtering_files", { 928 | {"INCLUDED", std::to_string(filtered.size())}, 929 | {"EXCLUDED", std::to_string(excluded_count)} 930 | }) << std::endl; 931 | } 932 | 933 | return filtered; 934 | } 935 | } 936 | 937 | /** 938 | * @brief Progress tracking and performance measurement module. 939 | */ 940 | namespace progress { 941 | struct CompressionStats { 942 | size_t original_size = 0; 943 | size_t compressed_size = 0; 944 | double compression_time = 0.0; 945 | int thread_count = 1; 946 | 947 | double get_compression_ratio() const { 948 | return original_size > 0 ? (1.0 - static_cast(compressed_size) / original_size) * 100.0 : 0.0; 949 | } 950 | 951 | size_t get_saved_bytes() const { 952 | return original_size > compressed_size ? original_size - compressed_size : 0; 953 | } 954 | }; 955 | 956 | CompressionStats current_stats; 957 | std::chrono::high_resolution_clock::time_point start_time; 958 | 959 | void start_operation() { 960 | start_time = std::chrono::high_resolution_clock::now(); 961 | } 962 | 963 | void end_operation() { 964 | auto end_time = std::chrono::high_resolution_clock::now(); 965 | current_stats.compression_time = std::chrono::duration(end_time - start_time).count(); 966 | } 967 | 968 | void set_thread_count(int threads) { 969 | current_stats.thread_count = threads; 970 | } 971 | 972 | void set_original_size(size_t size) { 973 | current_stats.original_size = size; 974 | } 975 | 976 | void set_compressed_size(size_t size) { 977 | current_stats.compressed_size = size; 978 | } 979 | 980 | size_t calculate_directory_size(const std::string& path) { 981 | size_t total_size = 0; 982 | std::error_code ec; 983 | 984 | for (const auto& entry : fs::recursive_directory_iterator(path, ec)) { 985 | if (!ec && entry.is_regular_file()) { 986 | total_size += entry.file_size(ec); 987 | } 988 | } 989 | 990 | return total_size; 991 | } 992 | 993 | void print_stats(bool verbose, bool benchmark) { 994 | if (benchmark) { 995 | std::cout << i18n::get("operation_time", { 996 | {"TIME", std::to_string(current_stats.compression_time)} 997 | }) << std::endl; 998 | 999 | if (current_stats.original_size > 0 && current_stats.compressed_size > 0) { 1000 | std::cout << i18n::get("compression_ratio", { 1001 | {"RATIO", std::to_string(current_stats.get_compression_ratio())}, 1002 | {"SAVED", std::to_string(current_stats.get_saved_bytes())} 1003 | }) << std::endl; 1004 | } 1005 | 1006 | if (current_stats.thread_count > 1) { 1007 | std::cout << i18n::get("threads_info", { 1008 | {"COUNT", std::to_string(current_stats.thread_count)} 1009 | }) << std::endl; 1010 | } 1011 | } 1012 | } 1013 | } 1014 | namespace operation { 1015 | struct CompressionSource { 1016 | std::string path; 1017 | bool include_contents = false; 1018 | }; 1019 | 1020 | bool is_tool_available(std::string_view tool) { 1021 | #ifdef _WIN32 1022 | std::string command = "where " + std::string(tool) + " > nul 2>&1"; 1023 | #else 1024 | std::string command = "command -v " + std::string(tool) + " > /dev/null 2>&1"; 1025 | #endif 1026 | return system(command.c_str()) == 0; 1027 | } 1028 | 1029 | // Check if file extension is a split ZIP part (.z01, .z02, ... .z99) 1030 | bool is_split_zip_part(const std::string& path) { 1031 | fs::path p(path); 1032 | if (!p.has_extension()) return false; 1033 | 1034 | std::string ext = p.extension().string(); 1035 | std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c){ return std::tolower(c); }); 1036 | 1037 | return file_type::is_split_zip_extension(ext); 1038 | } 1039 | 1040 | // Find the main .zip file for a split archive 1041 | // Input can be .zip file or any split part (.z01, .z02, etc.) 1042 | // Returns empty string if main file not found 1043 | std::string find_split_zip_main(const std::string& any_part_path) { 1044 | fs::path p(any_part_path); 1045 | fs::path main_zip = p; 1046 | main_zip.replace_extension(".zip"); 1047 | 1048 | if (fs::exists(main_zip)) { 1049 | return main_zip.string(); 1050 | } 1051 | return ""; 1052 | } 1053 | 1054 | // Check if a ZIP file is part of a split archive (has .z01, .z02, etc.) 1055 | // Works with both .zip main file and .z01/.z02/etc. split parts 1056 | bool is_split_zip(const std::string& zip_path) { 1057 | fs::path p(zip_path); 1058 | if (!p.has_extension()) return false; 1059 | 1060 | std::string ext = p.extension().string(); 1061 | std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c){ return std::tolower(c); }); 1062 | 1063 | // If it's a split part (.z01, .z02, etc.), it's definitely a split archive 1064 | if (is_split_zip_part(zip_path)) { 1065 | return true; 1066 | } 1067 | 1068 | // If it's a .zip file, check if .z01 exists 1069 | if (ext != ".zip") return false; 1070 | 1071 | fs::path z01_path = p; 1072 | z01_path.replace_extension(".z01"); 1073 | return fs::exists(z01_path); 1074 | } 1075 | 1076 | // Build 7z extraction arguments (shared between 7z format and split ZIP) 1077 | void build_7z_extract_args(std::vector& args, 1078 | const std::string& source_path, 1079 | const std::string& target_dir_path, 1080 | const std::string& password) { 1081 | args.push_back("x"); 1082 | if (!password.empty()) { 1083 | args.push_back("-p" + password); 1084 | } 1085 | args.push_back(fs::absolute(source_path).string()); 1086 | args.push_back("-o" + fs::absolute(target_dir_path).string()); 1087 | args.push_back("-y"); 1088 | } 1089 | 1090 | #ifdef _WIN32 1091 | std::string quote_argument_for_windows(const std::string& arg) { 1092 | if (arg.empty()) { 1093 | return "\"\""; 1094 | } 1095 | if (arg.find_first_of(" \t\n\v\"") == std::string::npos) { 1096 | return arg; 1097 | } 1098 | 1099 | std::string quoted_arg; 1100 | quoted_arg.push_back('"'); 1101 | for (auto it = arg.begin(); ; ++it) { 1102 | unsigned int backslash_count = 0; 1103 | while (it != arg.end() && *it == '\\') { 1104 | ++it; 1105 | ++backslash_count; 1106 | } 1107 | 1108 | if (it == arg.end()) { 1109 | quoted_arg.append(backslash_count * 2, '\\'); 1110 | break; 1111 | } 1112 | 1113 | if (*it == '"') { 1114 | quoted_arg.append(backslash_count * 2 + 1, '\\'); 1115 | quoted_arg.push_back(*it); 1116 | } else { 1117 | quoted_arg.append(backslash_count, '\\'); 1118 | quoted_arg.push_back(*it); 1119 | } 1120 | } 1121 | quoted_arg.push_back('"'); 1122 | return quoted_arg; 1123 | } 1124 | #endif 1125 | 1126 | int execute_command(const std::string& tool, const std::vector& args, const std::string& working_dir = "") { 1127 | std::string full_command = tool; 1128 | for(const auto& arg : args) full_command += " " + arg; 1129 | 1130 | #ifdef _WIN32 1131 | PROCESS_INFORMATION piProcInfo; 1132 | STARTUPINFOA siStartInfo; 1133 | ZeroMemory(&piProcInfo, sizeof(PROCESS_INFORMATION)); 1134 | ZeroMemory(&siStartInfo, sizeof(STARTUPINFOA)); 1135 | siStartInfo.cb = sizeof(STARTUPINFOA); 1136 | siStartInfo.hStdError = GetStdHandle(STD_ERROR_HANDLE); 1137 | siStartInfo.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); 1138 | siStartInfo.hStdInput = GetStdHandle(STD_INPUT_HANDLE); 1139 | siStartInfo.dwFlags |= STARTF_USESTDHANDLES; 1140 | 1141 | std::string command_line = quote_argument_for_windows(tool); 1142 | for (const auto& arg : args) { 1143 | command_line += " " + quote_argument_for_windows(arg); 1144 | } 1145 | 1146 | BOOL bSuccess = CreateProcessA( 1147 | NULL, 1148 | &command_line[0], 1149 | NULL, NULL, TRUE, 0, NULL, 1150 | working_dir.empty() ? NULL : working_dir.c_str(), 1151 | &siStartInfo, &piProcInfo 1152 | ); 1153 | 1154 | if (!bSuccess) { 1155 | error::throw_error(error::ErrorCode::OPERATION_FAILED, {{"COMMAND", full_command}, {"EXIT_CODE", "CreateProcess_failed: " + std::to_string(GetLastError())}}); 1156 | } 1157 | 1158 | WaitForSingleObject(piProcInfo.hProcess, INFINITE); 1159 | DWORD exit_code; 1160 | GetExitCodeProcess(piProcInfo.hProcess, &exit_code); 1161 | 1162 | CloseHandle(piProcInfo.hProcess); 1163 | CloseHandle(piProcInfo.hThread); 1164 | 1165 | #else 1166 | pid_t pid = fork(); 1167 | if (pid == -1) { 1168 | error::throw_error(error::ErrorCode::OPERATION_FAILED, {{"COMMAND", full_command}, {"EXIT_CODE", "fork_failed"}}); 1169 | } 1170 | 1171 | if (pid == 0) { // Child process 1172 | if (!working_dir.empty()) { 1173 | if (chdir(working_dir.c_str()) != 0) { 1174 | perror("chdir failed in child"); 1175 | _exit(127); 1176 | } 1177 | } 1178 | 1179 | std::vector c_args; 1180 | c_args.push_back(const_cast(tool.c_str())); 1181 | for (const auto& arg : args) { 1182 | c_args.push_back(const_cast(arg.c_str())); 1183 | } 1184 | c_args.push_back(nullptr); 1185 | 1186 | execvp(c_args[0], c_args.data()); 1187 | perror("execvp failed"); 1188 | _exit(127); 1189 | } 1190 | 1191 | int status; 1192 | waitpid(pid, &status, 0); 1193 | int exit_code = WIFEXITED(status) ? WEXITSTATUS(status) : -1; 1194 | #endif 1195 | if (exit_code != 0) { 1196 | std::cerr << std::endl; 1197 | } 1198 | return exit_code; 1199 | } 1200 | 1201 | // Archive verification function 1202 | bool verify_archive(const std::string& archive_path, file_type::FileType format) { 1203 | std::string tool; 1204 | std::vector args; 1205 | 1206 | switch (format) { 1207 | case file_type::FileType::ARCHIVE_TAR: 1208 | case file_type::FileType::ARCHIVE_TAR_GZ: 1209 | case file_type::FileType::ARCHIVE_TAR_BZ2: 1210 | case file_type::FileType::ARCHIVE_TAR_XZ: 1211 | tool = "tar"; 1212 | args = {"-tf", archive_path}; 1213 | break; 1214 | case file_type::FileType::ARCHIVE_ZIP: 1215 | tool = "unzip"; 1216 | args = {"-t", archive_path}; 1217 | break; 1218 | case file_type::FileType::ARCHIVE_7Z: 1219 | tool = "7z"; 1220 | args = {"t", archive_path}; 1221 | break; 1222 | default: 1223 | return true; // Skip verification for unsupported formats 1224 | } 1225 | 1226 | if (!is_tool_available(tool)) return false; 1227 | 1228 | // Redirect output to avoid cluttering the console 1229 | int result = execute_command(tool, args); 1230 | return result == 0; 1231 | } 1232 | 1233 | namespace { 1234 | bool is_descendant_or_same(const fs::path& base, const fs::path& target) { 1235 | std::error_code ec; 1236 | fs::path relative = fs::relative(target, base, ec); 1237 | if (ec) return false; 1238 | if (relative.empty()) return true; 1239 | for (const auto& part : relative) { 1240 | if (part == "..") { 1241 | return false; 1242 | } 1243 | } 1244 | return true; 1245 | } 1246 | 1247 | fs::path determine_common_base(const std::vector& paths) { 1248 | if (paths.empty()) return fs::current_path(); 1249 | fs::path base = paths.front().parent_path(); 1250 | if (base.empty()) base = paths.front(); 1251 | while (!base.empty()) { 1252 | bool all_descendants = true; 1253 | for (const auto& p : paths) { 1254 | if (!is_descendant_or_same(base, p)) { 1255 | all_descendants = false; 1256 | break; 1257 | } 1258 | } 1259 | if (all_descendants) return base; 1260 | base = base.parent_path(); 1261 | } 1262 | fs::path fallback = paths.front().root_path(); 1263 | if (fallback.empty()) fallback = fs::current_path(); 1264 | return fallback; 1265 | } 1266 | 1267 | uintmax_t calculate_sources_size(const std::vector& canonical_sources) { 1268 | uintmax_t total = 0; 1269 | for (const auto& p : canonical_sources) { 1270 | std::error_code ec; 1271 | if (fs::is_directory(p, ec)) { 1272 | total += progress::calculate_directory_size(p.string()); 1273 | } else { 1274 | auto sz = fs::file_size(p, ec); 1275 | if (!ec) total += sz; 1276 | } 1277 | } 1278 | return total; 1279 | } 1280 | } // namespace 1281 | 1282 | void compress(const std::vector& sources, const std::string& target_path_str, 1283 | file_type::FileType target_format, const std::string& password, 1284 | const args::Options& options = {}) { 1285 | if (sources.empty()) { 1286 | error::throw_error(error::ErrorCode::MISSING_ARGS, {{"ADDITIONAL_INFO", "No sources provided for compression"}}); 1287 | } 1288 | 1289 | std::vector canonical_sources; 1290 | canonical_sources.reserve(sources.size()); 1291 | std::vector is_directory_flags; 1292 | is_directory_flags.reserve(sources.size()); 1293 | 1294 | for (const auto& src : sources) { 1295 | fs::path path_input(src.path); 1296 | if (!fs::exists(path_input)) { 1297 | error::throw_error(error::ErrorCode::INVALID_SOURCE, {{"PATH", src.path}}); 1298 | } 1299 | fs::path canonical = fs::weakly_canonical(path_input); 1300 | canonical_sources.push_back(canonical); 1301 | is_directory_flags.push_back(fs::is_directory(canonical)); 1302 | } 1303 | 1304 | bool single_contents_mode = (sources.size() == 1 && sources.front().include_contents && is_directory_flags.front()); 1305 | fs::path base_dir; 1306 | std::vector items_to_archive; 1307 | items_to_archive.reserve(sources.size()); 1308 | 1309 | // Start progress tracking 1310 | if (options.benchmark) { 1311 | progress::start_operation(); 1312 | progress::set_original_size(calculate_sources_size(canonical_sources)); 1313 | progress::set_thread_count(options.thread_count > 0 ? options.thread_count : 1); 1314 | } 1315 | 1316 | if (options.verbose && options.thread_count > 1) { 1317 | std::cout << i18n::get("threads_info", {{"COUNT", std::to_string(options.thread_count)}}) << std::endl; 1318 | } 1319 | 1320 | if (single_contents_mode) { 1321 | base_dir = canonical_sources.front(); 1322 | items_to_archive.push_back("."); 1323 | } else { 1324 | base_dir = determine_common_base(canonical_sources); 1325 | for (size_t idx = 0; idx < canonical_sources.size(); ++idx) { 1326 | const auto& canonical = canonical_sources[idx]; 1327 | std::error_code ec; 1328 | fs::path relative = fs::relative(canonical, base_dir, ec); 1329 | if (ec || relative.empty() || relative == ".") { 1330 | fs::path fallback = canonical.filename(); 1331 | if (fallback.empty()) { 1332 | fallback = canonical; 1333 | } 1334 | relative = fallback; 1335 | } 1336 | items_to_archive.push_back(relative.string()); 1337 | } 1338 | } 1339 | 1340 | std::string tool; 1341 | std::vector args; 1342 | std::string working_dir_for_cmd = base_dir.empty() ? "" : base_dir.string(); 1343 | if (working_dir_for_cmd.empty()) { 1344 | working_dir_for_cmd = fs::current_path().string(); 1345 | } 1346 | 1347 | switch (target_format) { 1348 | case file_type::FileType::ARCHIVE_TAR: 1349 | case file_type::FileType::ARCHIVE_TAR_GZ: 1350 | case file_type::FileType::ARCHIVE_TAR_BZ2: 1351 | case file_type::FileType::ARCHIVE_TAR_XZ: 1352 | if (!password.empty()) std::cout << i18n::get("warning_tar_password") << std::endl; 1353 | tool = "tar"; 1354 | if (!is_tool_available(tool)) error::throw_error(error::ErrorCode::TOOL_NOT_FOUND, {{"TOOL_NAME", tool}}); 1355 | { 1356 | std::string flags; 1357 | if (target_format == file_type::FileType::ARCHIVE_TAR) flags = "-cf"; 1358 | if (target_format == file_type::FileType::ARCHIVE_TAR_GZ) flags = "-czf"; 1359 | if (target_format == file_type::FileType::ARCHIVE_TAR_BZ2) flags = "-cjf"; 1360 | if (target_format == file_type::FileType::ARCHIVE_TAR_XZ) flags = "-cJf"; 1361 | args = {flags, fs::absolute(target_path_str).string()}; 1362 | args.insert(args.end(), items_to_archive.begin(), items_to_archive.end()); 1363 | } 1364 | break; 1365 | case file_type::FileType::ARCHIVE_ZIP: 1366 | tool = "zip"; 1367 | if (!is_tool_available(tool)) error::throw_error(error::ErrorCode::TOOL_NOT_FOUND, {{"TOOL_NAME", tool}}); 1368 | if (!password.empty()) args.insert(args.end(), {"-P", password}); 1369 | if (options.compression_level > 0) { 1370 | args.push_back("-" + std::to_string(options.compression_level)); 1371 | } 1372 | args.push_back("-r"); // Recurse into directories 1373 | args.push_back(fs::absolute(target_path_str).string()); 1374 | args.insert(args.end(), items_to_archive.begin(), items_to_archive.end()); 1375 | break; 1376 | case file_type::FileType::ARCHIVE_7Z: 1377 | tool = "7z"; 1378 | if (!is_tool_available(tool)) error::throw_error(error::ErrorCode::TOOL_NOT_FOUND, {{"TOOL_NAME", tool}}); 1379 | args.push_back("a"); // Add to archive 1380 | if (!password.empty()) args.push_back("-p" + password); 1381 | if (options.compression_level > 0) { 1382 | args.push_back("-mx=" + std::to_string(options.compression_level)); 1383 | } 1384 | args.push_back(fs::absolute(target_path_str).string()); 1385 | args.insert(args.end(), items_to_archive.begin(), items_to_archive.end()); 1386 | break; 1387 | case file_type::FileType::ARCHIVE_LZ4: 1388 | tool = "lz4"; 1389 | if (!is_tool_available(tool)) error::throw_error(error::ErrorCode::TOOL_NOT_FOUND, {{"TOOL_NAME", tool}}); 1390 | if (options.compression_level > 0) { 1391 | args.push_back("-" + std::to_string(options.compression_level)); 1392 | } 1393 | args.push_back("-r"); // Recursive 1394 | if (items_to_archive.size() != 1) { 1395 | error::throw_error(error::ErrorCode::UNKNOWN_FORMAT, {{"INFO", "Multiple sources are not supported for lz4 compression."}}); 1396 | } 1397 | args.push_back(items_to_archive.front()); 1398 | args.push_back(fs::absolute(target_path_str).string()); 1399 | break; 1400 | case file_type::FileType::ARCHIVE_ZSTD: 1401 | tool = "zstd"; 1402 | if (!is_tool_available(tool)) error::throw_error(error::ErrorCode::TOOL_NOT_FOUND, {{"TOOL_NAME", tool}}); 1403 | if (options.compression_level > 0) { 1404 | args.push_back("-" + std::to_string(options.compression_level)); 1405 | } 1406 | args.push_back("-r"); // Recursive 1407 | if (items_to_archive.size() != 1) { 1408 | error::throw_error(error::ErrorCode::UNKNOWN_FORMAT, {{"INFO", "Multiple sources are not supported for zstd compression."}}); 1409 | } 1410 | args.push_back(items_to_archive.front()); 1411 | args.push_back("-o"); 1412 | args.push_back(fs::absolute(target_path_str).string()); 1413 | break; 1414 | case file_type::FileType::ARCHIVE_XAR: 1415 | tool = "xar"; 1416 | if (!is_tool_available(tool)) error::throw_error(error::ErrorCode::TOOL_NOT_FOUND, {{"TOOL_NAME", tool}}); 1417 | args.push_back("-cf"); 1418 | args.push_back(fs::absolute(target_path_str).string()); 1419 | args.insert(args.end(), items_to_archive.begin(), items_to_archive.end()); 1420 | break; 1421 | default: 1422 | error::throw_error(error::ErrorCode::UNKNOWN_FORMAT, {{"INFO", "Unsupported target format for compression."}}); 1423 | } 1424 | 1425 | std::cout << i18n::get("compressing") << std::endl; 1426 | int result = execute_command(tool, args, working_dir_for_cmd); 1427 | if (result != 0) { 1428 | error::throw_error(error::ErrorCode::OPERATION_FAILED, {{"COMMAND", tool}, {"EXIT_CODE", std::to_string(result)}}); 1429 | } 1430 | 1431 | // End progress tracking and collect stats 1432 | if (options.benchmark) { 1433 | progress::end_operation(); 1434 | std::error_code ec; 1435 | if (fs::exists(target_path_str)) { 1436 | auto size = fs::file_size(target_path_str, ec); 1437 | if (!ec) { 1438 | progress::set_compressed_size(size); 1439 | } 1440 | } 1441 | } 1442 | 1443 | // Verify archive integrity if requested 1444 | if (options.verify) { 1445 | std::cout << i18n::get("verifying") << std::endl; 1446 | if (verify_archive(target_path_str, target_format)) { 1447 | std::cout << i18n::get("verification_success") << std::endl; 1448 | } else { 1449 | std::cout << i18n::get("verification_failed") << std::endl; 1450 | } 1451 | } 1452 | 1453 | std::cout << i18n::get("operation_complete") << std::endl; 1454 | 1455 | // Print performance statistics 1456 | if (options.benchmark || options.verbose) { 1457 | progress::print_stats(options.verbose, options.benchmark); 1458 | } 1459 | } 1460 | 1461 | void compress(const std::string& source_path_str, const std::string& target_path_str, 1462 | file_type::FileType target_format, const std::string& password, 1463 | const args::Options& options = {}) { 1464 | bool has_trailing_slash = !source_path_str.empty() && (source_path_str.back() == '/' || source_path_str.back() == '\\'); 1465 | CompressionSource src{source_path_str, has_trailing_slash}; 1466 | compress({src}, target_path_str, target_format, password, options); 1467 | } 1468 | 1469 | void decompress(const std::string& source_path, const std::string& target_dir_path, 1470 | file_type::FileType source_type, const std::string& password, 1471 | const args::Options& options = {}) { 1472 | if (!fs::exists(target_dir_path)) { 1473 | try { fs::create_directories(target_dir_path); } 1474 | catch (const fs::filesystem_error& e) { error::throw_error(error::ErrorCode::INVALID_TARGET, {{"PATH", target_dir_path}, {"REASON", e.what()}}); } 1475 | } 1476 | 1477 | std::string tool; 1478 | std::vector args; 1479 | 1480 | switch (source_type) { 1481 | case file_type::FileType::ARCHIVE_TAR: 1482 | case file_type::FileType::ARCHIVE_TAR_GZ: 1483 | case file_type::FileType::ARCHIVE_TAR_BZ2: 1484 | case file_type::FileType::ARCHIVE_TAR_XZ: 1485 | if (!password.empty()) std::cout << i18n::get("warning_tar_password") << std::endl; 1486 | tool = "tar"; 1487 | if (!is_tool_available(tool)) error::throw_error(error::ErrorCode::TOOL_NOT_FOUND, {{"TOOL_NAME", tool}}); 1488 | { 1489 | std::string flags; 1490 | if (source_type == file_type::FileType::ARCHIVE_TAR) flags = "-xf"; 1491 | if (source_type == file_type::FileType::ARCHIVE_TAR_GZ) flags = "-xzf"; 1492 | if (source_type == file_type::FileType::ARCHIVE_TAR_BZ2) flags = "-xjf"; 1493 | if (source_type == file_type::FileType::ARCHIVE_TAR_XZ) flags = "-xJf"; 1494 | args = {flags, fs::absolute(source_path).string(), "-C", fs::absolute(target_dir_path).string()}; 1495 | } 1496 | break; 1497 | case file_type::FileType::ARCHIVE_ZIP: 1498 | if (is_split_zip(source_path)) { 1499 | // Split ZIP archives require 7z (unzip doesn't support them) 1500 | tool = "7z"; 1501 | if (!is_tool_available(tool)) { 1502 | // Provide specific error for split ZIP 1503 | throw error::HitpagException(error::ErrorCode::TOOL_NOT_FOUND, 1504 | i18n::get("error_split_zip_requires_7z")); 1505 | } 1506 | 1507 | // If user provided a split part (.z01, .z02), find the main .zip file 1508 | std::string actual_source = source_path; 1509 | if (is_split_zip_part(source_path)) { 1510 | actual_source = find_split_zip_main(source_path); 1511 | if (actual_source.empty()) { 1512 | error::throw_error(error::ErrorCode::INVALID_SOURCE, 1513 | {{"PATH", fs::path(source_path).replace_extension(".zip").string()}, 1514 | {"REASON", i18n::get("error_split_zip_main_not_found", 1515 | {{"PATH", fs::path(source_path).replace_extension(".zip").string()}})}}); 1516 | } 1517 | } 1518 | 1519 | if (options.verbose) { 1520 | std::cout << i18n::get("info_split_zip_detected") << std::endl; 1521 | } 1522 | 1523 | build_7z_extract_args(args, actual_source, target_dir_path, password); 1524 | } else { 1525 | tool = "unzip"; 1526 | if (!is_tool_available(tool)) error::throw_error(error::ErrorCode::TOOL_NOT_FOUND, {{"TOOL_NAME", tool}}); 1527 | if (!password.empty()) args.insert(args.end(), {"-P", password}); 1528 | args.insert(args.end(), {"-o", fs::absolute(source_path).string(), "-d", fs::absolute(target_dir_path).string()}); 1529 | } 1530 | break; 1531 | case file_type::FileType::ARCHIVE_RAR: 1532 | tool = "unrar"; 1533 | if (!is_tool_available(tool)) error::throw_error(error::ErrorCode::TOOL_NOT_FOUND, {{"TOOL_NAME", "unrar"}}); 1534 | args.push_back("x"); // eXtract with full paths 1535 | if (!password.empty()) args.push_back("-p" + password); 1536 | args.insert(args.end(), {"-o+", fs::absolute(source_path).string(), fs::absolute(target_dir_path).string()}); // -o+: overwrite existing 1537 | break; 1538 | case file_type::FileType::ARCHIVE_7Z: 1539 | tool = "7z"; 1540 | if (!is_tool_available(tool)) error::throw_error(error::ErrorCode::TOOL_NOT_FOUND, {{"TOOL_NAME", tool}}); 1541 | build_7z_extract_args(args, source_path, target_dir_path, password); 1542 | break; 1543 | case file_type::FileType::ARCHIVE_LZ4: 1544 | tool = "lz4"; 1545 | if (!is_tool_available(tool)) error::throw_error(error::ErrorCode::TOOL_NOT_FOUND, {{"TOOL_NAME", tool}}); 1546 | args.push_back("-d"); // Decompress 1547 | args.push_back(fs::absolute(source_path).string()); 1548 | args.push_back(fs::absolute(target_dir_path).string()); 1549 | break; 1550 | case file_type::FileType::ARCHIVE_ZSTD: 1551 | tool = "zstd"; 1552 | if (!is_tool_available(tool)) error::throw_error(error::ErrorCode::TOOL_NOT_FOUND, {{"TOOL_NAME", tool}}); 1553 | args.push_back("-d"); // Decompress 1554 | args.push_back(fs::absolute(source_path).string()); 1555 | args.push_back("-o"); 1556 | args.push_back(fs::absolute(target_dir_path).string()); 1557 | break; 1558 | case file_type::FileType::ARCHIVE_XAR: 1559 | tool = "xar"; 1560 | if (!is_tool_available(tool)) error::throw_error(error::ErrorCode::TOOL_NOT_FOUND, {{"TOOL_NAME", tool}}); 1561 | args.push_back("-xf"); 1562 | args.push_back(fs::absolute(source_path).string()); 1563 | args.push_back("-C"); 1564 | args.push_back(fs::absolute(target_dir_path).string()); 1565 | break; 1566 | default: 1567 | error::throw_error(error::ErrorCode::UNKNOWN_FORMAT, {{"INFO", "Unsupported source format for decompression."}}); 1568 | } 1569 | 1570 | std::cout << i18n::get("decompressing") << std::endl; 1571 | int result = execute_command(tool, args); 1572 | if (result != 0) { 1573 | error::throw_error(error::ErrorCode::OPERATION_FAILED, {{"COMMAND", tool}, {"EXIT_CODE", std::to_string(result)}}); 1574 | } 1575 | std::cout << i18n::get("operation_complete") << std::endl; 1576 | } 1577 | } 1578 | 1579 | /** 1580 | * @brief Interactive mode user interface module. 1581 | * 1582 | * Guides the user through the compression/decompression process with a series of prompts. 1583 | */ 1584 | namespace interactive { 1585 | /** 1586 | * @brief Helper to get trimmed input from the user. 1587 | * 1588 | * [MODIFIED] Now handles EOF (end-of-file) from stdin gracefully by throwing 1589 | * an exception, preventing infinite loops if the input stream is closed. 1590 | * @return The trimmed user input string. 1591 | * @throws std::runtime_error if the input stream is closed (EOF). 1592 | */ 1593 | std::string get_input() { 1594 | std::string input; 1595 | if (!std::getline(std::cin, input)) { 1596 | // If getline fails, it's likely due to EOF (e.g., Ctrl+D). 1597 | // Throw an exception to allow the main loop to catch it and exit gracefully. 1598 | throw std::runtime_error(i18n::get("error_input_stream_closed")); 1599 | } 1600 | if (!input.empty()) { 1601 | input.erase(0, input.find_first_not_of(" \t\n\r")); 1602 | input.erase(input.find_last_not_of(" \t\n\r") + 1); 1603 | } 1604 | return input; 1605 | } 1606 | 1607 | /** 1608 | * @brief Securely reads a password from the terminal without echoing characters. 1609 | * 1610 | * Provides visual feedback ('*') for each character typed. 1611 | * Implemented for both Windows and POSIX systems. 1612 | * @param prompt The message to display to the user. 1613 | * @return The entered password. 1614 | */ 1615 | std::string get_password_interactively(std::string_view prompt) { 1616 | std::cout << prompt << std::flush; 1617 | std::string password; 1618 | #ifdef _WIN32 1619 | char ch; 1620 | while ((ch = _getch()) != '\r') { // Enter key 1621 | if (ch == '\b') { // Backspace 1622 | if (!password.empty()) { 1623 | password.pop_back(); 1624 | std::cout << "\b \b"; 1625 | } 1626 | } else { 1627 | password += ch; 1628 | std::cout << '*'; 1629 | } 1630 | } 1631 | #else 1632 | termios oldt, newt; 1633 | if (tcgetattr(STDIN_FILENO, &oldt) != 0) { 1634 | // Failed to get terminal attributes, fallback to regular input 1635 | std::getline(std::cin, password); 1636 | return password; 1637 | } 1638 | 1639 | newt = oldt; 1640 | newt.c_lflag &= ~(ECHO | ICANON); // Disable echoing and canonical mode 1641 | if (tcsetattr(STDIN_FILENO, TCSANOW, &newt) != 0) { 1642 | // Failed to set terminal attributes, fallback to regular input 1643 | std::getline(std::cin, password); 1644 | return password; 1645 | } 1646 | 1647 | // Use RAII to ensure terminal settings are restored 1648 | struct TerminalRestorer { 1649 | const termios& old_settings; 1650 | TerminalRestorer(const termios& old) : old_settings(old) {} 1651 | ~TerminalRestorer() { tcsetattr(STDIN_FILENO, TCSANOW, &old_settings); } 1652 | } restorer(oldt); 1653 | 1654 | char ch; 1655 | while (read(STDIN_FILENO, &ch, 1) == 1 && ch != '\n') { 1656 | if (ch == 127 || ch == '\b') { // Handle backspace/delete 1657 | if (!password.empty()) { 1658 | password.pop_back(); 1659 | std::cout << "\b \b" << std::flush; 1660 | } 1661 | } else { 1662 | password += ch; 1663 | std::cout << '*' << std::flush; 1664 | } 1665 | } 1666 | // Terminal will be restored automatically by destructor 1667 | #endif 1668 | std::cout << std::endl; 1669 | return password; 1670 | } 1671 | 1672 | // Gets a valid integer choice from the user within a specified range. 1673 | int get_choice(int min_val, int max_val) { 1674 | while (true) { 1675 | std::cout << "> "; 1676 | std::string input = get_input(); 1677 | try { 1678 | int choice = std::stoi(input); 1679 | if (choice >= min_val && choice <= max_val) return choice; 1680 | } catch (...) {} 1681 | std::cout << i18n::get("invalid_choice") << std::endl; 1682 | } 1683 | } 1684 | 1685 | // Gets a yes/no confirmation from the user. 1686 | bool get_confirmation(std::string_view prompt_key, const std::map& placeholders = {}) { 1687 | std::cout << i18n::get(prompt_key, placeholders); 1688 | while (true) { 1689 | std::string input = get_input(); 1690 | if (!input.empty()) { 1691 | char choice = static_cast(std::tolower(static_cast(input[0]))); 1692 | if (choice == 'y') return true; 1693 | if (choice == 'n') return false; 1694 | } 1695 | std::cout << i18n::get("invalid_choice") << " (y/n): "; 1696 | } 1697 | } 1698 | 1699 | // Data-driven menu item structure for format selection. 1700 | struct MenuItem { 1701 | std::string key; 1702 | file_type::FileType type; 1703 | bool supports_password; 1704 | }; 1705 | 1706 | void run(args::Options& options) { 1707 | std::cout << i18n::get("interactive_mode") << std::endl; 1708 | 1709 | if (options.source_path.empty()) { 1710 | std::cout << "Please enter source path: "; 1711 | options.source_path = get_input(); 1712 | } 1713 | 1714 | file_type::FileType source_type = file_type::recognize_source_type(options.source_path); 1715 | 1716 | std::cout << "Source: " << options.source_path << " (" << file_type::get_file_type_string(source_type) << ")" << std::endl; 1717 | 1718 | file_type::OperationType op_type = (source_type == file_type::FileType::DIRECTORY || source_type == file_type::FileType::REGULAR_FILE) 1719 | ? file_type::OperationType::COMPRESS : file_type::OperationType::DECOMPRESS; 1720 | 1721 | std::cout << "Detected operation: " << (op_type == file_type::OperationType::COMPRESS ? "Compress" : "Decompress") << ". Change? (y/n): "; 1722 | std::string change_op_input = get_input(); 1723 | if (!change_op_input.empty() && std::tolower(static_cast(change_op_input[0])) == 'y') { 1724 | std::cout << i18n::get("ask_operation") << std::endl << i18n::get("operation_compress") << std::endl << i18n::get("operation_decompress") << std::endl; 1725 | op_type = (get_choice(1, 2) == 1) ? file_type::OperationType::COMPRESS : file_type::OperationType::DECOMPRESS; 1726 | } 1727 | 1728 | file_type::FileType target_format = file_type::FileType::UNKNOWN; 1729 | 1730 | if (op_type == file_type::OperationType::COMPRESS) { 1731 | const std::vector formats = { 1732 | {"format_tar_gz", file_type::FileType::ARCHIVE_TAR_GZ, false}, 1733 | {"format_zip", file_type::FileType::ARCHIVE_ZIP, true}, 1734 | {"format_7z", file_type::FileType::ARCHIVE_7Z, true}, 1735 | {"format_tar", file_type::FileType::ARCHIVE_TAR, false}, 1736 | {"format_tar_bz2", file_type::FileType::ARCHIVE_TAR_BZ2, false}, 1737 | {"format_tar_xz", file_type::FileType::ARCHIVE_TAR_XZ, false}, 1738 | {"format_lz4", file_type::FileType::ARCHIVE_LZ4, false}, 1739 | {"format_zstd", file_type::FileType::ARCHIVE_ZSTD, false}, 1740 | {"format_xar", file_type::FileType::ARCHIVE_XAR, false} 1741 | }; 1742 | std::cout << i18n::get("ask_format") << std::endl; 1743 | for(size_t i = 0; i < formats.size(); ++i) std::cout << i+1 << ". " << i18n::get(formats[i].key) << std::endl; 1744 | int choice = get_choice(1, formats.size()); 1745 | const auto& selected_format = formats[choice - 1]; 1746 | target_format = selected_format.type; 1747 | 1748 | std::cout << "Please enter target archive path: "; 1749 | options.target_path = get_input(); 1750 | if (options.target_path.empty()) error::throw_error(error::ErrorCode::INVALID_TARGET, {{"REASON", "Target path cannot be empty"}}); 1751 | 1752 | if (selected_format.supports_password && options.password.empty()) { 1753 | if (get_confirmation("ask_set_password")) { 1754 | while (true) { 1755 | std::string p1 = get_password_interactively(i18n::get("enter_password")); 1756 | std::string p2 = get_password_interactively(i18n::get("confirm_password")); 1757 | if (p1 == p2) { options.password = p1; break; } 1758 | else { std::cout << i18n::get("password_mismatch") << std::endl; } 1759 | } 1760 | } 1761 | } 1762 | } else { // Decompress 1763 | if (options.password.empty()) { 1764 | if (get_confirmation("ask_has_password")) { 1765 | options.password = get_password_interactively(i18n::get("enter_password")); 1766 | } 1767 | } 1768 | std::cout << "Please enter target directory (default: './'): "; 1769 | options.target_path = get_input(); 1770 | if (options.target_path.empty()) options.target_path = "."; 1771 | } 1772 | 1773 | const auto interactive_input_adapter = []() { return get_input(); }; 1774 | const auto interactive_output_adapter = [](const std::string& message) { std::cout << message << std::flush; }; 1775 | const auto interactive_error_adapter = [](const std::string& message) { std::cerr << message << std::flush; }; 1776 | 1777 | if (!target_path::resolve_existing_target(options.target_path, interactive_input_adapter, interactive_output_adapter, interactive_error_adapter)) { 1778 | std::cout << i18n::get("operation_canceled") << std::endl; 1779 | return; 1780 | } 1781 | 1782 | bool delete_source = get_confirmation("ask_delete_source", {{"SOURCE_PATH", options.source_path}}); 1783 | 1784 | if (op_type == file_type::OperationType::COMPRESS) { 1785 | operation::compress(options.source_path, options.target_path, target_format, options.password, options); 1786 | } else { 1787 | operation::decompress(options.source_path, options.target_path, source_type, options.password, options); 1788 | } 1789 | 1790 | if (delete_source) { 1791 | std::cout << "Deleting source: " << options.source_path << std::endl; 1792 | std::error_code ec; 1793 | fs::remove_all(options.source_path, ec); 1794 | if (ec) std::cerr << "Warning: Failed to delete source '" << options.source_path << "': " << ec.message() << std::endl; 1795 | else std::cout << "Source deleted." << std::endl; 1796 | } 1797 | } 1798 | } 1799 | 1800 | /** 1801 | * @brief Main entry point of the application. 1802 | * 1803 | * Orchestrates the entire workflow: 1804 | * 1. Parses command-line arguments. 1805 | * 2. Handles help and version flags. 1806 | * 3. Dispatches to interactive or command-line mode. 1807 | * 4. Catches all exceptions for graceful error reporting and exit. 1808 | */ 1809 | int main(int argc, char* argv[]) { 1810 | try { 1811 | args::Options options = args::parse(argc, argv); 1812 | 1813 | if (options.show_help) { 1814 | args::show_help(); 1815 | return 0; 1816 | } 1817 | if (options.show_version) { 1818 | args::show_version(); 1819 | return 0; 1820 | } 1821 | 1822 | if (options.password_prompt) { 1823 | options.password = interactive::get_password_interactively(i18n::get("enter_password")); 1824 | } 1825 | 1826 | if (options.interactive_mode) { 1827 | interactive::run(options); 1828 | } else { 1829 | const auto cli_input_adapter = []() { return cli_io::get_input(); }; 1830 | const auto cli_output_adapter = [](const std::string& message) { std::cout << message << std::flush; }; 1831 | const auto cli_error_adapter = [](const std::string& message) { std::cerr << message << std::flush; }; 1832 | 1833 | if (options.source_paths.size() > 1) { 1834 | // Ensure target is not the same as any source. 1835 | if (fs::exists(options.target_path)) { 1836 | for (const auto& src : options.source_paths) { 1837 | if (fs::exists(src)) { 1838 | std::error_code ec; 1839 | if (fs::equivalent(src, options.target_path, ec) && !ec) { 1840 | error::throw_error(error::ErrorCode::SAME_PATH); 1841 | } 1842 | } 1843 | } 1844 | } 1845 | 1846 | file_type::FileType target_type = file_type::recognize_by_extension(options.target_path); 1847 | if (!options.force_format.empty()) { 1848 | file_type::FileType forced_type = file_type::parse_format_string(options.force_format); 1849 | if (forced_type == file_type::FileType::UNKNOWN) { 1850 | error::throw_error(error::ErrorCode::UNKNOWN_FORMAT, {{"INFO", "Invalid format specified: " + options.force_format}}); 1851 | } 1852 | target_type = forced_type; 1853 | } 1854 | 1855 | if (target_type == file_type::FileType::UNKNOWN) { 1856 | error::throw_error(error::ErrorCode::UNKNOWN_FORMAT, {{"INFO", "Target format could not be determined. Please specify --format or use archive extension in target path."}}); 1857 | } 1858 | 1859 | if (!target_path::resolve_existing_target(options.target_path, cli_input_adapter, cli_output_adapter, cli_error_adapter)) { 1860 | std::cout << i18n::get("operation_canceled") << std::endl; 1861 | std::cout << i18n::get("goodbye") << std::endl; 1862 | return 0; 1863 | } 1864 | 1865 | std::vector compression_sources; 1866 | compression_sources.reserve(options.source_paths.size()); 1867 | for (const auto& src : options.source_paths) { 1868 | compression_sources.push_back({src, false}); 1869 | } 1870 | 1871 | operation::compress(compression_sources, options.target_path, target_type, options.password, options); 1872 | } else { 1873 | // Prevent operating on the same file/directory 1874 | if (fs::exists(options.source_path) && fs::exists(options.target_path)) { 1875 | std::error_code ec; 1876 | if (fs::equivalent(options.source_path, options.target_path, ec) && !ec) { 1877 | error::throw_error(error::ErrorCode::SAME_PATH); 1878 | } 1879 | } 1880 | 1881 | file_type::RecognitionResult result = file_type::recognize(options.source_path, options.target_path); 1882 | 1883 | // Override target format if manually specified 1884 | if (!options.force_format.empty()) { 1885 | file_type::FileType forced_type = file_type::parse_format_string(options.force_format); 1886 | if (forced_type == file_type::FileType::UNKNOWN) { 1887 | error::throw_error(error::ErrorCode::UNKNOWN_FORMAT, {{"INFO", "Invalid format specified: " + options.force_format}}); 1888 | } 1889 | 1890 | // For compression, override the target format 1891 | if (result.operation == file_type::OperationType::COMPRESS) { 1892 | result.target_type_hint = forced_type; 1893 | } else { 1894 | // For decompression, override the source type detection 1895 | result.source_type = forced_type; 1896 | } 1897 | } 1898 | 1899 | // Check if we need format specification for compression 1900 | if (result.operation == file_type::OperationType::COMPRESS && 1901 | result.target_type_hint == file_type::FileType::UNKNOWN) { 1902 | error::throw_error(error::ErrorCode::UNKNOWN_FORMAT, {{"INFO", "Target format could not be determined. Please specify --format or use archive extension in target path."}}); 1903 | } 1904 | 1905 | if (!target_path::resolve_existing_target(options.target_path, cli_input_adapter, cli_output_adapter, cli_error_adapter)) { 1906 | std::cout << i18n::get("operation_canceled") << std::endl; 1907 | std::cout << i18n::get("goodbye") << std::endl; 1908 | return 0; 1909 | } 1910 | 1911 | if (result.operation == file_type::OperationType::COMPRESS) { 1912 | operation::compress(options.source_path, options.target_path, result.target_type_hint, options.password, options); 1913 | } else if (result.operation == file_type::OperationType::DECOMPRESS) { 1914 | operation::decompress(options.source_path, options.target_path, result.source_type, options.password, options); 1915 | } 1916 | } 1917 | } 1918 | 1919 | std::cout << i18n::get("goodbye") << std::endl; 1920 | 1921 | } catch (const error::HitpagException& e) { 1922 | std::cerr << e.what() << std::endl; 1923 | return static_cast(e.code()); 1924 | } catch (const std::exception& e) { 1925 | // This will catch standard exceptions, including the one thrown by get_input() on EOF. 1926 | std::cerr << "An unexpected error occurred: " << e.what() << std::endl; 1927 | return static_cast(error::ErrorCode::UNKNOWN_ERROR); 1928 | } catch (...) { 1929 | std::cerr << "An unknown, non-standard error occurred." << std::endl; 1930 | return static_cast(error::ErrorCode::UNKNOWN_ERROR); 1931 | } 1932 | 1933 | return 0; 1934 | } 1935 | --------------------------------------------------------------------------------